diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 9099689136..0000000000 --- a/.editorconfig +++ /dev/null @@ -1,11 +0,0 @@ -# http://editorconfig.org - -root = true - -[*] -charset = utf-8 -indent_style = space -indent_size = 2 -end_of_line = lf -insert_final_newline = true -trim_trailing_whitespace = true diff --git a/.gitattributes b/.gitattributes index 88d3ce27e8..83cb362591 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,21 +1,2 @@ -# CRLF +# FUCK CRLF * text eol=lf - -# Force images/fonts to be handled as binaries -*.jpg binary -*.jpeg binary -*.gif binary -*.png binary -*.t3x binary -*.t3d binary -*.exe binary -*.data binary -*.ttf binary -*.eof binary -*.eot binary -*.swf binary -*.mov binary -*.mp4 binary -*.mp3 binary -*.ogg binary -*.flv binary diff --git a/.github/ISSUE_TEMPLATE/Bug_report.md b/.github/ISSUE_TEMPLATE/Bug_report.md deleted file mode 100644 index 5f18f1b969..0000000000 --- a/.github/ISSUE_TEMPLATE/Bug_report.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve - ---- - -**Description:** - - - - -**Steps to Reproduce:** - - - - -**Output:** - - - - -**My `ionic info`:** - - -``` - -``` - - -**Other Information:** - diff --git a/.github/ISSUE_TEMPLATE/Feature_request.md b/.github/ISSUE_TEMPLATE/Feature_request.md deleted file mode 100644 index 7ebfb74202..0000000000 --- a/.github/ISSUE_TEMPLATE/Feature_request.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project - ---- - - diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index d0a9746f65..0000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,6 +0,0 @@ -version: 2 -updates: -- package-ecosystem: npm - directory: "/" - schedule: - interval: daily diff --git a/.github/ionic-issue-bot.yml b/.github/ionic-issue-bot.yml deleted file mode 100644 index 0bbe69e835..0000000000 --- a/.github/ionic-issue-bot.yml +++ /dev/null @@ -1,128 +0,0 @@ -triage: - label: triage - dryRun: false - -closeAndLock: - labels: - - label: "ionitron: support" - message: > - Thanks for the issue! This issue appears to be a support request. We - use this issue tracker exclusively for bug reports and feature - requests. For support questions, please see our [Support - Page](https://ionicframework.com/support). - - - Thank you for using Ionic! - - label: "ionitron: pro" - message: > - Thanks for the issue! This issue appears to be related to our - commercial products. We use this issue tracker exclusively for bug - reports and feature requests. Please [open a support - ticket](https://ionicframework.com/support/request) and we'll be happy - to assist you. - - - Thank you for using Ionic! - - label: "ionitron: missing template" - message: > - Thanks for the issue! It appears that you have not filled out the - provided issue template. We use this issue template in order to gather - more information and further assist you. Please create a new issue and - ensure the template is fully filled out. - - - Thank you for using Ionic! - - label: "ionitron: old major version" - message: > - Thanks for the issue! This issue appears to be associated with an old - version of the Ionic CLI. Please update to the latest CLI version, - which supports all versions of the Ionic Framework. If the issue is - relevant and if it persists after updating to the latest CLI version, - please create a new issue. - - - Thank you for using Ionic! - - label: "ionitron: cordova" - message: > - Thanks for the issue! We use this issue tracker exclusively for bug - reports and feature requests for the Ionic CLI. It appears that this - issue is associated with Cordova. - - - If you need Cordova support, please try asking in the [Ionic - Forum](https://forum.ionicframework.com/). You can visit our [Support - Page](https://ionicframework.com/support) for all options. If you believe - this is an issue with Cordova, please see Cordova's [Reporting - Issues](https://cordova.apache.org/contribute/issues.html) page. - - - Thank you for using Ionic! - close: true - lock: true - dryRun: false - -# stale: -# days: 365 -# maxIssuesPerRun: 100 -# exemptLabels: -# - good first issue -# - triage -# exemptProjects: true -# exemptMilestones: true -# label: "ionitron: stale issue" -# message: > -# Thanks for the issue! This issue is being closed due to inactivity. If this -# is still an issue with the latest version of Ionic, please create a new -# issue and ensure the template is fully filled out. - - -# Thank you for using Ionic! -# close: true -# lock: true -# dryRun: false - -wrongRepo: - repos: - - label: "ionitron: framework" - repo: ionic - message: > - Thanks for the issue! We use this issue tracker exclusively for bug - reports and feature requests for the Ionic CLI. It appears that this - issue is associated with the Ionic Framework. I am moving this issue to - the Ionic Framework repository. Please track this issue over there. - - - Thank you for using Ionic! - - label: "ionitron: capacitor" - repo: capacitor - message: > - Thanks for the issue! We use this issue tracker exclusively for bug - reports and feature requests for the Ionic CLI. It appears that this - issue is associated with Capacitor. I am moving this issue to the - Capacitor repository. Please track this issue over there. - - - Thank you for using Ionic! - - label: "ionitron: ionic-native" - repo: ionic-native - message: > - Thanks for the issue! We use this issue tracker exclusively for bug - reports and feature requests for the Ionic CLI. It appears that this - issue is associated with Ionic Native. I am moving this issue to the - Ionic Native repository. Please track this issue over there. - - - Thank you for using Ionic! - - label: "ionitron: ionic-site" - repo: ionic-site - message: > - Thanks for the issue! We use this issue tracker exclusively for bug - reports and feature requests for the Ionic CLI. It appears that this - issue is associated with the Ionic website. I am moving this issue to - the Ionic website repository. Please track this issue over there. - - - Thank you for using Ionic! - close: true - lock: true - dryRun: false diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml deleted file mode 100644 index 7d82e66baf..0000000000 --- a/.github/workflows/cd.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: CD - -on: - push: - branches: - - stable - -permissions: - contents: write - id-token: write - packages: write - -jobs: - build: - name: Build, Test, and Deploy - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - uses: actions/setup-node@v3 - with: - node-version: 18 - registry-url: https://registry.npmjs.org/ - cache: npm - cache-dependency-path: '**/package.json' - - run: npm install - - run: npm run bootstrap - - run: npm run publish:ci - env: - GIT_AUTHOR_NAME: Ionitron - GIT_AUTHOR_EMAIL: hi@ionicframework.com - GIT_COMMITTER_NAME: Ionitron - GIT_COMMITTER_EMAIL: hi@ionicframework.com - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - name: Sleep while npm takes its time - run: sleep 20 - - name: GitHub Container Registry Login - run: echo ${{ github.token }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin - - name: Build Container - run: | - docker build \ - --build-arg IONIC_CLI_VERSION=$(npm info @ionic/cli dist-tags.latest) \ - --tag ghcr.io/${{ github.repository }}:latest \ - --tag ghcr.io/${{ github.repository}}:$(npm info @ionic/cli dist-tags.latest) \ - . - - name: Push Container as latest - run: docker push ghcr.io/${{ github.repository }}:latest - - name: Push Container as version - run: docker push ghcr.io/${{ github.repository }}:$(npm info @ionic/cli dist-tags.latest) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index dc65031b87..0000000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: CI - -on: - push: - branches-ignore: - - stable - pull_request: - branches-ignore: - - stable - -jobs: - build-and-test: - name: Build and Test (Node ${{ matrix.node }}) - runs-on: ubuntu-latest - timeout-minutes: 30 - strategy: - matrix: - node: - - 18.x - - 16.x - steps: - - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node }} - - uses: actions/checkout@v3 - - name: Restore Dependency Cache - uses: actions/cache@v3 - with: - path: ~/.npm - key: ${{ runner.OS }}-dependency-cache-${{ hashFiles('**/package.json') }} - - run: npm install - - run: npm run bootstrap - - run: npm run lint - - run: npm test diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml deleted file mode 100644 index 0901147bdd..0000000000 --- a/.github/workflows/docker-image.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: BuildDockerImage - -on: workflow_dispatch - -jobs: - build-docker-container: - name: Build and Deploy Docker Container with latest CLI - runs-on: ubuntu-latest - steps: - - uses: actions/setup-node@v3 - with: - node-version: 16.x - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - name: GitHub Container Registry Login - run: echo ${{ github.token }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin - - name: Build Container - run: | - docker build \ - --build-arg IONIC_CLI_VERSION=$(npm info @ionic/cli dist-tags.latest) \ - --tag ghcr.io/${{ github.repository }}:latest \ - --tag ghcr.io/${{ github.repository}}:$(npm info @ionic/cli dist-tags.latest) \ - . - - name: Push Container as latest - run: docker push ghcr.io/${{ github.repository }}:latest - - name: Push Container as version - run: docker push ghcr.io/${{ github.repository }}:$(npm info @ionic/cli dist-tags.latest) diff --git a/.gitignore b/.gitignore index bed7a15eca..d0acd17ee4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,4 @@ -.DS_Store *.sw* *.cookies node_modules/ .idea/ -.vscode/ -coverage/ -.coveralls.yml -.changelog/ -npm-debug.log* -lerna-debug.log - -/docs -dist - -package-lock.json* diff --git a/.jscs.json b/.jscs.json new file mode 100644 index 0000000000..4e02926354 --- /dev/null +++ b/.jscs.json @@ -0,0 +1,41 @@ +{ + "excludeFiles": ["src/ngLocale/**"], + "disallowKeywords": ["with"], + "disallowMixedSpacesAndTabs": true, + "disallowMultipleLineStrings": true, + "disallowNewlineBeforeBlockStatements": true, + "disallowSpaceAfterObjectKeys": true, + "disallowSpaceAfterPrefixUnaryOperators": ["++", "--", "+", "-", "~", "!"], + "disallowSpaceBeforeBinaryOperators": [","], + "disallowSpaceBeforePostfixUnaryOperators": ["++", "--"], + "disallowSpacesInAnonymousFunctionExpression": { + "beforeOpeningRoundBrace": true + }, + "disallowSpacesInFunctionDeclaration": { + "beforeOpeningRoundBrace": true + }, + "disallowSpacesInNamedFunctionExpression": { + "beforeOpeningRoundBrace": true + }, + "disallowSpacesInsideArrayBrackets": true, + "disallowSpacesInsideParentheses": true, + "disallowTrailingComma": true, + "disallowTrailingWhitespace": true, + "requireCommaBeforeLineBreak": true, + "requireLineFeedAtFileEnd": true, + "requireSpaceAfterBinaryOperators": ["?", ":", "+", "-", "/", "*", "%", "==", "===", "!=", "!==", ">", ">=", "<", "<=", "&&", "||"], + "requireSpaceBeforeBinaryOperators": ["?", ":", "+", "-", "/", "*", "%", "==", "===", "!=", "!==", ">", ">=", "<", "<=", "&&", "||"], + "requireSpaceAfterKeywords": ["if", "else", "for", "while", "do", "switch", "return", "try", "catch"], + "requireSpaceBeforeBlockStatements": true, + "requireSpacesInConditionalExpression": { + "afterTest": true, + "beforeConsequent": true, + "afterConsequent": true, + "beforeAlternate": true + }, + "requireSpacesInFunction": { + "beforeOpeningCurlyBrace": true + }, + "validateLineBreaks": "LF", + "validateParameterSeparator": ", " +} diff --git a/.npmrc b/.npmrc deleted file mode 100644 index 43c97e719a..0000000000 --- a/.npmrc +++ /dev/null @@ -1 +0,0 @@ -package-lock=false diff --git a/CHANGELOG.md b/CHANGELOG.md index 2661d2e5a9..cf05d242ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,23 +1,441 @@ -# Changelog +### 1.7.7 -Please see each package's `CHANGELOG.md` for changes. +* `ionic-app-lib` updated to `0.6.3` -#### CLIs +### 1.7.6 -* **`@ionic/cli`**: [`CHANGELOG.md`](https://github.com/ionic-team/ionic-cli/blob/develop/packages/%40ionic/cli/CHANGELOG.md) -* **`@ionic/lab`**: [`CHANGELOG.md`](https://github.com/ionic-team/ionic-cli/blob/develop/packages/%40ionic/lab/CHANGELOG.md) -* **`@ionic/v1-toolkit`**: [`CHANGELOG.md`](https://github.com/ionic-team/ionic-cli/blob/develop/packages/%40ionic/v1-toolkit/CHANGELOG.md) +* `ionic-app-lib` updated to `0.6.2` -#### Libraries +### 1.7.5 -* **`@ionic/cli-framework`**: [`CHANGELOG.md`](https://github.com/ionic-team/ionic-cli/blob/develop/packages/%40ionic/cli-framework/CHANGELOG.md) -* **`@ionic/cli-framework-prompts`**: [`CHANGELOG.md`](https://github.com/ionic-team/ionic-cli/blob/develop/packages/%40ionic/cli-framework-prompts/CHANGELOG.md) -* **`@ionic/discover`**: [`CHANGELOG.md`](https://github.com/ionic-team/ionic-cli/blob/develop/packages/%40ionic/discover/CHANGELOG.md) -* **`@ionic/utils-array`**: [`CHANGELOG.md`](https://github.com/ionic-team/ionic-cli/blob/develop/packages/%40ionic/utils-array/CHANGELOG.md) -* **`@ionic/utils-fs`**: [`CHANGELOG.md`](https://github.com/ionic-team/ionic-cli/blob/develop/packages/%40ionic/utils-fs/CHANGELOG.md) -* **`@ionic/utils-network`**: [`CHANGELOG.md`](https://github.com/ionic-team/ionic-cli/blob/develop/packages/%40ionic/utils-network/CHANGELOG.md) -* **`@ionic/utils-object`**: [`CHANGELOG.md`](https://github.com/ionic-team/ionic-cli/blob/develop/packages/%40ionic/utils-object/CHANGELOG.md) -* **`@ionic/utils-process`**: [`CHANGELOG.md`](https://github.com/ionic-team/ionic-cli/blob/develop/packages/%40ionic/utils-process/CHANGELOG.md) -* **`@ionic/utils-stream`**: [`CHANGELOG.md`](https://github.com/ionic-team/ionic-cli/blob/develop/packages/%40ionic/utils-stream/CHANGELOG.md) -* **`@ionic/utils-subprocess`**: [`CHANGELOG.md`](https://github.com/ionic-team/ionic-cli/blob/develop/packages/%40ionic/utils-subprocess/CHANGELOG.md) -* **`@ionic/utils-terminal`**: [`CHANGELOG.md`](https://github.com/ionic-team/ionic-cli/blob/develop/packages/%40ionic/utils-terminal/CHANGELOG.md) +* `ionic-app-lib` updated to `0.6.1` + +### 1.7.4 + +* app-lib @0.6.0 + +### 1.7.3 + +* Updating app-lib dependency + +### 1.7.2 + +* Updating app-lib dependency + +### 1.7.1 + +* Fix incorrect passwords being sent in Android Credentials for security command. + +### 1.7.0 + +* Added security command for managing Security Profiles. +* Added package command for Ionic Package. + +### 1.6.5 + +* Updating app-lib dependency + +### 1.6.4 + +* Updating app-lib dependency + +### 1.6.3 + +* Updated tooltips for io command. + +### 1.6.2 + +* Added `io init` command to initialize your project with ionic.io. +* Added config command to centralize ionic.io services configuration. +* Added `--deploy` flag to upload command. + +### 1.6.1 + +* Fix(share): Properly checking that the app exists with app id before attemping to upload. Fix login passing args to get from args or prompting. +* Fix(spelling): Fix spelling mistake of CLI output. + +### 1.6.0 + +* Fix(upload): Bumped archiver back to 0.5.1 - it was causing an issue related to unzipped compressed files on Android devices - see: https://github.com/driftyco/ionic-cli/issues/494 and https://github.com/archiverjs/node-archiver/issues/113. +* Refactor(share): Share is now available in ionic-app-lib. +* Update serve method `start` to check for document root and reject promise if it does not exist instead of exiting process with Util.fail. +* Fix for upload - if you have a script with a query string, it will not get mangled from the removeCacheBusters call. Fixes issue https://github.com/driftyco/ionic-cli/issues/504. +* Fix(browser): Fix for remove crosswalk, pass in the app directory for the project file, then use that instance object to save. Fixes CLI bug https://github.com/driftyco/ionic-cli/issues/500. +* Fix(state): cordovaPlatforms in package.json no longer gets duplicate entry. +* Feature(start): add the ability to add bower packages to a starter project. +* Fix(start): Ensure appSetup.bower is set so that the appSetup.bower.length call doesnt cause a run time exception. Handle the exception thrown from initCordova in the chain by rethrowing the exception if the app setup process fails. +* Fix(platform): Remove console.log command from ionic-cordova-lib, bump to 5.1.5 to have that change. +* Fix(lab): Update preview.html to have utf-8 charset meta tag. +* Style(share): Show the finished message as green +* Fix(login): Remove lowercase of email. +* Feature(project): Expose project to module. +* Fix(upload): Remove entity parsing to fix https://github.com/driftyco/ionic-cli/issues/452#issuecomment-117376542 +* Fix(info): Add check runtime call to show upgrade messages for dependencies that are not fulfilled. +* Fix(start): Ensure appSetup.bower is set so that the appSetup.bower.length call doesnt cause a run time exception. Handle the exception thrown from initCordova in the chain by rethrowing the exception if the app setup process fails. +* Fix(platform): Remove console.log command from ionic-cordova-lib, bump to 5.1.5 to have that change. +* Update ionic-cordova-lib to 5.1.4 for fix with cordova lib run propagating errors to callers. +* Fix for serve - directory root is using path.join instead of path.resolve. +* Add build platform to the cordova command. +* Bump version of ionic-cordova-lib. + +### 1.5.5 + +* Fix(start): Fetch codepen was trying to fetch invalid html/css/js files because of a leading '/'. The trailing slash has been removed. + +### 1.5.4 + +* Fix for error adding Crosswalk to existing ionic project. + +### 1.5.3 + +* Fix for login issue with share - now correctly prompts for ionic.io login. + + +### 1.5.2 + +* Fix for login issue with upload and push - now correctly prompts for ionic.io login. + +### 1.5.1 + +* Fix for ionic serve to specify a browser. +* Added help test for ionic push - `ionic help push`. +* Added a plethora of tests for confidence in refactoring of the command options. +* Fix for the repeated "Ionic not Defined" error. +* Fix for ionic upload - removes the BOM (byte order mark) certain users were having - was leaving unwanted artifacts. This has been corrected. +* Fix to no longer run the hooks permissions on every cordova command. + +### 1.5.0 + +* Fix for Project - now can work from any directory, not just a directory that contains a project. +* Fix for Ionic upload - you can now include a note - `ionic upload --note 'This build fixes the menu'`. +* Login command now exists in ionic-app-lib. +* Upload command now exists in ionic-app-lib. +* Setup command now exists in ionic-app-lib. +* Add 10.39.236.1 for crosswalk lite. +* Add in settings file to have settings across applications. +* Fix for `ionic start --io-app-id ` to properly add the app ID to the project file. + +### 1.4.5 + +* Fix for `ionic browser remove crosswalk` - fix for passing arguments and app directory. +* Fix for `ionic browser upgrade crosswalk` - passes app directory correctly. + +### 1.4.4 + +* Fix upload to now work behind proxies. +* Fix for start - now includes new plugin ID's for Cordova 5.0. +* Fix for serve - fixes argument short name for lab and platform. +* Ionic run with livereload now shows command tips before and after the cordova command completes. +* Fix for `ionic run -l --all` - now respects the all addresses to serve on 0.0.0.0. + +### 1.4.3 + +* Patch an issue where the server commands are not working from the `ionic run` with livereload. + +### 1.4.2 + +* Fixing a bug with serve that will duplicate console logging from the browser. +* Fix for a bug when serve wont start console logs with `--consolelogs` argument. +* Added flag `--platform` for serve command that opens the browser with those platform specific styles (android/ios). + +### 1.4.1 + +* Corrected a bug with ionic state restore command - it now properly passes the app directory to be fixed. +* Corrected landscape and portrait sizes for the resources command. + +### 1.4.0 + +* Extracting core logic for the CLI into ionic-app-lib. +* Certain commands have been moved to the ionic-app-lib - notably: start, serve, hooks, info, browser, and some of cordova commands. +* Ionic serve now allows all IP addresses so you can access the server outside of your machine - use `ionic serve --all` or `ionic serve --address 0.0.0.0` to serve to all addresses. +* Ionic hooks have had some issues with permissions - those are now added in when an app is started. Also there is the `ionic hooks permissions` command to grant those hooks execute permissions. +* When starting an application on a Mac, the iOS platform will be automatically added. +* Ionic Browser command now reverts to using the Cordova CLI if CLI v5.0 is installed. +* Updating Crosswalk to have canary version 14.42.334.0. +* Crosswalk now contains the cordova whitelist by default. +* Ionic sass setup now checks that gulp is installed globally - and if not - tells the user how to set it up. +* Ionic serve command now has a `--nogulp` option to avoid running gulp on serve. + +### 1.3.22 + +* Fix for the upload command to correct issues with the view app cachebuster + +### 1.3.21 + +* Fix for Ionic default hooks permissions + +### 1.3.20 + +* Adding in a command with ionic start to provide an ionic io app ID. `ionic start --io-app-id ` + +### 1.3.19 + +* Added in the `ionic docs` command to assist you in getting Ionic docs opened faster from the CLI! View all with `ionic docs ls`, or type in your desired docs `ionic docs collectionRepeat`. Ionic docs will be opened for the version of Ionic that you are using in your project (ex RC0, RC1, etc). +* Added in the `ionic state` command to help organize your Cordova platforms and plugins by storing the information in the package.json file. Try out `ionic state save` and `ionic state restore`. +* Added in the `ionic hooks` command to help users deal with the default Ionic hooks. In 1.3.18, they were removed by default. That has been turned off, and now to opt-out, use `ionic hooks remove`, or to add back in `ionic hooks add`. +* Added in `--noresources` option for `ionic platform add` - to avoid getting the default Ionic resources. +* Updated default Crosswalk version to 12.41.296.5. +* Updated latest Cordova Android commits from Cordova master to fix various bugs. +* Updated latest Cordova Crosswalk Engine from master to fix various bugs. + +### 1.3.18 + +* Added an option to `ionic serve` to specify a default browser for that Ionic project. `ionic serve --defaultBrowser safari` +* Added an option when adding platforms to not include the default Ionic cordova hooks - `ionic platform add ios --no-hook` +* Ionic CLI now removes some of the older cordova hooks that try to manage plugins - this is now handled by cordova. +* Added an argument to not add default Ionic icon and splash screen resources +* Modified the cordova run command to check for the platform passed - this should resolve issues users are having with crosswalk and android. + +### 1.3.17 + +* Added in default Ionic icons and splashscreens for your iOS and Android applications! Try them out `ionic resources --default`. +* To note: if you have entries in your config.xml file for icons or splashscreens or files existing in your resources directory, +* neither the settings nor the directory will not be overridden. +* To force resource folder with the ionic icons, use `ionic resources --default --force` +* Added in the ability to start an Ionic application from a Plnkr url - try it `ionic start http://embed.plnkr.co/dFvL8n/preview` +* Fixed the no cordova option when using the shorthand `-w` - `ionic start -w folderX blank` should now work +* Ionic info will now look up your version of ios-deploy - which is needed for ios application deployments - `ionic info` + + +### 1.3.16 + +* Added the ability to share an Ionic app with another user via email `ionic share developer@theirdomain.com` + +### 1.3.15 + +* Updated the `ionic link` command to work properly with the `--reset` option +* Fixed the `ionic run --livereload` on windows - now properly gives the prompt for server commands. +* Updated Crosswalk Versions for Canary 13.41.318.0 and beta of 12.41.296.4. +* Fixed the `ionic login` command to properly look at email addresses without lowercasing them. + +### 1.3.14 + +* Fixing the `ionic emulate --livereload` and `ionic run --livereload` to continue to accept user input for server commands. +* Added the `ionic link` command to allow you to specify your Ionic App ID. + +### 1.3.13 + +* Added the ionic.project property `createDocumentRoot` to aid users with build systems to create the folder and run tasks before calling serve. + +### 1.3.12 + +* Explicitly state which platform resources should be built by providing a platform name in the command +* The serve command now allows you to specify a browser to open other than your default - `ionic serve --browser safari` +* The serve command now allows you to specify a path to start the browser in so you can go straight to what you want to test - `ionic serve -o /#/tab/dash` +* The serve command now checks for existing server and live reload ports before trying to start up servers. If either serve host/port is used, then the port is incremented and informs the user of the change, then starts the server to avoid Address conflicts. +* There was a bug when multiple addresses were available - it gave the option to select the address but immediately started listening to console commands for the server. This has been corrected, and now correctly prompts for the address. +* The serve command proxy now accepts another property `proxyNoAgent`: (optional) true/false, if true opts out of connection pooling, see [HttpAgent](http://nodejs.org/api/http.html#http_class_http_agent) +* Added in the `proxyNoAgent` property on `ionic.project` proxies to be true/false, if true opts out of connection pooling, see [HttpAgent](http://nodejs.org/api/http.html#http_class_http_agent) + + +### 1.3.11 + +* Updating task order in the CLI output for help - putting more important tasks at the top, and lesser used ones at the bottom. +* Updated README to have basic info at top, more advanced information at bottom. +* Bumping cordova-android to our fork version of c0.5.6 to have latest commits from the Cordova-android team. +* Bumping cordova-crosswalk-engine to our fork version of c0.6.2 for latest changes by the Mobile chrome team. +* Added option to have your livereload server run off the address passed from the `--address` argument. +* Updated README to have proper `ionic serve` flag for `--lab` +* Updated README to give user instructions to avoid using sudo. +* Changed module for opbeat to use forked version - `opbeat-ionic` that will help us log uncaught exceptions with ionic-cli and user environment runtime information + +### 1.3.10 + +* Updating after_platform_add hook 010_install_plugins.js to check to see if the directory running the commands is in fact a valid Ionic project directory. +* ionic start now checks that you pass a valid directory name, no longer accepting '.' +* Fix for install_plugins to check that is in a valid ionic project +* Checks for invalid contents of your config.xml file and reports those to help you fix the errors +* Fix for `ionic info` - now properly displays OSX Mavericks as operating system if it is indeed Mavericks + +### 1.3.9 + +* Fixed an error where running `ionic serve` and then pressing `q` in the console would have an error trying to close a non-existent process +* Fixing an error where it tries to read a promise from a null/undefined object. +* Updating the error message for if/when `ionic templates` fails to download latest templates +* Added semver to the required node modules for version checking +* Added a version checking utility for cordova cli and node - `ionic info` tells you what you need to run correctly +* Added a version check in the browser command - that way you can stay up to date where it matters +* Modified browser process addition process to use `ionic platform add` to ensure hooks are set up properly +* Modified browser process to change permissions on files using `fs` instead of `shelljs` +* Now upon receiving an error, the CLI will dump system environment information to help the user copy/paste to issues + +### 1.3.8 + +* Added a check in reading to read the ionic.project file in and catches and reports any exception that may from loading invalid characters in JSON. +* Added in an additional browser command `ionic browser clean` that will clean out all the artifacts from the browser additions +* Modified the browser addition process by copying crosswalk libraries over as its own method, and calling this even if the xwalk libraries are downloaded. + +### 1.3.7 + +* Fixed some capitalization errors on the Ionic download url + +### 1.3.6 + +* Fixed some bad lowercasing in the sign up prompt with IONIC_DASH + +### 1.3.5 + +* Added a sign up prompt after starting a new ionic app to create an ionic.io account to take advantage of all the extra features Ionic has to provide. + +### 1.3.4 + +* Added the ionic news updates for when `ionic start` finishes - alerts the users of the latest changes for ionic +* Updated cordova android and the cordova crosswalk engine to be versioned. Now they pull the latest dev commits. +* Due to the cordova crosswalk engine changing its plugin ID, the browser command now removes the older plugin name (org.apache.cordova.crosswalk.engine to org.crosswalk.engine) +* Updated cordova android to have a gradle.properties file to give the user options to build multiple architectures by default + +### 1.3.3 + +* Added the ability to specify an address when using `ionic serve` by specifying the address as an argument: `ionic serve --address 192.168.1.100` +* Added the ability to download and target select versions of beta / canary versions of Crosswalk - see `ionic browser list` to see versions available. +* Fixed the issue with `ionic serve` - when typing 'q' or 'quit' in the prompt, it will properly kill the gulp spawned process. Previously, it was left behind. +* When adding a browser for a platform, the version of that browser and name shall be saved. +* Now when you type `ionic browser versions` - it will list all installed browsers and versions for the platform its installed for. +* Bumped connect-livereload up to 0.5.2 to resolve [an issue](https://github.com/intesso/connect-livereload/issues/41) from its repository regarding cookies. +* Fixes for uploading - now provides more meaningful errors. +* Added the ability to list all Ionic starter templates available for Ionic. Use `ionic start --list` or `ionic templates` to see available starter templates. +* Updating ionic help information to give better understanding to ions and bower components `ionic help add`, `ionic help remove` and `ionic help list` +* Updating ionic help information about the `ionic serve --lab` feature to let users know how to use it. +* Fixed a small bug when using `ionic start --sass dir template` - before the boolean command line arguments were eating the following argument. This has been fixed by adding boolean properties to optimist. +* Added the stdio inheritance to have your gulp watch task inherit coloring. +* Added the ability for you to specify an alternate document root to use with `ionic serve` other than the default `www`. This is specified in your `ionic.project` file as a `documentRoot` property. +* Adding a cordova hook to remove Ionic SASS files from platforms folders. This should save you about 340K of space on your device builds. +* Adding a cordova hook to ensure platforms and plugins folder exist before adding a platform. +* Adding a cordova hook to store plugins in package.json file as `cordovaPlugins` when a plugin is added or removed. +* Adding a cordova hook to install plugins listed in package.json file as `cordovaPlugins` after a platform is added to the project. + + +### 1.3.2 + +* Added another fix for the way the cookies were handled for `ionic upload` - changing from `cookie.name` to `cookie.key` + +### 1.3.1 + +* [Adding the Crosswalk browser for Ionic](http://ionicframework.com/blog/crosswalk-comes-to-ionic/) +* See all the browsers available to install - `ionic browser list`. *NOTE: Only stable releases are allowed for now.* +* You can now specify [which version of the Crosswalk run time](https://download.01.org/crosswalk/releases/crosswalk/android/stable/) you want to use - `ionic browser add crosswalk@8.37.189.14`. +* Caching the Crosswalk downloads - once you’ve installed a version in a project, running `ionic browser add crosswalk` will not re-download the webviews if they have previously been downloaded. +* Fixed an issue with `ionic upload` - now you should be able to log in and re-use your login cookies without errors. + +### 1.3.0 + +* You can now use [Crosswalk in your Android projects](http://forum.ionicframework.com/t/crosswalk-integration-beta-in-ionic-cli/15190). Crosswalk is a way to package your Chrome Webview and use it with Cordova. Use the `ionic help browser` command to get more information about it. +* Automatically add the `SplashScreen` and `SplashScreenDelay` preference configs for Android splash screens +* When an orientation is locked using the [preference config](http://cordova.apache.org/docs/en/edge/config_ref_index.md.html#The%20config.xml%20File_global_preferences), only build the necessary splash screen images + + +### 1.2.14 + +* [Automating Icons and Splash Screens](http://ionicframework.com/blog/automating-icons-and-splash-screens/) +* Automatically create icon resources from source file: `ionic resources --icon` +* Automatically create splash screen resources from source file: `ionic resources --splash` +* Update config.xml resource platform nodes + + +### 1.2.13 + +* Locking Gulp at 3.8.8 to avoid adding the v8flags module dependency + + +### 1.2.12 + +* Updating the npm-shrinkwrap + + +### 1.2.11 + +* Updating the Labs styles + + +### 1.2.10 + +* Updated the serve command for the `serve --labs` to use `IONIC_LAB_URL` + + +### 1.2.9 + +* [Introduced Ionic Labs](http://ionicframework.com/blog/ionic-lab/) - a way to see preview iOS and Android side by side in the browser +* Added proxy-middleware to provide proxying to APIs from the `serve` command +* Updated README doc about how to use the proxy +* Injects platform specific class to HTML to view it as an iOS or Android device in browser +* Bumped `serve-static` to 1.7.1 to avoid some errors with the `serve` for users of Node 0.12 +* Added the `add` command to use ionic to manage bower components +* Ionic now reads the Node environment variable `http_proxy` along with the passed `PROXY` variable to get around a local proxy + + +### 1.2.8 + +* CSRF cookie fixes + + +### 1.2.7 + +* npm-shrinkwrap +* Update ionic.io API URL + + +### 1.2.6 + +* Fix `fs.chmodSync: Error: ENOENT` for existing projects +* Fix lib update +* Add ionic app task +* Starter projects can provide `app.json` to specify plugins and sass setup + + +### 1.2.5 + +* Do not watch `www/lib/` files by default +* Set watchPatterns within ionic.project config file +* Friendly EMFILE error when too many files are watched +* Ensure config.xml content[src] gets reset after run/emulate +* Improve fetchArchive error handling +* Fix SSL Cert errors +* Do not prompt for address selection when there's only one + + +### 1.2.4 + +* Use `cross-spawn` module to fix errors with using spawn on Windows +* Start ionic project from any Github repo +* Start ionic projects using a local directory +* Use specific npm versions in package.json to avoid any future errors from breaking changes +* Fix write errors after downloading github archive files +* Refactor sass setup to use gulpStartupTasks ionic.project property instead + + +### 1.2.3 + +* From the server, use `restart` or `r` to restart the client app from the root +* From the server, use `goto` or `g` a url to have the client app navigate to the given url +* From the server, use `consolelogs` or `c` enable/disable console log output +* From the server, use `serverlogs` or `s` to enable/disable server log output +* From the server, use `quit` or `q` to shutdown the server and exit +* Print out Ionic server command tips +* LiveReload server logs specify which device made the request (iOS, Android, etc.) +* Remember address selection [#91](https://github.com/driftyco/ionic-cli/issues/91) +* Reset address selection with `ionic address` +* Add localhost as an option of possible addresses to use [#88](https://github.com/driftyco/ionic-cli/issues/88) +* Inject scripts after charset [#87](https://github.com/driftyco/ionic-cli/issues/87) +* Improved error message when unable to find an IP address [#85](https://github.com/driftyco/ionic-cli/issues/85) +* Fix config.xml errors when in the wrong working directory [#84](https://github.com/driftyco/ionic-cli/issues/84) + + +### 1.2.2 + +* ReferenceError hot fix + + +### 1.2.1 + +* Clean up any cmd flags which may confuse Cordova [#83](https://github.com/driftyco/ionic-cli/issues/83) +* Select available IP address prompt [#82](https://github.com/driftyco/ionic-cli/issues/82) +* Fix black screen on load [#81](https://github.com/driftyco/ionic-cli/issues/81) + + +### 1.2.0 + +* LiveReload from a native app during development +* Option to print out console logs to the terminal +* Option to print out server logs to the terminal +* Start Ionic projects from Codepen urls +* [Live Reload All the Things: Ionic CLI's Latest Features](http://ionicframework.com/blog/live-reload-all-things-ionic-cli/) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index 7fa589d26c..0000000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,3 +0,0 @@ -# Contributor Code of Conduct - -Please see [CODE_OF_CONDUCT.md](https://github.com/ionic-team/ionic/blob/HEAD/CODE_OF_CONDUCT.md) in the Ionic Framework repository. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 6dd9bcd487..0000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,164 +0,0 @@ -# Contributing - -:mega: **Support/Questions?**: Please see our [Support -Page](https://ionicframework.com/support) for general support questions. The -issues on GitHub should be reserved for bug reports and feature requests. - -### Branches - -* [`develop`](https://github.com/ionic-team/ionic-cli/tree/develop): **development** branch -* [`stable`](https://github.com/ionic-team/ionic-cli/tree/stable): **stable** version - -##### Version Branches - -These are mostly for reference--older major versions are typically not maintained. - -* [`5.x`](https://github.com/ionic-team/ionic-cli/tree/5.x) -* [`4.x`](https://github.com/ionic-team/ionic-cli/tree/4.x) -* [`3.x`](https://github.com/ionic-team/ionic-cli/tree/3.x) -* [`2.x`](https://github.com/ionic-team/ionic-cli/tree/2.x) (*legacy version*) - -### Bug Reports - -Run the command(s) with `--verbose` to produce debugging output. We may ask for -the full command output, including debug statements. - -Please also copy/paste the output of the `ionic info` command into your issue -and be as descriptive as possible. Include any steps that might help us -reproduce your issue. - -### Feature Requests - -Post an issue describing your feature to open a dialogue with us. We're happy -to hear from you! - -### Pull Requests - -Pull requests are most welcome! But, if you plan to add features or do large -refactors, please **open a dialogue** with us first by creating an issue. Small -bug fixes are welcome any time. - -#### Help Wanted - -Looking for small issues to help with? You can browse the [`help -wanted`](https://github.com/ionic-team/ionic-cli/labels/help%20wanted) label. -These are issues that have been marked as great opportunities for someone's -first PR to the Ionic CLI. :heart_eyes: - -### Local Setup - -#### Structure - -The Ionic CLI is organized into a monorepo. Here are the packages: - -##### CLIs - -* [`packages/@ionic/cli`](https://github.com/ionic-team/ionic-cli/tree/develop/packages/%40ionic/cli): Ionic CLI executable and library. - -##### Libraries - -* [`packages/@ionic/cli-framework`](https://github.com/ionic-team/ionic-cli/tree/develop/packages/%40ionic/cli-framework): Framework for command-line programs. -* [`packages/@ionic/cli-framework-prompts`](https://github.com/ionic-team/ionic-cli/tree/develop/packages/%40ionic/cli-framework-prompts): Command-line prompting framework that wraps [Inquirer.js](https://github.com/SBoudrias/Inquirer.js). -* [`packages/@ionic/discover`](https://github.com/ionic-team/ionic-cli/tree/develop/packages/%40ionic/discover): Service discovery library used for `ionic serve` with the [Ionic DevApp](https://ionicframework.com/docs/appflow/devapp/) (now retired). -* [`packages/@ionic/utils-array`](https://github.com/ionic-team/ionic-cli/tree/develop/packages/%40ionic/utils-array): General purpose array library with asynchronous map/filter/reduce. -* [`packages/@ionic/utils-fs`](https://github.com/ionic-team/ionic-cli/tree/develop/packages/%40ionic/utils-fs): Filesystem library that wraps [fs-extra](https://github.com/jprichardson/node-fs-extra) for Node.js. -* [`packages/@ionic/utils-network`](https://github.com/ionic-team/ionic-cli/tree/develop/packages/%40ionic/utils-network): Network library for Node.js. -* [`packages/@ionic/utils-object`](https://github.com/ionic-team/ionic-cli/tree/develop/packages/%40ionic/utils-object): General purpose object library. -* [`packages/@ionic/utils-process`](https://github.com/ionic-team/ionic-cli/tree/develop/packages/%40ionic/utils-process): OS process library for Node.js. -* [`packages/@ionic/utils-stream`](https://github.com/ionic-team/ionic-cli/tree/develop/packages/%40ionic/utils-stream): Stream library for Node.js. -* [`packages/@ionic/utils-subprocess`](https://github.com/ionic-team/ionic-cli/tree/develop/packages/%40ionic/utils-subprocess): Subprocess library that uses [cross-spawn](https://github.com/moxystudio/node-cross-spawn) for Node.js. -* [`packages/@ionic/utils-terminal`](https://github.com/ionic-team/ionic-cli/tree/develop/packages/%40ionic/utils-terminal): Terminal and command-line environment library for Node.js. - -#### Toolset - -* npm 8+ is required. -* Node 16+ is required. -* The codebase is written in [TypeScript](https://www.typescriptlang.org/). If - you're unfamiliar with TypeScript, we recommend using [VS - Code](https://code.visualstudio.com/) and finding a tutorial to familiarize - yourself with basic concepts. -* The test suite uses [Jest](https://facebook.github.io/jest/). - -#### Setup - -1. Fork the repo & clone it locally. -1. `npm install` to install the dev tools. -1. `npm run bootstrap` (will install package dependencies and link packages - together) -1. Optionally `npm run link` to make `ionic` and other bin files point to your - dev CLI. -1. `npm run watch` will spin up TypeScript watch scripts for all packages. -1. TypeScript source files are in `packages/**/src`. -1. Good luck! :muscle: Please open an issue if you have questions or something - is unclear. - -#### Running Dev CLI - -To switch between dev CLI and stable CLI, you can use a Node version manager -such as [nvm](https://github.com/creationix/nvm) and switch between -environments, e.g. - -```bash -nvm install v16 -nvm alias cli-local v16 -``` - -You can even set up an alias in your terminal that sets `IONIC_CONFIG_DIRECTORY` -to have seperate configuration environments. - -```bash -alias cli-local="nvm use cli-local && export IONIC_CONFIG_DIRECTORY=$HOME/.ionic/cli-local" -``` - -When the Node environment is created, create a symlink to `packages/@ionic/cli/bin/ionic` within `$NVM_BIN`: - -```bash -ln -s $(pwd)/packages/@ionic/cli/bin/ionic $NVM_BIN -``` - -##### Debugging - -The following workflow is recommended for debugging the Ionic CLI: - -1. Place - [`debugger;`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/debugger) - statements where desired. -1. Run the CLI via `node` to use Node's `--inspect-brk` flag: - - * Instead of `~/path/to/ionic ` use `node --inspect-brk - ~/path/to/ionic `. - * Instead of `ionic `, try `node --inspect-brk $(which ionic) - ` (works on Mac and Linux). - -1. Open `chrome://inspect` in Chrome and select the remote target to use - DevTools for debugging. - -Read more about Node debugging in the [Debugging -Guide](https://nodejs.org/en/docs/guides/debugging-getting-started/). - -##### Publishing - -CI automatically publishes the next version semantically from analyzing commits in `stable`. To maintain a shared history between `develop` and `stable`, the branches must be rebased with each other locally. - -* When it's time to cut a release from `develop`: - - ``` - git checkout stable - git rebase develop - git push origin stable - ``` - -* Await successful publish in CI. Ionitron will push the updated versions and tags to `stable`. -* Sync `develop` with `stable`. - - ``` - git pull origin stable - git checkout develop - git rebase stable - git push origin develop - ``` - -To publish **testing** versions, follow these steps: - -1. Cancel any watch scripts. -1. Run `npm run publish:testing` diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 2a73e5c8dc..0000000000 --- a/Dockerfile +++ /dev/null @@ -1,14 +0,0 @@ -FROM node:lts - -# Set CI to true & LANG -ENV CI=1 -ENV LANG en_US.UTF-8 - -# Install the desired version of @ionic/cli -ARG IONIC_CLI_VERSION - -RUN npm i -g @ionic/cli@${IONIC_CLI_VERSION} && \ - ionic --no-interactive config set -g daemon.updates false && \ - ionic --no-interactive config set -g telemetry false - -WORKDIR /usr/src/app \ No newline at end of file diff --git a/LICENSE b/LICENSE index 7c5808ced6..5387be979f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,20 @@ The MIT License (MIT) -Copyright (c) 2017 Drifty Co +Copyright (c) 2013 Drifty Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 9abf7f12f1..3e5242f3b1 100644 --- a/README.md +++ b/README.md @@ -1,52 +1,587 @@ -[![Build Status][ci-badge]][ci-badge-url] -[![Dependabot Status](https://api.dependabot.com/badges/status?host=github&repo=ionic-team/ionic-cli)](https://dependabot.com) -[![npm][npm-badge]][npm-badge-url] +[![Circle CI](https://circleci.com/gh/driftyco/ionic-cli.svg?style=svg)](https://circleci.com/gh/driftyco/ionic-cli) -# Ionic CLI +Ionic-Cli +========= -The Ionic command line interface (CLI) is your go-to tool for developing [Ionic][ionic-homepage] apps. +The Ionic Framework command line utility makes it easy to start, build, run, and emulate [Ionic](http://ionicframework.com/) apps. In addition, it comes with (optional!) integration with the [Ionic Platform](http://ionic.io/), a set of mobile services perfect for Ionic apps. -### Installation +Use the `ionic --help` command for more detailed task information. + +## Installing + +```bash +$ npm install -g ionic +``` + +*Note: For a global install of `-g ionic`, OSX/Linux users may need to prefix the command with `sudo` or can setup [proper file permissions on OSX for npm](http://www.johnpapa.net/how-to-use-npm-global-without-sudo-on-osx/) to install without `sudo`. * + + +## Starting an Ionic App + +```bash +$ ionic start myapp [template] +``` + +Starter templates can either come from a named template, a Github repo, a Codepen, or a local directory. A starter template is what becomes the `www` directory within the Cordova project. + +__Named template starters:__ + +* [tabs](https://github.com/driftyco/ionic-starter-tabs) (Default) +* [sidemenu](https://github.com/driftyco/ionic-starter-sidemenu) +* [maps](https://github.com/driftyco/ionic-starter-maps) +* [salesforce](https://github.com/driftyco/ionic-starter-salesforce) +* [complex-list](https://github.com/driftyco/ionic-starter-complex-list) +* [blank](https://github.com/driftyco/ionic-starter-blank) + +__Github Repo starters:__ + +* Any Github repo url, ex: [https://github.com/driftyco/ionic-starter-tabs](https://github.com/driftyco/ionic-starter-tabs) +* Named templates are simply aliases to Ionic starter repos + +__Codepen URL starters:__ + +* Any Codepen url, ex: [http://codepen.io/ionic/pen/odqCz](http://codepen.io/ionic/pen/odqCz) +* [Ionic Codepen Demos](http://codepen.io/ionic/public-list/) + +__Plunker URL starters:__ + +* Any Plunker url, ex: [http://embed.plnkr.co/dFvL8n/preview](http://embed.plnkr.co/dFvL8n/preview) + +__Local directory starters:__ + +* Relative or absolute path to a local directory + +__Command-line flags/options:__ + + [--appname|-a] ....... Human readable name for the app + (Use quotes around the name) + [--id|-i] ............ Package name set in the config + ex: com.mycompany.myapp + [--no-cordova|-w] .... Do not create an app targeted for Cordova + [--sass|-s] ........... Setup the project to use Sass CSS precompiling + [--list|-l] .......... List starter templates available + + [--io-app-id] ......... The Ionic.io app ID to use + +## Adding a platform target + +```bash +$ ionic platform ios android +``` + +## Testing in a Browser + +Use `ionic serve` to start a local development server for app dev and testing. This is useful for both desktop browser testing, and to test within a device browser which is connected to the same network. Additionally, this command starts LiveReload which is used to monitor changes in the file system. As soon as you save a file the browser is refreshed automatically. View [Using Sass](https://github.com/driftyco/ionic-cli/blob/master/README.md#using-sass) if you would also like to have `ionic serve` watch the project's Sass files. + +```bash +$ ionic serve [options] +``` + +## Building your app + +```bash +$ ionic build ios +``` + +## Live Reload App During Development (beta) + +The `run` or `emulate` command will deploy the app to the specified platform devices/emulators. You can also run __live reload__ on the specified platform device by adding the `--livereload` option. The live reload functionality is similar to `ionic serve`, but instead of developing and debugging an app using a standard browser, the compiled hybrid app itself is watching for any changes to its files and reloading the app when needed. This reduces the requirement to constantly rebuild the app for small changes. However, any changes to plugins will still require a full rebuild. For live reload to work, the dev machine and device must be on the same local network, and the device must support [web sockets](http://caniuse.com/websockets). + +With live reload enabled, an app's console logs can also be printed to the terminal/command prompt by including the `--consolelogs` or `-c` option. Additionally, the development server's request logs can be printed out using `--serverlogs` or `-s` options. + +__Command-line flags/options for `run` and `emulate`:__ + + [--livereload|-l] ....... Live Reload app dev files from the device (beta) + [--consolelogs|-c] ...... Print app console logs to Ionic CLI (live reload req.) + [--serverlogs|-s] ....... Print dev server logs to Ionic CLI (live reload req.) + [--port|-p] ............. Dev server HTTP port (8100 default, live reload req.) + [--livereload-port|-i] .. Live Reload port (35729 default, live reload req.) + [--all|-a] .............. Specify to run on all addresses, 0.0.0.0, so you can view externally + [--browser|-w] .......... Specifies the browser to use (safari, firefox, chrome) + [--browseroption|-o] .... Specifies a path to open to (/#/tab/dash) + [--debug|--release] + +While the server is running for live reload, you can use the following commands within the CLI: + + restart or r to restart the client app from the root + goto or g and a url to have the app navigate to the given url + consolelogs or c to enable/disable console log output + serverlogs or s to enable/disable server log output + quit or q to shutdown the server and exit + + +## Emulating your app + +Deploys the Ionic app on specified platform emulator. This is simply an alias for `run --emulator`. + +```bash +$ ionic emulate ios [options] +``` + +## Running your app + +Deploys the Ionic app on specified platform devices. If a device is not found it'll then deploy to an emulator/simulator. + +```bash +$ ionic run ios [options] +``` + +## Icon and Splash Screen Image Generation + +[Automatically generate icons and splash screens](http://ionicframework.com/blog/automating-icons-and-splash-screens/) from source images to create each size needed for each platform, in addition to copying each resized and cropped image into each platform's resources directory. Source images can either be a `png`, `psd` __Photoshop__ or `ai` __Illustrator__ file. Images are generated using Ionic's image resizing and cropping server, instead of requiring special libraries and plugins to be installed locally. + +Since each platform has different image requirements, it's best to make a source image for the largest size needed, and let the CLI do all the resizing, cropping and copying for you. Newly generated images will be placed in the `resources` directory at the root of the Cordova project. Additionally, the CLI will update and add the correct `` configs to the project's [config.xml](http://cordova.apache.org/docs/en/edge/config_ref_images.md.html#Icons%20and%20Splash%20Screens) file. + +During the build process, Cordova (v3.6 or later) will look through the project's [config.xml](http://cordova.apache.org/docs/en/edge/config_ref_images.md.html#Icons%20and%20Splash%20Screens) file and copy the newly created resource images to the platform's specific resource folder. For example, Android's resource folder can be found in `platforms/android/res`, and iOS uses `platforms/ios/APP_NAME/Resources`. + + +### Icon Source Image + +Save an `icon.png`, `icon.psd` or `icon.ai` file within the `resources` directory at the root of the Cordova project. The icon image's minimum dimensions should be 192x192 px, and should have __no__ rounded corners. Note that each platform will apply it's own mask and effects to the icons. For example, iOS will automatically apply it's custom rounded corners, so the source file should not already come with rounded corners. This [Photoshop icon template](http://code.ionicframework.com/resources/icon.psd) provides the recommended size and guidelines of the artwork's safe zone. + +```bash +$ ionic resources --icon +``` + +- [Photoshop Icon Template](http://code.ionicframework.com/resources/icon.psd) + + +### Splash Screen Source Image + +Save a `splash.png`, `splash.psd` or `splash.ai` file within the `resources` directory at the root of the Cordova project. Splash screen dimensions vary for each platform, device and orientation, so a square source image is required the generate each of various sizes. The source image's minimum dimensions should be 2208x2208 px, and its artwork should be centered within the square, knowning that each generated image will be center cropped into landscape and portait images. The splash screen's artwork should roughly fit within a center square (1200x1200 px). This [Photoshop splash screen template](http://code.ionicframework.com/resources/splash.psd) provides the recommended size and guidelines of the artwork's safe zone. Additionally, when the `Orientation` [preference config](http://cordova.apache.org/docs/en/edge/config_ref_index.md.html#The%20config.xml%20File_global_preferences) is set to either `landscape` or `portrait` mode, then only the necessary images will be generated. + +```bash +$ ionic resources --splash +``` + +- [Photoshop Splash Screen Template](http://code.ionicframework.com/resources/splash.psd) + + +### Generating Icons and Splash Screens + +To generate both icons and splash screens, follow the instructions above and run: + +```bash +$ ionic resources +``` + +### Platform Specific Resource Images + +One source image can be used to generate images for each platform by placing the file within the `resources` directory, such as `resources/icon.png`. To use different source images for individual platforms, place the source image in the respective platform's directory. For example, to use a different icon for Android, it should follow this path: `resources/android/icon.png`, and a different image for iOS would use this path: `resources/ios/icon.png`. + +### Generating Exact Platform Resources + +By default the `ionic resources` command will automatically figure out which platforms it should generate according to what platforms have been added to your project. However, you can also explicitly state which resources should be built by providing a platform name in the command. The example below would generate only `ios` resources (even if the platform hasn't been added to the project). + +```bash +$ ionic resources ios +``` + +### Default Ionic Resources + +Ionic provides you some default icons and splash screens to give you a better idea of how to size your icons and splashscreen, as well as how to modify your config.xml file for your own icons. + +```bash +$ ionic resources --default +``` + +If you already have a resources directory, the command above will not over write your files. If you wish to force an over write, use `ionic resources --default --force`. + +When starting a new app and adding a platform `ionic platform add ios` - the default icons and splashscreens will be downloaded and your config.xml will be modified to set up the default resources. This should help you identify your Ionic apps easier as well as help you get the file structure and configuration correct. + + +## Crosswalk for Android + +In v1.3.0 and later, you can now specify which browser to use in your Cordova Android projects. Currently we only support [Crosswalk](https://crosswalk-project.org/) and have plans to support more browsers later. + +Execute `ionic browser add crosswalk` to add the Crosswalk browser to your Android project. By default, this will install the `12.41.296.5` version of Crosswalk. + +If you'd like to specify a different version of Crosswalk, run `ionic browser list` to see which browsers are available and what versions. Then run `ionic browser add crosswalk@10.39.235.15`. + +All that is left is to run the project as normal - `ionic run android`. + +If you'd like to build without Crosswalk for Android SDK 21 or later, do the following: + +``` +ionic browser revert android +ionic build android --release -- --minSdkVersion 21 +``` + +## Advanced serve options + +__LiveReload__ + +By default, LiveReload will watch for changes in your `www/` directory, +excluding `www/lib/`. To change this, you can specify a `watchPatterns` +property in the `ionic.project` file located in your project root to watch +(or not watch) for specific changes. + +```json +{ + "name": "myApp", + "app_id": "", + "watchPatterns": [ + "www/js/*", + "!www/css/**/*" + ] +} +``` + +For a reference on glob pattern syntax, check out +[globbing patterns](http://gruntjs.com/configuring-tasks#globbing-patterns) on +the Grunt website. + +__Gulp Integration__ + +When running `ionic serve`, you can have Ionic run any Gulp tasks you specify by putting them into your `ionic.project` as a `gulpStartupTasks` property as follows: + +```json +{ + "name": "SmoothRiders", + "gulpStartupTasks": [ + "watch" + ] +} + +``` + +Now, when you run `ionic serve`, it will run the `watch` task while starting the Ionic server. + +If you would like to disable gulp from running during serve, pass the `--nogulp` option. + +NOTE: + +```bash +$ ionic setup sass +``` + +will add a `watchPatterns` propery with the default values to your `ionic.project` +file that you can then edit, in addition to the `gulpStartupTasks` property +described in the [Using Sass](https://github.com/driftyco/ionic-cli/blob/master/README.md#using-sass) section. + + +__Service Proxies:__ + +The `serve` command can add some proxies to the http server. These proxies are useful if you are developing in the browser and you need to make calls to an external API. With this feature you can proxy request to the external api through the ionic http server preventing the `No 'Access-Control-Allow-Origin' header is present on the requested resource` error. + +In the `ionic.project` file you can add a property with an array of proxies you want to add. The proxies are object with the following properties: + +* `path`: string that will be matched against the beginning of the incoming request URL. +* `proxyUrl`: a string with the url of where the proxied request should go. +* `proxyNoAgent`: (optional) true/false, if true opts out of connection pooling, see [HttpAgent](http://nodejs.org/api/http.html#http_class_http_agent) + +```json +{ + "name": "appname", + "email": "", + "app_id": "", + "proxies": [ + { + "path": "/v1", + "proxyUrl": "https://api.instagram.com/v1" + } + ] +} ``` -npm install -g @ionic/cli + +Using the above configuration, you can now make requests to your local server at `http://localhost:8100/v1` to have it proxy out requests to `https://api.instagram.com/v1` + +For example: + +```js +angular.module('starter.controllers', []) +.constant('InstagramApiUrl', '') +// .contant('InstagramApiUrl','https://api.instagram.com') +//In production, make this the real URL + +.controller('FeedCtrl', function($scope, $http, InstagramApiUrl) { + + $scope.feed = null; + + $http.get(InstagramApiUrl + '/v1/media/search?client_id=1&lat=48&lng=2.294351').then(function(data) { + console.log('data ' , data) + $scope.feed = data; + }) + +}) ``` -Detailed installation instructions can be found in the [CLI documentation](https://ionicframework.com/docs/installation/cli). +See also [this gist](https://gist.github.com/jbavari/d9c1c94058c4fdd4e935) for more help. + +__Command-line flags/options:__ -### Usage + [--consolelogs|-c] ...... Print app console logs to Ionic CLI + [--serverlogs|-s] ....... Print dev server logs to Ionic CLI + [--port|-p] ............. Dev server HTTP port (8100 default) + [--livereload-port|-i] .. Live Reload port (35729 default) + [--nobrowser|-b] ........ Disable launching a browser + [--nolivereload|-r] ..... Do not start live reload + [--noproxy|-x] .......... Do not add proxies + [--address] ............. Serves in the browser at the specified address + [--lab] ................. Serves both iOS and Android in the browser + [--nogulp] .............. Serve without running gulp tasks + [--platform|-t] ......... Serve the platform specific styles in the browser (ios/android) -The Ionic CLI ships with command documentation, accessible in your terminal by using the `--help` flag. These docs are also available [online][ionic-cli-docs]. +## Using Ionic Labs -* For a list of commands: +We've extended the serve command to open the new Lab UI that features iOS and Android side-by-side. +```bash +$ ionic serve --lab ``` -ionic --help -ionic cordova --help -ionic capacitor --help + +If you've used the serve command before, you'll feel right at home with this one. Just like serve, it opens your app in a browser, but now it shows you what your app will look like on a phone, with both iOS and Android side by side. + +And of course, it supports Live Reload and all the other goodies we've added over the last couple of months. + +## Serving an alternate document root + +If you'd like to test your app in the browser and you use a folder other than the default of `www`, you can specify this folder in your `ionic.project` file. + +You might also want to have the document root be created if you use some sort of build system, we suggest using `createDocumentRoot` for that so that `ionic serve` will create that folder for you. + +It is also advised you specify the watch patterns for this document root as well, as follows: + +```json +{ + "name": "SmoothRiders", + "gulpStartupTasks": [ + "watch" + ], + "documentRoot": "app", + "createDocumentRoot": "app", + "watchPatterns": [ + "app/js/*", + "!app/css/**/*" + ] +} + +``` + +## Update Ionic lib + +Update Ionic library files, which are found in the `www/lib/ionic` directory. If bower is being used +by the project, this command will automatically run `bower update ionic`, otherwise this command updates +the local static files from Ionic's CDN. + +```bash +$ ionic lib update ``` +*Note: Using bower? This command does not update Ionic's dependencies. Run `bower update` to update Ionic and all of it's dependencies defined in `bower.json`.* + +## Packaging an app (beta) -* For command usage and a list of options: +Using Ionic's service, you can compile and package your project into an app-store ready app without +requiring native SDKs on your machine. +```bash +$ ionic package debug android ``` -ionic start --help -ionic serve --help -ionic cordova run --help + +The third argument can be either `debug` or `release`, and the last argument can be either `android` or `ios`. + + +## Cordova Commands + +Ionic uses Cordova underneath, so you can also substitute Cordova commands to prepare/build/emulate/run, or to add additional plugins. + +*Note: we occasionally send anonymous usage statistics to the Ionic team to make the tool better.* + +## Working around proxies + +If you have a proxy you need to get around, you can pass that proxy with the default `http_proxy` [node environment variable](https://www.npmjs.org/doc/misc/npm-config.html#proxy) or an environment variable `proxy`. + +A few ways to set up and use the environment variable: + +```bash +export http_proxy=internal.proxy.com +# Or +export PROXY=internal.proxy.com + +ionic start my_app + +# Additionally, pass in line +PROXY=internal.proxy.com ionic start my_app ``` ---- -:book: **Documentation**: [https://ionicframework.com/docs/cli][ionic-cli-docs] +## Using Sass + +By default, starter projects are hooked up to Ionic's precompiled CSS file, which is found in the project's `www/lib/ionic/css` directory, and is linked to the app in the head of the root `index.html` file. However, Ionic projects can also be customized using [Sass](http://sass-lang.com/), which gives developers and designers "superpowers" in terms of creating and maintaining CSS. Below are two ways to setup Sass for your Ionic project (the `ionic setup sass` command simply does the manual steps for you). Once Sass has been setup for your Ionic project, then the `ionic serve` command will also watch for Sass changes. + +#### Setup Sass Automatically + + ionic setup sass + + +#### Setup Sass Manually + +1. Run `npm install` from the working directory of an Ionic project. This will install [gulp.js](http://gulpjs.com/) and a few handy tasks, such as [gulp-sass](https://www.npmjs.org/package/gulp-sass) and [gulp-minify-css](https://www.npmjs.org/package/gulp-minify-css). +2. Remove `` from the `` of the root `index.html` file. +3. Remove `` from the `` of the root `index.html` file. +4. Add `` to the `` of the root `index.html` file. +5. In the `ionic.project` file, add the JavaScript property `"gulpStartupTasks": ["sass", "watch"]` (this can also be customized to whatever gulp tasks you'd like). + + +# Ionic.io services + +The CLI supports operations to use backend services for your Ionic app. To get started, [visit the ionic.io homepage](http://ionic.io) and [sign up there](https://apps.ionic.io/signup). + +There are a few things you can utilize the CLI for to support ease of development. + +## Login + +Type `ionic login` to get logged in the CLI. + +### Login without prompt + +You can pass the email and password to login without being prompted for email and password. + +`ionic login --email user@ionic.io --password somepass` + +### Login with environment variables + +The CLI also supports settings environment variables to read off the email and password for the user. + +Set `IONIC_EMAIL` and `IONIC_PASSWORD` as variables to have the CLI read these instead of being prompted for them. + +## Upload your Ionic app + +Use the `ionic upload` command to take your current application you are developing and upload it to the Ionic.io servers. + +Now you can use [the ionic view app](http://view.ionic.io/) to view that application or have others view the application. + +After uploading the application, you will see a message: + + +``` +Uploading app.... + +Successfully uploaded (f23j9fjs) +``` + +This indicates you uploaded the application correctly, and the App ID is set to `f23j9fjs`. + +You can then view that App ID from the View app or the application listing on ionic.io. + +### Adding a note with your upload + +To add a note to your build, pass the `--note` option as follows: + +`ionic upload --note "This version of the application fixes the menu selections"`. + +## Set your Ionic Project App ID manually + +Use the `ionic link ` command to set your Ionic App ID to continue working with the same app with the Ionic platform across development enviroments. + +## Share the application with another user + +Use the `ionic share ` command to have an email sent to another person to have them view the Ionic application you are using. Note: You must have an ionic.io account as well as the user you are sharing with. + +# Ionic Docs + +To get Ionic documentation from the Ionic CLI, try using the `ionic docs` command. The command will look up your currently used Ionic version and open the docs specific for that version. Ex: RC0, RC1, etc. + +To view all docs, `ionic docs ls`. + +To get help with a doc you may not remember, just type the name close enough: `ionic docs list` and you will be prompted for suggestions that may match. + + +# Ionic Hooks + +Ionic provides some default hooks for you to use in your Cordova application. In versions prior to 1.3.18, these hooks were automatically installed via the `ionic platform` command. + +In 1.3.18, the hooks were automatically removed due to some errors users were having with Crosswalk and other plugins with variables. + +If you were a user who would still like to use those hooks, you can re-install these hooks with the `ionic hooks add` command. + +If you would like to remove these hooks yourself, use `ionic hooks remove` to get rid of them. + + +# Ionic State + +Ionic now provides a command to help you manage the state of your Ionic application. Previously Cordova hooks were used to save platforms and plugins to your `package.json` file. + +Now when using `ionic platform add`, `ionic platform rm`, `ionic plugin add`, or `ionic plugin rm`, the state command will automatically be used to save the platform or plugin state to the `package.json` file. + +If you would like to avoid saving plugins or platforms to the `package.json` file - pass in the `--nosave` option. (`ionic plugin add org.apache.cordova.file --nosave`). + +Your package.json file might look something like the following: + +```json +"cordovaPlatforms": [ + "ios", + { + "android": { + "id": "android", + "locator": "https://github.com/apache/cordova-android.git" + } + } +], +"cordovaPlugins": [ + "org.apache.cordova.device", + "org.apache.cordova.console", + "com.ionic.keyboard", + "org.apache.cordova.splashscreen", + { + "locator": "https://github.com/MobileChromeApps/cordova-crosswalk-engine.git", + "id": "org.cordova.croswalk" + }, + { + "locator": "/path/to/cloned/phonegap-facebook-plugin", + "id": "", + "variables": { + "APP_ID": "some_id", + "APP_NAME": "some_name" + } + } +] +``` + +## ionic state save + +The `ionic state save` command does some lookup in your platforms and plugins to save the current state of your cordova application. + +First it looks in your platforms folder to see which platforms are installed, and saves the name and version in your `package.json` file under the `cordovaPlatforms` attribute. + +Second it looks at your plugins `fetch.json` file to save the plugins installed both from the Cordova registry, locally installed plugins, as well as remote plugins from github or a remote HTTP url. + +## ionic state restore + +The `ionic state restore` command looks at the `cordovaPlugins` and `cordovaPlatforms` attributes in your `package.json` file to restore your application with those platforms and plugins. + +In the package.json file, `cordovaPlugins` will be stored as just their `ID` if they are installed from the Cordova registry. However, if they are local or remote, they will be stored as an object with those properties. Also to note, variables are stored for plugins with variables like the Facebook connect plugin. + +The `cordovaPlatforms` follow the same convention of either an `ID` for the platform name, if they are local or remote, they will be stored as an object with those properties. + +If you'd like, you can populate the `cordovaPlugins` and `cordovaPlatforms` by hand, then use the `ionic state restore` command to get your app ready to go. + +## ionic state clear + +The `ionic state clear` method will clear out your platforms and plugins directories, as well as removing the `cordovaPlugins` and `cordovaPlaforms` attributes in your `package.json` file. + +## ionic state reset + +The `ionic state reset` method will first remove your platforms and plugins folders. Then it will look at your `package.json` file to re-install the platforms and plugins as specified there. + +This command can be helpful for you to reinstall your plugins and platforms to get a fresh start. + -:mega: **Support/Questions?** Please see our [Support Page][ionic-support] for general support questions. The issues on GitHub should be reserved for bug reports and feature requests. +# ionic.project file -:sparkling_heart: **Want to contribute?** Please see [CONTRIBUTING.md](https://github.com/ionic-team/ionic-cli/blob/develop/CONTRIBUTING.md). +The ionic.project is a configuration for an ionic project that stores the following: -[ionic-homepage]: https://ionicframework.com -[ionic-cli-docs]: https://ionicframework.com/docs/cli -[ionic-support]: https://ionicframework.com/support +* `app_id` - the associated app ID in ionic.io. +* `browsers` - the installed browsers (CrossWalk). +* `createDocumentRoot` - boolean setting stating to create the designated.document root upon serve (for CI, using `app` folder, and compiling) +* `defaultBrowser` - the browser they prefer to use with `ionic serve`. Chrome, Firefox, etc. +* `documentRoot` - the associated document root with HTML/JS/CSS files. +* `gulpDependantTasks` - gulp tasks that are run before `ionic serve` launches. Think of `gulp build`, `gulp sass`, etc. +* `gulpStartupTasks` - gulp tasks that are run and kept alive during `ionic serve`. +* `imagePaths` - paths relative to document root that specify the release feature to compress images. +* `proxies` - designated proxies to use during `ionic serve`. +* `name` - the name of the application. +* `sass` - the setting to watch sass during `ionic serve`. +* `watchPatterns` - the patterns to watch and live reload during `ionic.serve`. -[ci-badge]: https://github.com/ionic-team/ionic-cli/workflows/CI/badge.svg -[ci-badge-url]: https://github.com/ionic-team/ionic-cli/actions?query=workflow%3ACI -[npm-badge]: https://img.shields.io/npm/v/@ionic/cli.svg -[npm-badge-url]: https://www.npmjs.com/package/@ionic/cli diff --git a/bin/ionic b/bin/ionic new file mode 100755 index 0000000000..e2cca06895 --- /dev/null +++ b/bin/ionic @@ -0,0 +1,9 @@ +#!/usr/bin/env node + +'use strict'; + +process.title = 'ionic'; + +var IonicCli = require('../lib/cli'); + +IonicCli.run(process.argv); diff --git a/e2e/e2e.spec.js b/e2e/e2e.spec.js new file mode 100644 index 0000000000..1ea7e017f7 --- /dev/null +++ b/e2e/e2e.spec.js @@ -0,0 +1,111 @@ +var fs = require('fs'), + helpers = require('./helpers'), + path = require('path'), + Q = require('q'), + shell = require('shelljs'); + +var IonicCli = require('../lib/cli'); +var IonicAppLib = require('ionic-app-lib'); + +var tmpDir = helpers.tmpDir('create_test'); +var appName = 'TestIonic'; +var appId = 'org.ionic.testing'; +var project = path.join(tmpDir, appName); +var optimist = require('optimist'); +var start = IonicAppLib.start; +var utils = IonicAppLib.utils; +var optimistSpy; + + +describe('end-to-end', function() { + beforeEach(function() { + jasmine.getEnv().defaultTimeoutInterval = 150000; + // if (optimistSpy) { + // optimistSpy.reset(); + // } + optimistSpy = spyOn(optimist, 'boolean'); + optimist.boolean.reset(); + + //Mock out call to get the app directory, return our project + spyOn(utils, 'getProjectDirectory').andReturn(project); + + //Disable console.log statements + // spyOn(IonicAppLib.events, 'on'); + // spyOn(process.stdout, 'write'); + spyOn(IonicAppLib.multibar, 'newBar').andReturn({tick: function(){}}); + + + shell.rm('-rf', project); + shell.mkdir('-p', tmpDir); + }); + afterEach(function() { + process.chdir(path.join(__dirname, '..')); // Needed to rm the dir on Windows. + // shell.rm('-rf', tmpDir); + }); + + describe('#start e2e', function() { + it('should call start with default template and folder name', function(done) { + console.log('default template'); + var args = { _: ['start', 'test'], verbose: true}; + //Mock out args from the commands. + // optimistSpy.andReturn({argv: args}); + optimistSpy.andCallFake(function(){ + return {argv: args}; + }); + + Q() + .then(function() { + return IonicCli.run(args); + }).then(function(){ + expect(path.join(project, 'www', 'index.html')).toExist(); + expect(path.join(project, 'www', 'templates', 'tabs.html')).toExist(); + }) + .catch(function(error) { + expect('this').toBe('not this'); + }) + .fin(done); + }); + + it('should call start with sidemenu template and folder name', function(done) { + console.log('sidemenu template'); + var args = { _: ['start', 'test', 'sidemenu'], verbose: true}; + //Mock out args from the commands. + // optimistSpy.andReturn({argv: args}); + optimistSpy.andCallFake(function(){ + return {argv: args}; + }); + + Q() + .then(function() { + return IonicCli.run(args); + }).then(function(){ + expect(path.join(project, 'www', 'index.html')).toExist(); + expect(path.join(project, 'www', 'templates', 'menu.html')).toExist(); + }) + .catch(function(error) { + expect('this').toBe('not this'); + }) + .fin(done); + }); + + it('should call start with blank template and folder name', function(done) { + var args = { _: ['start', 'test', 'blank'], verbose: true}; + //Mock out args from the commands. + optimistSpy.andCallFake(function(){ + return {argv: args}; + }); + + Q() + .then(function() { + return IonicCli.run(args); + }).then(function(){ + expect(path.join(project, 'www', 'index.html')).toExist(); + expect(path.join(project, 'www', 'js', 'app.js')).toExist(); + }) + .catch(function(error) { + expect('this').toBe('not this'); + }) + .fin(done); + }); + }); +}); diff --git a/e2e/helpers.js b/e2e/helpers.js new file mode 100644 index 0000000000..3db75d5c96 --- /dev/null +++ b/e2e/helpers.js @@ -0,0 +1,64 @@ +/** + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ + +var path = require('path'), + fs = require('fs'), + shell = require('shelljs'), + os = require('os'); + +module.exports.tmpDir = function(subdir) { + var dir = path.join(os.tmpdir(), 'e2e-test'); + if (subdir) { + dir = path.join(dir, subdir); + } + shell.mkdir('-p', dir); + return dir; +}; + +// Returns the platform that should be used for testing on this host platform. +/* +var host = os.platform(); +if (host.match(/win/)) { + module.exports.testPlatform = 'wp8'; +} else if (host.match(/darwin/)) { + module.exports.testPlatform = 'ios'; +} else { + module.exports.testPlatform = 'android'; +} +*/ + +// Just use Android everywhere; we're mocking out any calls to the `android` binary. +module.exports.testPlatform = 'android'; + +// Add the toExist matcher. +beforeEach(function() { + this.addMatchers({ + 'toExist': function() { + var notText = this.isNot ? ' not' : ''; + var self = this; + + this.message = function() { + return 'Expected file ' + self.actual + notText + ' to exist.'; + }; + + return fs.existsSync(this.actual); + } + }); +}); + diff --git a/e2e/shell.spec.js b/e2e/shell.spec.js new file mode 100644 index 0000000000..d6ec55e2b6 --- /dev/null +++ b/e2e/shell.spec.js @@ -0,0 +1,146 @@ +var fs = require('fs'), + helpers = require('./helpers'), + path = require('path'), + Q = require('q'), + request = require('request'), + shell = require('shelljs'); + +var IonicCli = require('../lib/cli'); +var IonicAppLib = require('ionic-app-lib'); +var Serve = IonicAppLib.serve; + +var tmpDir = helpers.tmpDir('create_test'); +var appName = 'TestIonic'; +var appId = 'org.ionic.testing'; +var project = path.join(tmpDir, appName); +var optimist = require('optimist'); +var start = IonicAppLib.start; +var utils = IonicAppLib.utils; +var optimistSpy; + +// What to test + + +// * [ ] Start +// * [ ] Serve +// * [ ] Run +// * [ ] + + +ddescribe('end-to-end', function() { + beforeEach(function() { + jasmine.getEnv().defaultTimeoutInterval = 150000; + // if (optimistSpy) { + // optimistSpy.reset(); + // } + optimistSpy = spyOn(optimist, 'boolean'); + optimist.boolean.reset(); + + //Mock out call to get the app directory, return our project + spyOn(utils, 'getProjectDirectory').andReturn(project); + + //Disable console.log statements + // spyOn(IonicAppLib.events, 'on'); + // spyOn(process.stdout, 'write'); + spyOn(IonicAppLib.multibar, 'newBar').andReturn({tick: function(){}}); + + + shell.rm('-rf', project); + shell.mkdir('-p', tmpDir); + + //Copy over created project here. + + }); + + afterEach(function() { + process.chdir(path.join(__dirname, '..')); // Needed to rm the dir on Windows. + // shell.rm('-rf', tmpDir); + }); + + describe('#start', function(){ + xit('should start a new app', function(done) { + shell.cd(tmpDir); + shell.exec('echo "yes" | ionic start s1'); + // expect(path.join(project, 's1', 'www', 'index.html')).toExist(); + // expect(path.join(project, 's1', 'www', 'templates', 'tabs.html')).toExist(); + shell.cd(path.join(tmpDir, 's1')); + + // var deferred = Q.defer(); + + // spyOn(Serve, 'showFinishedServeMessage').andReturn(deferred.promise); + + Q() + .then(function() { + // var deferred = Q.defer(); + // shell.exec('ionic serve -b & ', {async: true}); + // deferred.resolve(); + // }); + // return deferred.promise; + }) + .then(function(){ + console.log('ionic serve is done'); + var deferred = Q.defer(); + + request({ url: 'http://0.0.0.0:8100' }, function(err, res, body) { + try { + console.log('body returned', body); + deferred.resolve(body); + } catch(e) { + deferred.reject(body); + } + }); + return deferred.promise; + }) + .then(function(result) { + console.log('result:', result); + expect(result).toBe(true); + }) + .catch(function(err){ + expect('this').toBe('not this'); + }) + .fin(done); + + }); + + iit('should start a new app and run it', function() { + shell.cd(tmpDir); + shell.exec('echo "yes" | ionic start ' + appName); + shell.cd(project); + + expect(path.join(project, 'www', 'index.html')).toExist(); + expect(path.join(project, 'www', 'templates', 'tabs.html')).toExist(); + + shell.exec('ionic plugin add org.apache.cordova.splashscreen'); + + expect(path.join(project, 'plugins', 'org.apache.cordova.splashscreen', 'plugin.xml')).toExist(); + + shell.exec('ionic platform add android'); + + expect(path.join(project, 'platforms', 'android', 'AndroidManifest.xml')).toExist(); + expect(path.join(project, 'resources', 'icon.png')).toExist(); + + shell.exec('ionic hooks add'); + expect(path.join(project, 'hooks', 'after_plugin_add', '010_register_plugin.js')).toExist(); + + shell.exec('ionic hooks remove'); + expect(path.join(project, 'hooks', 'after_plugin_add', '010_register_plugin.js')).not.toExist(); + + shell.exec('ionic build ios'); + expect(path.join(project, 'platforms', 'ios', 'build', 'emulator', [appName, '.app'].join(''), 'config.xml')).toExist(); + + shell.exec('ionic build android'); + //NOTE this expects you're using ant to build. In cordova android 4.0.0 - gradle is used, ant-build wont be there. + expect(path.join(project, 'platforms', 'android', 'ant-build', 'MainActivity-debug.apk')).toExist(); + expect(path.join(project, 'platforms', 'android', 'build.xml')).toExist(); + + shell.exec('ionic browser add crosswalk'); + expect(path.join(project, 'engine', 'xwalk-webviews')).toExist(); + + shell.exec('ionic build android'); + expect(path.join(project, 'platforms', 'android', 'gradle.properties')).toExist(); + expect(path.join(project, 'platforms', 'android', 'build', 'outputs', 'apk', 'android-armv7-debug.apk')).toExist(); + expect(path.join(project, 'platforms', 'android', 'build', 'outputs', 'apk', 'android-x86-debug.apk')).toExist(); + + }); + }); +}); diff --git a/jest.config.base.js b/jest.config.base.js deleted file mode 100644 index a51134bb8a..0000000000 --- a/jest.config.base.js +++ /dev/null @@ -1,16 +0,0 @@ -module.exports = { - preset: 'ts-jest', - globals: { - 'ts-jest': { - diagnostics: { - // warnOnly: true, - }, - tsConfig: { - types: [ - "node", - "jest", - ], - }, - }, - }, -}; diff --git a/lerna.json b/lerna.json deleted file mode 100644 index 6648afabd4..0000000000 --- a/lerna.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "version": "independent", - "packages": [ - "packages/**" - ], - "command": { - "bootstrap": { - "hoist": true - }, - "version": { - "allowBranch": "stable" - } - } -} diff --git a/lib/cli.js b/lib/cli.js new file mode 100644 index 0000000000..94a6b2c0f1 --- /dev/null +++ b/lib/cli.js @@ -0,0 +1,562 @@ +var Cli = module.exports, + colors = require('colors'), + IonicAppLib = require('ionic-app-lib'), + ConfigXml = IonicAppLib.configXml, + IonicStore = require('./ionic/store').IonicStore, + IonicConfig = new IonicStore('ionic.config'), + Info = IonicAppLib.info, + Ionitron = require('./ionic/ionitron'), + optimist = require('optimist'), + path = require('path'), + settings = require('../package.json'), + Tasks = require('./tasks/cliTasks'), + Utils = IonicAppLib.utils, + logging = IonicAppLib.logging, + Q = require('q'), + semver = require('semver'); + +Cli.Tasks = TASKS = Tasks; + +Cli.IONIC_DASH = 'https://apps.ionic.io'; +//Cli.IONIC_DASH = 'http://localhost:8000'; +Cli.IONIC_API = '/api/v1/'; +Cli.PRIVATE_PATH = '.ionic'; + + +// Cli.dev = function dev() { +// if (settings.version.contains('dev') || settings.version.contains('beta') || settings.version.contains('alpha')) { +// return true; +// } +// return false; +// }; + +// The main entry point for the CLI +// This takes the process.argv array for arguments +// The args passed should be unfiltered. +// From here, we will filter the options/args out +// using optimist. Each command is responsible for +// parsing out the args/options in the way they need. +// This way, we can still test with passing in arguments. +Cli.run = function run(processArgv) { + + try { + //First we parse out the args to use them. + //Later, we will fetch the command they are trying to + //execute, grab those options, and reparse the arguments. + var argv = optimist(processArgv.slice(2)).argv; + + Cli.setUpConsoleLoggingHelpers(); + Cli.attachErrorHandling(); + Cli.checkLatestVersion(); + + process.on('exit', function(){ + Cli.printVersionWarning(); + }); + + //Before taking any more steps, lets check their + //environment and display any upgrade warnings + Cli.doRuntimeCheck(settings.version); + + if ((argv.version || argv.v) && !argv._.length) { + return Cli.version(); + } + + if(argv.ionitron) { + var lang = argv.es ? 'es' : 'en'; + return Ionitron.print(lang); + } + + if (argv.verbose) { + logging.logger.level = 'debug'; + } + + if(argv.help || argv.h) { + return Cli.printHelpLines(); + } + + if(argv['stats-opt-out']) { + IonicConfig.set('statsOptOut', true); + IonicConfig.save(); + console.log('Successful stats opt-out'); + return; + } + + var taskSetting = Cli.tryBuildingTask(argv); + if(!taskSetting) { + return Cli.printAvailableTasks(); + } + + var booleanOptions = Cli.getBooleanOptionsForTask(taskSetting); + + argv = optimist(processArgv.slice(2)).boolean(booleanOptions).argv; + + var taskModule = Cli.lookupTask(taskSetting.module); + var taskInstance = new taskModule(); + var promise = taskInstance.run(Cli, argv); + return promise; + } catch (ex) { + logging.logger.debug('Cli.Run - Error', ex); + return Utils.fail(ex); + } +}; + +Cli.getBooleanOptionsForTask = function getBooleanOptionsForTask(task) { + var availableTaskOptions = task.options; + var booleanOptions = []; + + if (availableTaskOptions) { + for (var key in availableTaskOptions) { + if (typeof availableTaskOptions[key] == 'string') { + continue; + } + var optionWithPipe = key; + var optionsSplit = optionWithPipe.split('|'); + booleanOptions.push(optionsSplit[0].substring(2)); + if (optionsSplit.length == 2) { + booleanOptions.push(optionsSplit[1].substring(1)); + } + } + } + + return booleanOptions; +}; + +Cli.setUpConsoleLoggingHelpers = function setUpConsoleLoggingHelpers() { + colors.setTheme({ + silly: 'rainbow', + input: 'grey', + small: 'grey', + verbose: 'cyan', + prompt: ['yellow', 'bold'], + info: 'white', + data: 'grey', + help: 'cyan', + warn: 'yellow', + debug: 'blue', + error: 'red' + }); + + var consoleInfo = console.info; + console.info = function() { + if (arguments.length === 1 && !arguments[0]) return; + var msg = ''; + for (var n in arguments) { + msg += arguments[n] + ' '; + } + consoleInfo.call(console, msg.blue.bold); + }; + + var consoleError = console.error; + console.error = function() { + if (arguments.length === 1 && !arguments[0]) return; + var msg = ' ✗'; + for (var n in arguments) { + msg += ' ' + arguments[n]; + } + consoleError.call(console, msg.red.bold); + }; + + console.success = function() { + if (arguments.length === 1 && !arguments[0]) return; + var msg = ' ✓'; + for (var n in arguments) { + msg += ' ' + arguments[n]; + } + console.log(msg.green.bold); + }; + + //Default level is set to 'info' + IonicAppLib.logging.createDefaultLogger(); +}; + +Cli.lookupTask = function lookupTask(module) { + try { + var taskModule = require(module).IonicTask; + return taskModule; + } catch (ex) { + throw ex; + } +}; + +Cli.printVersionWarning = function printVersionWarning() { + if (Cli.npmVersion && Cli.npmVersion != settings.version.trim()) { + process.stdout.write('\n------------------------------------\n'.red); + process.stdout.write('Ionic CLI is out of date:\n'.bold.yellow); + process.stdout.write( (' * Locally installed version: ' + settings.version + '\n').yellow ); + process.stdout.write( (' * Latest version: ' + Cli.npmVersion + '\n').yellow ); + process.stdout.write( (' * https://github.com/driftyco/ionic-cli/blob/master/CHANGELOG.md\n').yellow ); + process.stdout.write( ' * Run '.yellow + 'npm install -g ionic'.bold + ' to update\n'.yellow ); + process.stdout.write('------------------------------------\n\n'.red); + Cli.npmVersion = null; + } +}; + +Cli.checkLatestVersion = function checkLatestVersion() { + Cli.latestVersion = Q.defer(); + + try { + if (settings.version.indexOf('beta') > -1) { + // don't bother checking if its a beta + Cli.latestVersion.resolve(); + return; + } + + var versionCheck = IonicConfig.get('versionCheck'); + if (versionCheck && ((versionCheck + 86400000) > Date.now() )) { + // we've recently checked for the latest version, so don't bother again + Cli.latestVersion.resolve(); + return; + } + + var proxy = process.env.PROXY || process.env.http_proxy || null; + var request = require('request'); + request({ url: 'http://registry.npmjs.org/ionic/latest', proxy: proxy }, function(err, res, body) { + try { + Cli.npmVersion = JSON.parse(body).version; + IonicConfig.set('versionCheck', Date.now()); + IonicConfig.save(); + } catch(e) {} + Cli.latestVersion.resolve(); + }); + } catch (e) { + Cli.latestVersion.resolve(); + } + + return Cli.latestVersion.promise; +}; + +Cli.tryBuildingTask = function tryBuildingTask(argv) { + if (argv._.length === 0) { + return false; + } + var taskName = argv._[0]; + + return Cli.getTaskWithName(taskName); +}; + +Cli.getTaskWithName = function getTaskWithName(name) { + for (var i = 0; i < TASKS.length; i++) { + var t = TASKS[i]; + if(t.name === name) { + return t; + } + if (t.alt) { + for(var j = 0; j < t.alt.length; j++) { + var alt = t.alt[j]; + if (alt === name) { + return t; + } + } + } + } +}; + +Cli.printIonic = function printIonic() { + var w = function(s) { + process.stdout.write(s); + }; + + w(' _ _ \n'); + w(' (_) (_) \n'); + w(' _ ___ _ __ _ ___ \n'); + w(' | |/ _ \\| \'_ \\| |/ __|\n'); + w(' | | (_) | | | | | (__ \n'); + w(' |_|\\___/|_| |_|_|\\___| CLI v'+ settings.version + '\n'); +}; + +Cli.printAvailableTasks = function printAvailableTasks(argv) { + Cli.printIonic(); + process.stderr.write('\nUsage: ionic task args\n\n=======================\n\n'); + + if (process.argv.length > 2) { + process.stderr.write( (process.argv[2] + ' is not a valid task\n\n').bold.red ); + } + + process.stderr.write('Available tasks: '.bold); + process.stderr.write('(use --help or -h for more info)\n\n'); + + for (var i = 0; i < TASKS.length; i++) { + var task = TASKS[i]; + if (task.summary) { + var name = ' ' + task.name + ' '; + var dots = ''; + while ((name + dots).length < 20) { + dots += '.'; + } + process.stderr.write(name.green.bold + dots.grey + ' ' + task.summary.bold + '\n'); + } + } + + process.stderr.write('\n'); + Cli.processExit(1); +}; + +Cli.printHelpLines = function printHelpLines() { + Cli.printIonic(); + process.stderr.write('\n=======================\n'); + + for (var i = 0; i < TASKS.length; i++) { + var task = TASKS[i]; + if (task.summary) { + Cli.printUsage(task); + } + } + + process.stderr.write('\n'); + Cli.processExit(1); +}; + +Cli.printUsage = function printUsage(d) { + var w = function(s) { + process.stdout.write(s); + }; + + w('\n'); + + var rightColumn = 45; + var dots = ''; + var indent = ''; + var x, arg; + + var taskArgs = d.title; + + for(arg in d.args) { + taskArgs += ' ' + arg; + } + + w(taskArgs.green.bold); + + while( (taskArgs + dots).length < rightColumn + 1) { + dots += '.'; + } + + w(' ' + dots.grey + ' '); + + if(d.summary) { + w(d.summary.bold); + } + + for(arg in d.args) { + if( !d.args[arg] ) continue; + + indent = ''; + w('\n'); + while(indent.length < rightColumn) { + indent += ' '; + } + w( (indent + ' ' + arg + ' ').bold ); + + var argDescs = d.args[arg].split('\n'); + var argIndent = indent + ' '; + + for(x=0; x]*id="(.*)"', 'i'); + id = idRegEx.exec(configString)[1] + pluginToAdd = {id: id, locator: plugin}; + } catch(ex) { + console.log('There was an error retrieving the plugin.xml filr from the 010_register_plugin.js hook', ex); + } + } else { + pluginToAdd = plugin; + } + + if(typeof pluginToAdd == 'string' && packageJSON.cordovaPlugins.indexOf(pluginToAdd) == -1) { + packageJSON.cordovaPlugins.push(pluginToAdd); + } else if (typeof pluginToAdd == 'object') { + var pluginExists = false; + packageJSON.cordovaPlugins.forEach(function(checkPlugin) { + if(typeof checkPlugin == 'object' && checkPlugin.id == pluginToAdd.id) { + pluginExists = true; + } + }) + if(!pluginExists) { + packageJSON.cordovaPlugins.push(pluginToAdd); + } + } +}); + +fs.writeFileSync('package.json', JSON.stringify(packageJSON, null, 2)); diff --git a/lib/hooks/after_plugin_rm/010_deregister_plugin.js b/lib/hooks/after_plugin_rm/010_deregister_plugin.js new file mode 100644 index 0000000000..4f4c6b892e --- /dev/null +++ b/lib/hooks/after_plugin_rm/010_deregister_plugin.js @@ -0,0 +1,27 @@ +#!/usr/bin/env node + +/** + * Remove plugins from cordovaPlugins array after_plugin_rm + */ +var fs = require('fs'); +var packageJSON = require('../../package.json'); + +packageJSON.cordovaPlugins = packageJSON.cordovaPlugins || []; + +process.env.CORDOVA_PLUGINS.split(',').forEach(function (plugin) { + var index = packageJSON.cordovaPlugins.indexOf(plugin); + if (index > -1) { + packageJSON.cordovaPlugins.splice(index, 1); + } else { + //If it didnt find a match, it may be listed as {id,locator} + for(var i = 0, j = packageJSON.cordovaPlugins.length; i < j; i++) { + var packagePlugin = packageJSON.cordovaPlugins[i]; + if(typeof packagePlugin == 'object' && packagePlugin.id == plugin) { + packageJSON.cordovaPlugins.splice(index, 1); + break; + } + } + } +}); + +fs.writeFile('package.json', JSON.stringify(packageJSON, null, 2)); diff --git a/lib/hooks/after_prepare/010_add_platform_class.js b/lib/hooks/after_prepare/010_add_platform_class.js new file mode 100755 index 0000000000..06049c2898 --- /dev/null +++ b/lib/hooks/after_prepare/010_add_platform_class.js @@ -0,0 +1,98 @@ +#!/usr/bin/env node + +// Add Platform Class +// v1.0 +// Automatically adds the platform class to the body tag +// after the `prepare` command. By placing the platform CSS classes +// directly in the HTML built for the platform, it speeds up +// rendering the correct layout/style for the specific platform +// instead of waiting for the JS to figure out the correct classes. + +var fs = require('fs'); +var path = require('path'); + +var rootdir = process.argv[2]; + +function addPlatformBodyTag(indexPath, platform) { + // add the platform class to the body tag + try { + var platformClass = 'platform-' + platform; + var cordovaClass = 'platform-cordova platform-webview'; + + var html = fs.readFileSync(indexPath, { + encoding: 'utf8' + }); + + var bodyTag = findBodyTag(html); + if(!bodyTag) return; // no opening body tag, something's wrong + + if(bodyTag.indexOf(platformClass) > -1) return; // already added + + var newBodyTag = bodyTag; + + var classAttr = findClassAttr(bodyTag); + if(classAttr) { + // body tag has existing class attribute, add the classname + var endingQuote = classAttr.substring(classAttr.length-1); + var newClassAttr = classAttr.substring(0, classAttr.length-1); + newClassAttr += ' ' + platformClass + ' ' + cordovaClass + endingQuote; + newBodyTag = bodyTag.replace(classAttr, newClassAttr); + + } else { + // add class attribute to the body tag + newBodyTag = bodyTag.replace('>', ' class="' + platformClass + ' ' + cordovaClass + '">'); + } + + html = html.replace(bodyTag, newBodyTag); + + fs.writeFileSync(indexPath, html, { + encoding: 'utf8' + }); + + process.stdout.write('add to body class: ' + platformClass + '\n'); + } catch(e) { + process.stdout.write(e); + } +} + +function findBodyTag(html) { + // get the body tag + try{ + return html.match(/])(.*?)>/gi)[0]; + }catch(e){} +} + +function findClassAttr(bodyTag) { + // get the body tag's class attribute + try{ + return bodyTag.match(/ class=["|'](.*?)["|']/gi)[0]; + }catch(e){} +} + +if (rootdir) { + + // go through each of the platform directories that have been prepared + var platforms = (process.env.CORDOVA_PLATFORMS ? process.env.CORDOVA_PLATFORMS.split(',') : []); + + for(var x=0; x No installed build tools found. Please install the Android build tools version 19.1.0 or higher. + +Had to bump the minversionnumber in platform/android/AndroidManifest.xml to 14 from 10. + + + + +## Roadmap for Crosswalk integration + +Cordova release in Jan 2015 is pushing for pluggable web views + +In the meantime, do we go ahead and release now as a beta? + +When they do release, how do we integrate with those changes? + + + +`ionic browser add crosswalk` will add that engine file. +It will also add a platform for android +How do we handle the case of when they want to re-add stuffs? +Removing Android - browser remove feature + diff --git a/lib/ionic/add.js b/lib/ionic/add.js new file mode 100644 index 0000000000..01f593b59a --- /dev/null +++ b/lib/ionic/add.js @@ -0,0 +1,102 @@ +require('shelljs/global') + +var Task = require('./task').Task, + IonicCordova = require('./cordova').IonicTask, + IonicStats = require('./stats').IonicStats, + // argv = require('optimist').argv, + fs = require('fs'), + ioLib = require('ionic-app-lib').ioConfig, + path = require('path'), + bower = require('./bower'); + +var IonicTask = function() {}; + +IonicTask.prototype = new Task(); + +IonicTask.prototype.installBowerComponent = function installBowerComponent(componentName) { + var bowerInstallCommand = 'bower install --save-dev ' + componentName; + + var result = exec(bowerInstallCommand); + + if (result.code != 0) { + //Error happened, report it. + var errorMessage = 'Failed to find the bower component "'.error.bold + componentName.verbose + '"'.error.bold + '.\nAre you sure it exists?'.error.bold; + this.ionic.fail(errorMessage, 'add'); + } else { + var message = 'Bower component installed - ' + componentName; + console.log(message.green) + } +} + +IonicTask.prototype.uninstallBowerComponent = function uninstallBowerComponent(componentName) { + var bowerUninstallCommand = 'bower uninstall --save-dev ' + componentName; + + var result = exec(bowerUninstallCommand); + + if (result.code != 0) { + var errorMessage = 'Failed to find the bower component "'.error.bold + componentName.verbose + '"'.error.bold + '.\nAre you sure it exists?'.error.bold; + this.ionic.fail(errorMessage, 'add') + } else { + var message = 'Bower component removed - ' + componentName; + console.log(message.red); + } +} + +IonicTask.prototype.listComponents = function listComponents() { + try { + var bowerJson = require(path.join(process.cwd(), 'bower.json')); + console.log('Ions, bower components, or addons installed: '.info) + for (var bowerCompIndex in bowerJson.devDependencies) { + console.log(bowerCompIndex.green); + } + }catch(ex) { + console.log('This command can only be used when in an Ionic project directory'.red.bold); + } +} + +// Need to look at bower.json of package just installed and look for any cordova plugins required +// Check the directory in the projects `.bowerrc` file +// Then go to /path/to/bower/components//ionic-plugins.json - 'plugins' +// For each plugins - call 'ionic add plugin ' +IonicTask.prototype.run = function(ionic, argv) { + // console.log('running ', argv._) + this.ionic = ionic; + + IonicStats.t(); + + if (!bower.IonicBower.checkForBower()) { + this.ionic.fail(bower.IonicBower.installMessage, 'add'); + return; + } + + var action = argv._[0] + var componentName = argv._[1]; + var ioSet = false; + + try { + switch (action) { + case 'add': + ioSet = true; + this.installBowerComponent(componentName); + break; + case 'remove': + case 'rm': + this.uninstallBowerComponent(componentName); + break; + case 'list': + case 'ls': + case '': + this.listComponents(); + break; + } + } catch (error) { + var errorMessage = error.message ? error.message : error; + this.ionic.fail(errorMessage, 'service') + } + + // Inject the component into our index.html, if necessary, and save the app_id + ioLib.injectIoComponent(ioSet, componentName); + ioLib.warnMissingData(); +} + +exports.IonicTask = IonicTask; diff --git a/lib/ionic/app.js b/lib/ionic/app.js new file mode 100644 index 0000000000..23fd8ad77d --- /dev/null +++ b/lib/ionic/app.js @@ -0,0 +1,191 @@ +var fs = require('fs'), + path = require('path'), + parseUrl = require('url').parse, + request = require('request'), + argv = require('optimist').argv, + FormData = require('form-data'), + IonicProject = require('./project'), + IonicTask = require('./task').IonicTask, + IonicStats = require('./stats').IonicStats, + Task = require('./task').Task, + IonicLoginTask = require('./login').IonicTask, + IonicUploadTask = require('./upload').IonicTask, + IonicUtils = require('./utils'); + +var IonicTask = function() {}; + +IonicTask.prototype = new Task(); + +IonicTask.prototype.run = function(ionic) { + var self = this; + + self.ionic = ionic; + self.inputValues = {}; + self.useCmdArgs = false; + + this.getCmdLineOptions(); + + ionic.IONIC_DASH = 'http://stage.apps.ionic.io'; + + var login = new IonicLoginTask(); + login.get(ionic, function(jar) { + self.jar = jar; + + if (argv['versions'] || argv.v) { + self.versions(self); + } else if (argv['deploy'] || argv.d) { + if (!self.inputValues['uuid']) { + // Deploying a new version + var upload = new IonicUploadTask(); + + upload.setNote(self.inputValues['note'] || ''); + upload.run(self.ionic, argv, function() { + self.deploy(self); + }); + } else { + self.deploy(self); + } + } + }); +}; + +IonicTask.prototype.versions = function(self) { + var project = null; + + try { + project = IonicProject.load(); + } catch (ex) { + self.ionic.fail(ex.message); + return + } + + if (!project.get('app_id')) { + console.log("No versions uploaded!".bold.red); + return false; + } + + var form = new FormData(); + form.append('csrfmiddlewaretoken', self.getCsrfToken(self.jar)); + + var url = self.ionic.IONIC_DASH + self.ionic.IONIC_API + 'app/' + project.get('app_id') + '/versions'; + var params = parseUrl(url); + + form.submit({ + protocol: params.protocol, + hostname: params.hostname, + port: params.port, + path: params.path, + headers: form.getHeaders({ + cookie: IonicUtils.transformCookies(self.jar) + }) + }, + function (err, response) { + if (err) { + return self.ionic.fail('Error logging in: ' + err); + } + + response.setEncoding('utf8'); + response.on("data", function(data) { + try { + var d = JSON.parse(data); + if (d.errors && d.errors.length) { + for (var j = 0; j < d.errors.length; j++) { + console.log((d.errors[j]).bold.error); + } + return self.ionic.fail('Unable to fetch versions list'); + } + + var heading = [' UUID ', ' Created ', ' Note ']; + var heading_underline = ['------------', '----------', '----------------------------'] + + console.log(heading.join('\t')); + console.log(heading_underline.join('\t')); + + for (var j = 0; j < d.length; j++) { + var active = (d[j].active) ? ' *' : ' '; + var uuid = (d[j].active) ? d[j].uuid.substring(0,8).green : d[j].uuid.substring(0,8); + var note = d[j].note.substring(0,25) || '\t'; + + console.log([active + ' ' + uuid, ' ' + d[j].created, ' ' + note].join('\t')); + } + } catch(parseEx) { + return self.ionic.fail('Error response: ' + parseEx); + } + }); + }); +} + +IonicTask.prototype.deploy = function(self) { + var project = null; + + try { + project = IonicProject.load(); + } catch (ex) { + self.ionic.fail(ex.message); + return + } + + var url = self.ionic.IONIC_DASH + self.ionic.IONIC_API + 'app/' + project.get('app_id') + '/deploy'; + + request({ + method: 'POST', + url: url, + jar: self.jar, + form: { + uuid: self.inputValues['uuid'], + csrfmiddlewaretoken: self.getCsrfToken(self.jar) + }, + headers: form.getHeaders({ + cookie: jar.map(function(c) { + return c.key + "=" + encodeURIComponent(c.value); + }).join("; ") + }) + }, + function (err, response, body) { + if (err) { + return self.ionic.fail('Error deploying version: ' + err); + } + + try { + var d = JSON.parse(body); + if (d.errors && d.errors.length) { + for (var j = 0; j < d.errors.length; j++) { + console.log((d.errors[j]).bold.error); + } + return self.ionic.fail('Unable to deploy version'); + } + + console.log("Successfully deployed " + d.uuid); + + } catch (parseEx) { + return self.ionic.fail('Error response: ' + parseEx); + } + } + ); +}; + +IonicTask.prototype.getCmdLineOptions = function() { + var self = this; + + function getCmdArgValue(propertyName, shortName) { + var value = argv[propertyName] || argv[shortName]; + if(value) { + self.inputValues[propertyName] = value; + self.useCmdArgs = true; + } + } + + getCmdArgValue('note', 'n'); + getCmdArgValue('uuid', 'u'); +}; + +IonicTask.prototype.getCsrfToken = function(jar) { + for (var i = 0; i < jar.length; i++) { + if (jar[i].key == 'csrftoken') { + return jar[i].value; + } + } + return ''; +} + +exports.IonicTask = IonicTask; diff --git a/lib/ionic/assets/gradle.properties b/lib/ionic/assets/gradle.properties new file mode 100644 index 0000000000..b98069121e --- /dev/null +++ b/lib/ionic/assets/gradle.properties @@ -0,0 +1,8 @@ +cdvBuildMultipleApks=true +# cdvCompileSdkVersion=14 +# cdvBuildToolsVersion=22 +# cdvVersionCode=14 +# cdvMinSdkVersion=14 +# cdvReleaseSigningPropertiesFile=/users/path +# cdvDebugSigningPropertiesFile=/users/path +# cdvBuildArch=x86 diff --git a/lib/ionic/assets/ionitron.txt b/lib/ionic/assets/ionitron.txt new file mode 100644 index 0000000000..77a6540069 --- /dev/null +++ b/lib/ionic/assets/ionitron.txt @@ -0,0 +1,47 @@ + + + + h + `-oooooo/`.++ + ::-oooooooo... + `:.`:oooooo/ + ```-:oo` + /o. + ./:--:::::--..` + .-/+ooooooooooooooooo+/:. + `-/ooooooooooooooooooooooooooo+:. + -+ooooooooooooooooooooooooooooooooo+:` + :ooooooooooooooooooooooooooooooooooooooo/` + :ooooooooooooooooooooooooooooooooooooooooooo/` + `+oooooooooooooooooooooooooooooooooooooooooooooo- + -ooooooooooooooooooooooooooooooooooooooooooooooooo/ + -ooooooooooooooooooooooooooooooooooooooooooooooooooo/.-. + -ooooooooooooooooooooooooooooooooooooooooooooooooooooo/+o+` + `ooooooooooooooooooooooooooooooooooooooooooooooooooooooo:ooo` + /ooooooooooooooooooooooooooooooooooooooooooooooooooooooo+ooo/ + -+ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo+oooh + -ooooooo+ooooooooooooooooooooooooooooooooooooooooooooooooooooooooo + ooooooooo:oooooooooooooooooooooooooooooooooooooooooooooooooooooooo + ooooooooo+:ooooooooooooooooooooooooooooooooooooooooooooooooooooooo + oooooooooo.oooooooooooooooooooooooooooooooooooooooooooooooooooooo+ + oooooooooo:/oooooooooooooooooooooooooooooooooooooooooooooooooooo+` + +ooooooooo//oooooooooooooooooooooooooooooooooooooooooooooooooo`` + .ooooooooo/+oooooooooooooooooooooo:.-+oooooooo: `/oooooooooo- + :oooooooo/oooooooooooooooooooooo: `oooooooo. :ooooooooo/ + :ooooooooooooooooooooooooooooooo:--+ooooooooo+/oooooooooo/ + `:////ooooooooooooooooooooooooooooooooooooooooooooooooo: + `+oooooooooooooooooooooooooooooooooooooooooooooo. + -ooooooooooooooooooooooooooooooooooooooooooo:` + `:ooooooooooooooooooooooooooooooooooooooo/` + ./ooooooooooooooooooooooooooooooooo+-` + `-/ooooooooooooooooooooooooooo/-\`\ + `-:+ooooooooooooooooo+/-. \ \ + '\:--::::--/' | \ + / \ + -----------------------------------------------* *--------- + / \ + / \ + | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | + \ / + \ / + *--------------------------------------------------------------* diff --git a/lib/ionic/assets/preview.html b/lib/ionic/assets/preview.html new file mode 100644 index 0000000000..aad119a41f --- /dev/null +++ b/lib/ionic/assets/preview.html @@ -0,0 +1,190 @@ + + + + + + + + + + + + + + +
+ + +
+ + + +
+
+

iOS

+ +
+
+
+
+

Android

+ +
+
+
+ + + + diff --git a/lib/ionic/bower.js b/lib/ionic/bower.js new file mode 100644 index 0000000000..3c6657c23d --- /dev/null +++ b/lib/ionic/bower.js @@ -0,0 +1,59 @@ +require('shelljs/global') + +var fs = require('fs'), + path = require('path'), + colors = require('colors'); + +exports.IonicBower = { + + setIonicVersion: function(newVersion) { + var bowerData = this.getData(); + if (!bowerData.devDependencies) bowerData.devDependencies = {}; + bowerData.devDependencies.ionic = 'driftyco/ionic-bower#' + newVersion; + this.saveData(bowerData); + }, + + setAppName: function(newAppName) { + var bowerData = this.getData(); + bowerData.name = newAppName; + this.saveData(bowerData); + }, + + getData: function() { + var bowerFilePath = path.resolve('bower.json'); + + try { + if (fs.existsSync(bowerFilePath)) { + return require(bowerFilePath); + } + } catch (e) {} + + return { + "name": "HelloIonic", + "private": "true" + }; + }, + + saveData: function(bowerData) { + try { + var bowerFilePath = path.resolve('bower.json'); + fs.writeFileSync(bowerFilePath, JSON.stringify(bowerData, null, 2)); + } catch (e) { + console.log(('Error saving ' + bowerFilePath + ': ' + e).error.bold); + } + }, + + checkForBower: function checkForBower() { + var installed = false; + try { + var result = exec('bower -v', {silent: true}); + if (result.output.indexOf('command not found') == -1 && result.output.indexOf('not recognized') == -1) { + installed = true; + } + } catch (ex) { } + return installed; + }, + + installMessage: 'You must have bower installed to continue. \nType `npm install -g bower`' + +}; diff --git a/lib/ionic/browser.js b/lib/ionic/browser.js new file mode 100644 index 0000000000..99d1dddfd1 --- /dev/null +++ b/lib/ionic/browser.js @@ -0,0 +1,107 @@ +//Cross walk process + +//See the cordova-engine-crosswalk plugin for how to install the browser as a plugin +//https://github.com/MobileChromeApps/cordova-crosswalk-engine + +//Find CrossWalk webviews here: +//https://download.01.org/crosswalk/releases/crosswalk/android/stable/ + +//Download the release for cordova-crosswalk-engine +//Download the release for cordova-android with crosswalk +//Ensure Android API 19 is installed +//Run ionic platform rm android +//Run ionic platform add ./engine/cordova-android-crosswalk +//Run ionic plugin add ./engine/cordova-crosswalk-engine +//Run android update project on android file +//Run project - cordova run android BUILD_MULTIPLE_APKS=true + +var argv = require('optimist').argv, + Task = require('./task').Task, + IonicStats = require('./stats').IonicStats, + _ = require('underscore'), + IonicProject = require('./project'), + IonicAppLib = require('ionic-app-lib'), + Info = IonicAppLib.info, + Browser = IonicAppLib.browser; + +var IonicTask = function() {}; +IonicTask.prototype = new Task(); + +IonicTask.prototype.run = function run(ionic) { + try { + var self = this; + this.ionic = ionic; + + if(!Info.checkRuntime()) { + console.log('\nPlease update your runtime environment before running the browser command'); + return + } + + if(argv._.length < 2) { + return this.ionic.fail('Invalid command usage', 'browser'); + } + + var cmdArg, x, y, hasValidCmd = false; + + cmdArg = argv._[1] + + var validCommands = 'add remove rm list ls revert versions clean upgrade'.split(' '); + for(y=0; y 3 ? process.argv.slice(3) : []); + var cmdArg, x, y; + + // backwards compatibility prior to fully wrapping cordova cmds + if(cmdName == 'platform' && cmdArgs.length > 0 ) { + // `ionic platform ` used to actually run `ionic platform add ` + // if a cordova platform cmd isn't the cmd then automatically insert `add` + var hasCordovaCmd = false; + var validCommands = 'add remove rm list ls update up check'.split(' '); + for(x=0; x= COLUMN_LIMIT) { + table.push(row); + row = []; + count = 0; + } + row.push(addDoc); + count++; + }); + table.push(row); + console.log(table.toString()); + }); + + } catch (ex) { + console.log('Error listing docs:', ex); + } + console.log('Type "ionic docs " to open the help document for that doc.'.blue.bold); +}; + +Docs.openDefault = function openDefault() { + var Info = require('ionic-app-lib').info; + + var envInfo = Info.gatherInfo(); + var ionicVersion = envInfo.ionic; + + var url = 'http://ionicframework.com/docs/'; + if (ionicVersion) { + url += ionicVersion + '/api'; + console.log('Ionic version:', ionicVersion); + } + return require('open')(sanitizeUrl(url)); +}; + +Docs.lookUpCommand = function lookUpCommand(helpDoc) { + // console.log('Going thru doc commands', helpDoc) + //Go through all the different help topics + var docsList = IonicDocs.api; + helpDoc = helpDoc.replace(/-[a-zA-Z]/g, function(match) { + return match[1].toUpperCase(); + }); + + var docs = _.map(docsList, function(doc) { + return _.map(doc.docs, function(docItem) { + return { + topic: doc.id, + doc: docItem + }; + }); + }); + docs = _.flatten(docs); + + var match = _.find(docs, { doc: helpDoc }) || _.find(docs, { doc: '$' + helpDoc}); + if (match) { + return openDoc(match.topic, match.doc); + + } else if (helpDoc.length >= 3) { + + var lowerHelp = helpDoc.toLowerCase(); + var matches = docs + .filter(function(item) { + var lowerDoc = item.doc.toLowerCase(); + return lowerDoc.indexOf(lowerHelp) > -1 || + lowerHelp.indexOf(lowerDoc) > -1; + }) + .sort(function(a,b) { + return a.doc.length < b.doc.length ? -1 : 1; + }) + .slice(0,3); + + if (matches.length === 0) return console.log('No matching docs found for "' + helpDoc.cyan + '".'); + + prompt.message = 'Did you mean '; + if (matches.length > 1) { + prompt.message += matches.map(function(item, i) { + var str = ''; + if (i > 0) { + if (i === matches.length - 1) { + str = ' or '; + } else { + str = ', '; + } + } + str += '(' + (i + 1 + '').cyan + ') ' + item.doc; + return str; + }).join(''); + } else { + prompt.message += matches[0].doc; + } + prompt.message += '?'; + prompt.start(); + prompt.get({ + name: 'choice', + default: matches.length === 1 ? 'yes' : '1', + }, function(err, result) { + var num; + if (matches.length == 1) { + if (result.choice.match(/^y/i, result.choice.trim())) { + num = 1; + } + } else { + num = parseInt(result.choice.trim()); + } + if (!isNaN(num) && num > 0 && num <= matches.length) { + num--; // chioce (1-based) to index (0-based) + return openDoc(matches[num].topic, matches[num].doc); + } + }); + + } + + function openDoc(topic, doc) { + var url = sanitizeUrl('http://ionicframework.com/docs/api/' + topic + '/' + doc); + console.log('Opening Ionic document:'.green.bold, url.green.bold); + return require('open')(url); + } +}; + +IonicTask.prototype.run = function run(ionic) { + var self = this; + this.ionic = ionic; + + var docCmd = argv._[1]; + // console.log('docCmd', docCmd); + + if(docCmd == 'ls') { + Docs.list(); + } else if (!docCmd) { + // console.log('openDefault') + Docs.openDefault(); + } else { + // console.log('look up command', docCmd) + //Look up what it was + Docs.lookUpCommand(docCmd); + } + + IonicStats.t(); + +}; + +exports.IonicTask = IonicTask; diff --git a/lib/ionic/help.js b/lib/ionic/help.js new file mode 100644 index 0000000000..051c7179bb --- /dev/null +++ b/lib/ionic/help.js @@ -0,0 +1,41 @@ +var IonicProject = require('./project'), + IonicTask = require('./task').IonicTask, + IonicStats = require('./stats').IonicStats, + argv = require('optimist').argv, + Task = require('./task').Task; + +var IonicTask = function() {}; + +IonicTask.prototype = new Task(); + +IonicTask.prototype.run = function(ionic) { + // var self = this; + + // self.ionic = ionic; + + var command = argv._[1] ? argv._[1] : ''; + + var task = ionic.getTaskWithName(command); + + if(command == '') { + ionic.printHelpLines(); + return; + } + else if (!task) { + console.log('Command not found.'.red.bold) + return + } + + ionic.printIonic(); + process.stderr.write('\n=======================\n'); + + + ionic.printUsage(task); + + process.stderr.write('\n\n'); + + + // IonicStats.t(); +} + +exports.IonicTask = IonicTask; diff --git a/lib/ionic/hooks.js b/lib/ionic/hooks.js new file mode 100644 index 0000000000..77ee2498dc --- /dev/null +++ b/lib/ionic/hooks.js @@ -0,0 +1,49 @@ +var fs = require('fs'), + path = require('path'), + argv = require('optimist').argv, + Q = require('q'), + shelljs = require('shelljs'), + Task = require('./task').Task, + IonicStats = require('./stats').IonicStats, + _ = require('underscore'), + IonicProject = require('./project'), + IonicInfo = require('./info').IonicTask, + IonicAppLib = require('ionic-app-lib'), + Hooks = IonicAppLib.hooks; + +shelljs.config.silent = true; + +var IonicTask = function() {}; + +IonicTask.prototype = new Task(); + +IonicTask.prototype.run = function run(ionic) { + var self = this; + this.ionic = ionic; + + var cmd = argv._[1]; + try { + + switch(cmd) { + case 'add': + Hooks.add(process.cwd()); + break; + case 'remove': + Hooks.remove(process.cwd()); + break; + case 'perm': + case 'permissions': + Hooks.setHooksPermission(process.cwd()); + break; + default: + console.log('Please supply a command - either add or remove'.red.bold); + } + } catch (ex) { + console.log('error:', ex, ex.stack); + } + + IonicStats.t(); + +}; + +exports.IonicTask = IonicTask; diff --git a/lib/ionic/info.js b/lib/ionic/info.js new file mode 100644 index 0000000000..8d63911b18 --- /dev/null +++ b/lib/ionic/info.js @@ -0,0 +1,33 @@ +var fs = require('fs'), + path = require('path'), + argv = require('optimist').argv, + Task = require('./task').Task, + ionicAppLib = require('ionic-app-lib'), + Info = ionicAppLib.info, + IonicStats = require('./stats').IonicStats; + +var IonicTask = function() {}; + +IonicTask.prototype = new Task(); + +IonicTask.prototype.run = function(ionic) { + this.ionic = ionic; + + try { + + var info = Info.gatherInfo(); + + Info.getIonicVersion(info, process.cwd()); + Info.getIonicCliVersion(info, path.join(__dirname, '../../')); + + Info.printInfo(info); + + Info.checkRuntime(); + } catch (ex) { + this.ionic.fail('There was an error retrieving your environment information:', ex); + } + + IonicStats.t(); +} + +exports.IonicTask = IonicTask; diff --git a/lib/ionic/io-config.js b/lib/ionic/io-config.js new file mode 100644 index 0000000000..e0f39c05ec --- /dev/null +++ b/lib/ionic/io-config.js @@ -0,0 +1,60 @@ +var ioLib = require('ionic-app-lib').ioConfig, + argv = require('optimist').argv, + fs = require('fs'), + prompt = require('prompt'), + IonicProject = require('./project'), + IonicTask = require('./task').IonicTask, + Task = require('./task').Task, + IonicLoginTask = require('./login').IonicTask; + +var IonicTask = function() {}; + +IonicTask.prototype = new Task(); + +IonicTask.prototype.run = function(ionic) { + var self = this; + self.ionic = ionic; + + self.project = null; + + try { + self.project = IonicProject.load(); + } catch (ex) { + self.ionic.fail(ex.message); + return + } + + var set = false, + write = false, + key = '', + val = ''; + + if (argv['_'][1] == 'set' || argv['_'][1] == 'unset' || argv['_'][1] == 'build' || argv['_'][1] == 'info') { + if (argv['_'][1] == 'build') { + write = true; + ioLib.writeIoConfig(false, false, true); + } else if (argv['_'][1] == 'info') { + write = true; + ioLib.listConfig(); + } else { + if (argv['_'][1] == 'set') { + set = true; + } + if (argv['_'][2]) { + key = argv['_'][2]; + if (argv['_'][3] && set) { + val = argv['_'][3]; + } + } else { + self.ionic.fail("Invalid syntax, use 'ionic config key value'"); + } + } + } else { + self.ionic.fail("Invalid command, must use 'set', 'unset', 'build', or 'info'"); + } + if (!write) { + ioLib.writeIoConfig(key, val, set); + } +}; + +exports.IonicTask = IonicTask; diff --git a/lib/ionic/io-init.js b/lib/ionic/io-init.js new file mode 100644 index 0000000000..327ab79719 --- /dev/null +++ b/lib/ionic/io-init.js @@ -0,0 +1,49 @@ +var ioLib = require('ionic-app-lib').ioConfig, + argv = require('optimist').argv, + prompt = require('prompt'), + IonicProject = require('./project'), + IonicTask = require('./task').IonicTask, + Task = require('./task').Task, + Login = require('ionic-app-lib').login, + LoginTask = require('./login'), + Utils = require('./utils'); + +var IonicTask = function() {}; + +IonicTask.prototype = new Task(); + +IonicTask.prototype.run = function(ionic) { + var self = this; + self.ionic = ionic; + + self.project = null; + + try { + self.project = IonicProject.load(); + } catch (ex) { + self.ionic.fail(ex.message); + return + } + + Login.retrieveLogin() + .then(function(jar){ + if (!jar) { + console.log('No previous login existed. Attempting to log in now.'); + return LoginTask.login(argv); + } + return jar; + }) + .then(function(jar) { + if (argv['_'][1] == 'init') { + ioLib.initIoPlatform(process.cwd(), jar); + } else { + self.ionic.fail("Invalid command"); + } + }) + .catch(function(ex) { + console.log('Error', ex, ex.stack); + Utils.fail(ex); + }); +}; + +exports.IonicTask = IonicTask; diff --git a/lib/ionic/ionitron.js b/lib/ionic/ionitron.js new file mode 100644 index 0000000000..4ed7726c8c --- /dev/null +++ b/lib/ionic/ionitron.js @@ -0,0 +1,59 @@ +var colors = require('colors'), + path = require('path'), + fs = require('fs'); + +var ionitronStatements = [ + 'Hello human, what shall we build today?', + '*BEEP BEEP* ALL YOUR BASE ARE BELONG TO US *BEEP BEEP*', + 'Prepare to dominate your hybrid app. Engaging now.', + 'My sensors indicate you have an undying love for ionic, |\n | or is it just me?\t\t\t\t\t\t', + 'That\'s a nice looking app you have there. \t\t\t |\n | Definitely one of the better human made apps I\'ve seen.\t', + 'Oh, hi there. I was just not indexing your hard drive, |\n | definitely not doing that. ', + 'That would need bee\'s approval', + 'Fork you! Oh, I\'m sorry, wrong branch.' +]; + +var ionitronStatements_es = [ + '\u0021Hola humano! \u00BFQu\u00E9 vamos a construir hoy?', + '*BEEP BEEP* TU BASE NOS PERTENECE *BEEP BEEP*', + 'Prep\u00E1rate para dominar las aplicaciones h\u00EDbridas. |\n | Participa ahora.\t\t\t\t\t\t', + 'Mis sensores indican que sientes amor eterno hacia Ionic, |\n | \u00BFo es solo hacia m\u00ED?\t\t\t\t\t\t', + 'Es una bonita aplicaci\u00F3n esa que tienes. \t\t\t |\n | Eres el mejor desarrollador humano que he visto.\t\t', + 'Oh, hola. No estaba indexando tu disco duro, no hago eso.', + 'Es necesitaria la aprobaci\u00F3n de las abejas.', + 'Bif\u00Farcate! Oh, Lo siento, rama equivocada. ' +]; + +var ionictronAsciiFile = 'ionitron.txt'; + +module.exports.print = function print(lang) { + + var ionitronPath = path.join(__dirname, 'assets', ionictronAsciiFile); + var contents = fs.readFileSync(ionitronPath, 'utf8'); + var messageContent; + + switch (lang) { + case 'es': + //Replace the 'h' (antenna) by Ñ hahaha' + contents = contents.replace('h', '\u00D1'); + messageContent = [ionitronStatements_es[Math.floor(Math.random() * (ionitronStatements_es.length - 0) + 0)]].join(''); + break; + default: + messageContent = [ionitronStatements[Math.floor(Math.random() * (ionitronStatements.length - 0) + 0)]].join(''); + } + + var replaceString = '@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@'; + // console.log(messageContent.length, replaceString.length) + if (messageContent.length < replaceString.length) { + var diff = (replaceString.length) - messageContent.length; + var i = 0; + // console.log(diff, i) + while (i < diff) { + messageContent = messageContent + ' '; + i++; + } + } + contents = contents.replace(replaceString, messageContent) + console.log(contents.cyan); + return; +} diff --git a/lib/ionic/ions.js b/lib/ionic/ions.js new file mode 100644 index 0000000000..61aba3ba13 --- /dev/null +++ b/lib/ionic/ions.js @@ -0,0 +1,65 @@ +var argv = require('optimist').argv, + IonicProject = require('./project'), + Task = require('./task').Task, + IonicStats = require('./stats').IonicStats, + _ = require('underscore'); + +var ions = [ + { + ion: 'ionic-ion-header-shrink', + name: 'Header Shrink', + description: 'A shrinking header effect like Facebook\'s' + }, + { + ion: 'ionic-ion-drawer', + name: 'Android Drawer', + description: 'Android-style drawer menu' + }, + { + ion: 'ionic-ion-ios-buttons', + name: 'iOS Rounded Buttons', + description: 'iOS "Squircle" style icons' + }, + { + ion: 'ionic-ion-swipe-cards', + name: 'Swipeable Cards', + description: 'Swiping interaction as seen in Jelly' + }, + { + ion: 'ionic-ion-tinder-cards', + name: 'Tinder Cards', + description: 'Tinder style card swiping interaction' + } +] + +var IonicTask = function() {}; + +IonicTask.prototype = new Task(); + +IonicTask.prototype.run = function(ionic) { + console.log('Ionic Ions - ions power your app to be awesome.'); + console.log('A curated collection of useful addons, components, and ux interactions for extending ionic.') + console.log('\n') + + _.each(ions, function(ion) { + + var rightColumn = 20; + var dots = ''; + var indent = ''; + var x, arg; + + var startStr = ion.name; + + + while( (startStr + dots).length < rightColumn + 1) { + dots += '.'; + } + + // Header Shrink ... A shrinking header effect like Facebook\'s .. + var installStr = ['\'ionic add ', ion.ion, '\''].join('') + console.log(ion.name, dots, installStr) + console.log(ion.description, '\n') + }) +} + +exports.IonicTask = IonicTask; diff --git a/lib/ionic/lib.js b/lib/ionic/lib.js new file mode 100644 index 0000000000..30490f8626 --- /dev/null +++ b/lib/ionic/lib.js @@ -0,0 +1,340 @@ +var fs = require('fs'), + os = require('os'), + shelljs = require('shelljs/global'), + request = require('request'), + path = require('path'), + unzip = require('unzip'), + argv = require('optimist').argv, + prompt = require('prompt'), + colors = require('colors'), + exec = require('child_process').exec, + Q = require('q'), + Task = require('./task').Task, + IonicStats = require('./stats').IonicStats; + +var IonicTask = function() {}; + +IonicTask.prototype = new Task(); + +IonicTask.prototype.run = function(ionic) { + var self = this; + self.local = {}; + self.versionData = {}; + self.usesBower = false; + + if (!fs.existsSync( path.resolve('www') )) { + return ionic.fail('"www" directory cannot be found. Make sure the working directory is at the top level of an Ionic project.', 'lib'); + } + + this.loadVersionData(); + + if(argv._.length > 1 && (argv._[1].toLowerCase() == 'update' || argv._[1].toLowerCase() == 'up')) { + this.updateLibVersion(); + } else { + // just version info + this.printLibVersions(); + } + +}; + + +IonicTask.prototype.loadVersionData = function() { + this.versionFilePath = path.resolve('www/lib/ionic/version.json'); + if( !fs.existsSync(this.versionFilePath) ) { + this.versionFilePath = path.resolve('www/lib/ionic/bower.json'); + this.usesBower = fs.existsSync(this.versionFilePath); + } + + try { + this.local = require(this.versionFilePath); + } catch(e) { + console.log('Unable to load ionic lib version information'.bold.error); + } +}; + + +IonicTask.prototype.printLibVersions = function() { + var self = this; + + console.log('Local Ionic version: '.bold.green + this.local.version + ' (' + this.versionFilePath + ')'); + + this.getVersionData('latest').then(function(){ + console.log('Latest Ionic version: '.bold.green + self.versionData.version_number + ' (released ' + self.versionData.release_date + ')'); + + if(self.local.version != self.versionData.version_number) { + console.log(' * Local version is out of date'.yellow ); + } else { + console.log(' * Local version up to date'.green ); + } + }); +}; + + +IonicTask.prototype.getVersionData = function(version) { + var q = Q.defer(); + var self = this; + + var url = 'http://code.ionicframework.com'; + if(version == 'latest') { + url += '/latest.json'; + } else { + url += '/' + version + '/version.json'; + } + + var proxy = process.env.PROXY || process.env.http_proxy || null; + request({ url: url, proxy: proxy }, function(err, res, body) { + try { + if(err || !res || !body) { + console.log('Unable to receive version data'); + q.reject(); + return; + } + if(res.statusCode == 404) { + console.log( ('Invalid version: ' + version).bold.red ); + q.reject(); + return; + } + if(res.statusCode >= 400) { + console.log('Unable to load version data'); + q.reject(); + return; + } + self.versionData = JSON.parse(body); + self.versionData.version_number = self.versionData.version_number || self.versionData.version; + self.versionData.version_codename = self.versionData.version_codename || self.versionData.codename; + self.versionData.release_date = self.versionData.release_date || self.versionData.date; + + q.resolve(); + } catch(e) { + console.log('Error loading '.bold.error + version.bold + ' version information'.bold.error ); + q.reject(); + } + }); + + return q.promise; +}; + + +IonicTask.prototype.updateLibVersion = function() { + var self = this; + + if(self.usesBower) { + var bowerCmd = exec('bower update ionic'); + + bowerCmd.stdout.on('data', function (data) { + process.stdout.write(data); + }); + + bowerCmd.stderr.on('data', function (data) { + if(data) { + process.stderr.write(data.toString().error.bold); + } + }); + + return; + } + + prompt.message = ''; + prompt.delimiter = ''; + prompt.start(); + + var libPath = path.resolve('www/lib/ionic/'); + + console.log('Are you sure you want to replace '.green.bold + libPath.bold + ' with an updated version of Ionic?'.green.bold); + + var promptProperties = { + areYouSure: { + name: 'areYouSure', + description: '(yes/no):'.yellow.bold, + required: true + } + }; + + prompt.get({properties: promptProperties}, function (err, promptResult) { + if(err) { + return console.log(err); + } + + var areYouSure = promptResult.areYouSure.toLowerCase().trim(); + if(areYouSure == 'yes' || areYouSure == 'y') { + self.getLatest(); + } + }); +}; + + +IonicTask.prototype.getLatest = function() { + var self = this; + + var dirPath = path.resolve('www/lib/'); + if(!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath); + } + + dirPath = path.resolve('www/lib/ionic/'); + if(!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath); + } + + var version = argv.version || argv.v; + if(version === true || !version) { + version = 'latest'; + } + + this.getVersionData(version).then(function(){ + if(version == 'latest') { + console.log('Latest version: '.bold.green + self.versionData.version_number + ' (released ' + self.versionData.release_date + ')'); + } else { + console.log('Version: '.bold.green + self.versionData.version_number + ' (released ' + self.versionData.release_date + ')'); + } + self.downloadZip(self.versionData.version_number); + }); + +}; + + +IonicTask.prototype.downloadZip = function(version) { + var self = this; + + var archivePath = 'https://github.com/driftyco/ionic-bower/archive/v' + version + '.zip'; + + self.tmpExtractPath = path.resolve('www/lib/.ionic'); + self.tmpZipPath = path.join(self.tmpExtractPath, 'ionic.zip'); + + if( !fs.existsSync(self.tmpExtractPath) ) { + fs.mkdirSync(self.tmpExtractPath); + } + + console.log('Downloading: '.green.bold + archivePath); + + var proxy = process.env.PROXY || null; + request({ url: archivePath, rejectUnauthorized: false, encoding: null, proxy: proxy }, function(err, res, body) { + if(err) { + console.log(err); + return; + } + if(res.statusCode == 404 || res.statusCode == 406) { + console.log( ('Invalid version: ' + version).bold.red ); + return; + } + if(res.statusCode >= 400) { + console.log('Unable to download zip (' + res.statusCode + ')'); + return; + } + try { + fs.writeFileSync(self.tmpZipPath, body); + self.updateFiles(); + } catch(e) { + console.error(e); + } + }).on('response', function(res){ + + var ProgressBar = require('progress'); + var bar = new ProgressBar('[:bar] :percent :etas', { + complete: '=', + incomplete: ' ', + width: 30, + total: parseInt(res.headers['content-length'], 10) + }); + + res.on('data', function (chunk) { + try { + bar.tick(chunk.length); + } catch(e){} + }); + + }).on('error', function(err) { + try { + fs.unlink(self.tmpZipPath); + } catch(e) { + console.error(e); + } + console.error(err); + }); + +}; + + +IonicTask.prototype.updateFiles = function(version) { + var self = this; + var ionicLibDir = path.resolve('www/lib/ionic'); + + try { + var readStream = fs.createReadStream(self.tmpZipPath); + readStream.on('error', function(err) { + console.log( ('updateFiles readStream: ' + err).error ); + }); + + var writeStream = unzip.Extract({ path: self.tmpExtractPath }); + writeStream.on('close', function() { + try { + rm('-rf', self.tmpZipPath) + } catch(e){} + + try { + var dir = fs.readdirSync(self.tmpExtractPath); + var copyFrom; + for(var x=0; x new Date()) { + ionic.jar = jar; + callback(jar); + return; + } + } + } + } + + this.run(ionic, argv, callback); +}; + +LoginTask.IonicTask = IonicTask; diff --git a/lib/ionic/multibar.js b/lib/ionic/multibar.js new file mode 100644 index 0000000000..05de1e8f63 --- /dev/null +++ b/lib/ionic/multibar.js @@ -0,0 +1,103 @@ +var ProgressBar = require('progress'); + +function Multibar(stream) { + this.stream = stream || process.stderr; + this.cursor = 0; + this.bars = []; + this.terminates = 0; +} + +Multibar.prototype = { + newBar: function(schema, options) { + options.stream = this.stream; + var bar = new ProgressBar(schema, options); + this.bars.push(bar); + var index = this.bars.length - 1; + + // alloc line + this.move(index); + this.stream.write('\n'); + this.cursor ++; + + // replace original + var self = this; + bar.otick = bar.tick; + bar.oterminate = bar.terminate; + bar.tick = function(value, options) { + self.tick(index, value, options); + } + bar.terminate = function() { + self.terminates++; + if (self.terminates == self.bars.length) { + self.terminate(); + } + } + + return bar; + }, + + terminate: function() { + this.move(this.bars.length); + this.stream.clearLine(); + this.stream.cursorTo(0); + }, + + move: function(index) { + if (!this.stream.isTTY) return; + this.stream.moveCursor(0, index - this.cursor); + this.cursor = index; + }, + + tick: function(index, value, options) { + var bar = this.bars[index]; + if (bar) { + this.move(index); + bar.otick(value, options); + } + } +} + +// var mbars = new Multibar(); +// var bars = []; + +// function addBar() { +// bars.push(mbars.newBar(' :title [:bar] :percent', { +// complete: '=' +// , incomplete: ' ' +// , width: 30 +// , total: 100 +// })); +// return bars[bars.length - 1]; +// } + +// function forward() { +// for (var i = 0; i < bars.length; i++) { +// bars[i].tick(1, { title: 'forward: ' }); +// } +// if (bars[0].curr > 60) { +// addBar().tick(bars[0].curr); +// backward(); +// } else { +// setTimeout(forward, 20); +// } +// } + +// function backward() { +// for (var i = 0; i < bars.length; i++) { +// bars[i].tick(-1, { title: 'backward: ' }); +// } +// if (bars[0].curr == 0) { +// mbars.terminate(); +// console.log("End all"); +// } else { +// setTimeout(backward, 20); +// } +// } + +// for(var i = 0; i < 5; i++) { +// addBar(); +// } + +// forward(); + +module.exports = new Multibar(); diff --git a/lib/ionic/package.js b/lib/ionic/package.js new file mode 100644 index 0000000000..c4d244883d --- /dev/null +++ b/lib/ionic/package.js @@ -0,0 +1,326 @@ +var fs = require('fs'), + path = require('path'), + _ = require('underscore'), + Q = require('q'), + moment = require('moment'), + expandTilde = require('expand-tilde'), + ProgressBar = require('progress'), + IonicAppLib = require('ionic-app-lib'), + Utils = IonicAppLib.utils, + Login = IonicAppLib.login, + Package = IonicAppLib.package, + LoginTask = require('./login'), + Table = require('./table'), + Task = require('./task').Task; + +var Project = IonicAppLib.project; +var IonicTask = function() {}; + +IonicTask.prototype = new Task(); + +IonicTask.prototype.run = function(ionic, argv) { + + var cmd; + + if (argv._.length < 2) { + cmd = 'list'; + } else { + cmd = argv._[1]; + } + + var dir = null, + project = null, + appId = null; + + try { + dir = process.cwd(); + project = Project.load(dir); + appId = project.get('app_id'); + + if (!appId) { + throw new Error('Missing Ionic App ID.'); + } + } catch (ex) { + return Utils.fail(ex, 'package'); + } + + switch (cmd) { + case 'build': + return packageBuild(ionic, argv, dir, project, appId); + case 'list': + return packageList(ionic, argv, dir, project, appId); + case 'info': + return packageInfo(ionic, argv, dir, project, appId); + case 'download': + return packageDownload(ionic, argv, dir, project, appId); + } + + return Utils.fail("Unknown subcommand '" + cmd + "'.", 'package') + +}; + +function packageBuild(ionic, argv, dir, project, appId) { + + if (argv._.length < 3) { + return Utils.fail("Specify a valid platform (android or ios).", 'package'); + } + + var jar, + platform = argv._[2], + buildMode = argv.release ? 'release' : 'debug'; + + if (!_.contains(['android', 'ios'], platform)) { + return Utils.fail("Invalid platform '" + platform + "', please choose either 'android' or 'ios'.", 'package'); + } + + return Login.retrieveLogin() + .then(function(jar) { + if (!jar) { + console.log('No previous login existed. Attempting to log in now.'); + return LoginTask.login(argv); + } + return jar; + }) + .then(function(j) { + var options = {}; + jar = j; + + if (typeof argv.p !== 'undefined') { + argv.profile = argv.p; + } + + if (typeof argv.noresources !== 'undefined') { + options.noresources = true; + } + + if (platform === 'android') { + if (buildMode === 'debug') { + return Package.buildAndroidDebug(dir, jar, appId, options); + } else if (buildMode === 'release') { + if (typeof argv.profile === 'undefined') { + return Utils.fail("Must specify security profile for android release builds (--profile ).", 'package'); + } + + return Package.buildAndroidRelease(dir, jar, appId, argv.profile, options); + } + } else if (platform === 'ios') { + if (typeof argv.profile === 'undefined') { + return Utils.fail("Must specify security profile for ios builds (--profile ).", 'package'); + } + + return Package.buildIOS(dir, jar, appId, argv.profile, buildMode, options); + } + + return Q.reject('Unrecognized platform/build mode.'); + }) + .catch(function(ex) { + Utils.fail(ex, 'package'); + }); + +}; + +function formatStatus(status) { + switch (status) { + case 'SUCCESS': + return status.green; + case 'FAILED': + return status.red; + } + + return status; +} + +function formatDate(date) { + var d = new Date(date); + + return moment(d).format('MMM Do, YYYY H:mm:ss'); +} + +function packageList(ionic, argv, dir, project, appId) { + + var limit = 25; + + return Login.retrieveLogin() + .then(function(jar) { + if (!jar) { + console.log('No previous login existed. Attempting to log in now.'); + return LoginTask.login(argv); + } + return jar; + }) + .then(function(jar) { + return Package.listBuilds(appId, jar); + }) + .then(function(body) { + if (body.data.length === 0) { + console.log('You don\'t have any builds yet!'); + console.log('Type ' + 'ionic help package'.yellow + ' to learn how to use Ionic Package.'); + } else { + var count = 0, + headers = ['id', 'status', 'platform', 'mode'], + table, + screenWidth = process.stdout.getWindowSize()[0]; + + if (screenWidth > 100) { + headers.push('started'); + } + + if (screenWidth > 125) { + headers.push('finished'); + } + + table = new Table({ head: headers }); + + _.each(body.data.slice(0, limit), function(build) { + count++; + + var row = [ + build.id, + formatStatus(build.status), + build.platform, + build.mode + ]; + + if (screenWidth > 100) { + row.push(formatDate(build.created)); + } + + if (screenWidth > 125) { + row.push(build.completed ? formatDate(build.completed) : ''); + } + + table.push(row); + }); + + console.log(''); + console.log(table.toString()); + console.log('\nShowing', String(count).yellow, 'of your latest builds.'); + console.log(''); + } + }) + .catch(function(ex) { + Utils.fail(ex, 'package'); + }); + +} + +function packageInfo(ionic, argv, dir, project, appId) { + + var jar; + + return Login.retrieveLogin() + .then(function(jar) { + if (!jar) { + console.log('No previous login existed. Attempting to log in now.'); + return LoginTask.login(argv); + } + return jar; + }) + .then(function(j) { + jar = j; + + if (argv._.length < 3) { + return Package.listBuilds(appId, jar); + } + + return { data: [ { id: argv._[2] } ] }; + }) + .then(function(body) { + return Package.getBuild(appId, jar, body.data[0].id, { fields: ['output'] }); + }) + .then(function(body) { + var table = new Table(), + build = body.data; + + table.push( + ['id'.yellow, build.id], + ['status'.yellow, formatStatus(build.status)], + ['platform'.yellow, build.platform], + ['mode'.yellow, build.mode], + ['started'.yellow, formatDate(build.created)] + ); + + if (build.completed) { + table.push(['completed'.yellow, formatDate(build.completed)]); + } + + console.log(''); + console.log(table.toString()); + console.log(''); + + if (build.output) { + console.log('output'.yellow + ':'); + console.log(''); + console.log(build.output); + console.log(''); + } + + }) + .catch(function(ex) { + Utils.fail(ex, 'package'); + }); + +} + +function packageDownload(ionic, argv, dir, project, appId) { + + var jar, + bar, + downloadDirectory; + + if (typeof argv.d !== 'undefined') { + argv['destination'] = argv.d; + } + + if (typeof argv['destination'] === 'undefined') { + downloadDirectory = dir; + } else { + downloadDirectory = path.resolve(expandTilde(argv['destination'])); + } + + return Login.retrieveLogin() + .then(function(jar) { + if (!jar) { + console.log('No previous login existed. Attempting to log in now.'); + return LoginTask.login(argv); + } + return jar; + }) + .then(function(j) { + jar = j; + + if (argv._.length < 3) { + return Package.listBuilds(appId, jar); + } + + return { data: [ { id: argv._[2] } ] }; + }) + .then(function(body) { + return Package.downloadBuild(appId, jar, body.data[0].id, downloadDirectory); + }) + .then(function(filename) { + if (typeof bar !== 'undefined') { + bar.tick(bar.total); + } + + console.log('Wrote:', filename); + console.success('Done!'.green); + }, null, function(state) { + if (typeof bar === 'undefined') { + bar = new ProgressBar('Downloading... [:bar] :percent :etas', { + complete: '=', + incomplete: ' ', + width: 30, + total: state.total + }); + } + + bar.tick(state.received); + }) + .catch(function(ex) { + Utils.fail(ex); + }); + +} + +exports.IonicTask = IonicTask; diff --git a/lib/ionic/ports.js b/lib/ionic/ports.js new file mode 100644 index 0000000000..5fe7a1a7e4 --- /dev/null +++ b/lib/ionic/ports.js @@ -0,0 +1,209 @@ +/* + * portfinder.js: A simple tool to find an open port on the current machine. + * + * (C) 2011, Charlie Robbins + * + */ + +var fs = require('fs'), + net = require('net'), + path = require('path'), + async = require('async'); + // mkdirp = require('mkdirp').mkdirp; + +// +// ### @basePort {Number} +// The lowest port to begin any port search from +// +exports.basePort = 8000; + +// +// ### @basePath {string} +// Default path to begin any socket search from +// +exports.basePath = '/tmp/portfinder' + +exports.getPort = function (options, callback) { + if (!callback) { + callback = options; + options = {}; + } + + options.port = options.port || exports.basePort; + options.host = options.host || null; + options.server = options.server || net.createServer(function () { + // + // Create an empty listener for the port testing server. + // + }); + + function onListen () { + options.server.removeListener('error', onError); + // console.log('onListen') + options.server.close(); + callback(null, options.port) + } + + function onError (err) { + // console.log('Error:', err) + options.server.removeListener('listening', onListen); + + if (err.code !== 'EADDRINUSE' && err.code !== 'EACCES') { + return callback(err); + } + + exports.getPort({ + port: exports.nextPort(options.port), + host: options.host, + server: options.server + }, callback); + } + + options.server.once('error', onError); + options.server.once('listening', onListen); + options.server.listen(options.port, options.host); +}; + +exports.getPorts = function (count, options, callback) { + if (!callback) { + callback = options; + options = {}; + } + + // console.log('calling getPorts') + + var lastPort = null; + async.timesSeries(count, function(index, asyncCallback) { + if (lastPort) { + options.port = exports.nextPort(lastPort); + } + + exports.getPort(options, function (err, port) { + if (err) { + asyncCallback(err); + } else { + lastPort = port; + asyncCallback(null, port); + } + }); + }, callback); +}; + +// +// ### function getSocket (options, callback) +// #### @options {Object} Settings to use when finding the necessary port +// #### @callback {function} Continuation to respond to when complete. +// Responds with a unbound socket using the specified directory and base +// name on the current machine. +// +exports.getSocket = function (options, callback) { + if (!callback) { + callback = options; + options = {}; + } + + options.mod = options.mod || 0755; + options.path = options.path || exports.basePath + '.sock'; + + // + // Tests the specified socket + // + function testSocket () { + fs.stat(options.path, function (err) { + // + // If file we're checking doesn't exist (thus, stating it emits ENOENT), + // we should be OK with listening on this socket. + // + if (err) { + if (err.code == 'ENOENT') { + callback(null, options.path); + } + else { + callback(err); + } + } + else { + // + // This file exists, so it isn't possible to listen on it. Lets try + // next socket. + // + options.path = exports.nextSocket(options.path); + exports.getSocket(options, callback); + } + }); + } + + // + // Create the target `dir` then test connection + // against the socket. + // + function createAndTestSocket (dir) { + mkdirp(dir, options.mod, function (err) { + if (err) { + return callback(err); + } + + options.exists = true; + testSocket(); + }); + } + + // + // Check if the parent directory of the target + // socket path exists. If it does, test connection + // against the socket. Otherwise, create the directory + // then test connection. + // + function checkAndTestSocket () { + var dir = path.dirname(options.path); + + fs.stat(dir, function (err, stats) { + if (err || !stats.isDirectory()) { + return createAndTestSocket(dir); + } + + options.exists = true; + testSocket(); + }); + } + + // + // If it has been explicitly stated that the + // target `options.path` already exists, then + // simply test the socket. + // + return options.exists + ? testSocket() + : checkAndTestSocket(); +}; + +// +// ### function nextPort (port) +// #### @port {Number} Port to increment from. +// Gets the next port in sequence from the +// specified `port`. +// +exports.nextPort = function (port) { + return port + 1; +}; + +// +// ### function nextSocket (socketPath) +// #### @socketPath {string} Path to increment from +// Gets the next socket path in sequence from the +// specified `socketPath`. +// +exports.nextSocket = function (socketPath) { + var dir = path.dirname(socketPath), + name = path.basename(socketPath, '.sock'), + match = name.match(/^([a-zA-z]+)(\d*)$/i), + index = parseInt(match[2]), + base = match[1]; + + if (isNaN(index)) { + index = 0; + } + + index += 1; + return path.join(dir, base + index + '.sock'); +}; diff --git a/lib/ionic/project.js b/lib/ionic/project.js new file mode 100644 index 0000000000..d72872a481 --- /dev/null +++ b/lib/ionic/project.js @@ -0,0 +1,70 @@ +var fs = require('fs'), + path = require('path'); + +module.exports = { + PROJECT_FILE: 'ionic.project', + PROJECT_DEFAULT: { + name: '', + app_id: '' + }, + load: function() { + this.file = this.PROJECT_FILE; + + if(fs.existsSync(this.file)) { + try { + this.data = JSON.parse(fs.readFileSync(this.file)); + } catch(ex) { + throw new Error('There was an error loading your ionic.project file: ' + ex.message); + } + } else { + if(fs.existsSync('www')) { + var parts = path.resolve('./').split(path.sep); + var dirname = parts[parts.length-1]; + this.create(dirname); + this.save('./'); + } + } + return this; + }, + create: function(name) { + this.file = this.PROJECT_FILE; + this.data = this.PROJECT_DEFAULT; + if(name) { + this.set('name', name); + } + return this; + }, + save: function(targetPath) { + if(!this.data) { + console.trace(); + console.error('This should never happen!'); + } + try { + fs.writeFileSync((targetPath?targetPath + '/':'') + this.PROJECT_FILE, JSON.stringify(this.data, null, 2)); + } catch(e) { + console.error('Unable to save settings file:', e); + } + }, + get: function(k) { + if(!this.data) { + return null; + } + if(k) { + return this.data[k]; + } else { + return this.data; + } + }, + set: function(k, v) { + if(!this.data) { + this.data = this.PROJECT_DEFAULT; + } + this.data[k] = v; + }, + remove: function(k) { + if(!this.data) { + this.data = this.PROJECT_DEFAULT; + } + this.data[k] = ''; + } +}; diff --git a/lib/ionic/prompt.js b/lib/ionic/prompt.js new file mode 100644 index 0000000000..101e140935 --- /dev/null +++ b/lib/ionic/prompt.js @@ -0,0 +1,26 @@ +var prompt = require('prompt'), + IonicAppLib = require('ionic-app-lib'), + Utils = IonicAppLib.utils, + Q = require('q'); + +var Prompt = module.exports; + +Prompt.prompt = function(schema, argv) { + var q = Q.defer(); + prompt.override = argv; + prompt.message = ''; + prompt.delimiter = ''; + prompt.start(); + prompt.get(schema, function (err, result) { + if (err) { + if (err.message === 'canceled') { + return q.reject(false); + } else { + return q.reject(err); + } + } + + return q.resolve(result); + }); + return q.promise; +}; diff --git a/lib/ionic/push.js b/lib/ionic/push.js new file mode 100644 index 0000000000..8c23fc9e2d --- /dev/null +++ b/lib/ionic/push.js @@ -0,0 +1,468 @@ +var fs = require('fs'), + path = require('path'), + parseUrl = require('url').parse, + request = require('request'), + argv = require('optimist').argv, + prompt = require('prompt'), + FormData = require('form-data'), + IonicProject = require('./project'), + IonicTask = require('./task').IonicTask, + IonicStats = require('./stats').IonicStats, + Task = require('./task').Task, + IonicAppLib = require('ionic-app-lib'), + Login = IonicAppLib.login, + LoginTask = require('./login'); + // IonicLoginTask = require('./login').IonicTask; + +var IonicTask = function() {}; + +IonicTask.prototype = new Task(); + +IonicTask.prototype.run = function(ionic, argv) { + var self = this; + self.ionic = ionic; + self.inputValues = {}; + self.inputFiles = {}; + self.useCmdArgs = false; + + this.getCmdLineOptions(); + + Login.retrieveLogin() + .then(function(jar){ + if (!jar) { + console.log('No previous login existed. Attempting to log in now.'); + return LoginTask.login(argv); + } + return jar; + }) + .then(function(jar) { + self.jar = jar; + + if (argv._[1] && argv._[1].trim() == 'webhook_url') { + if (argv._[2]) { + self.set_webhook_url(self, argv._[2].trim()); + } else { + console.log('You need to specify a webhook url!'.bold.red); + } + } else if (argv['google-api-key']) { + self.set_google_api_key(self, argv['google-api-key'].trim()); + } else if (argv['send'] || argv.s) { + // Dev wants to send a push notification + self.send_push(self); + } else if (argv['ios-dev-cert'] || argv['ios-prod-cert'] || argv.d || argv.p) { + // Dev wants to upload an ios push cert + self.upload_cert(self); + } else if (argv['production-mode'] || argv.l) { + // Dev wants to switch between test/live mode + // this will look like --live-mode=[y/n] + // where y will put into live and n will put into dev + self.set_production_mode(self); + } + }) + .catch(function(ex) { + console.log('An error occurred', ex); + }) + +}; + +IonicTask.prototype.set_webhook_url = function(self, webhook_url) { + var project = IonicProject.load(); + + if (!project.get('app_id')) { + console.log("You need to upload your app first!".bold.red); + return false; + } + + var url = self.ionic.IONIC_DASH + self.ionic.IONIC_API + 'app/' + project.get('app_id') + '/push/webhook-url'; + var params = parseUrl(url); + + var form = new FormData(); + form.append('csrfmiddlewaretoken', self.getCsrfToken(self.jar)); + form.append('webhook_url', webhook_url); + + form.submit({ + protocol: params.protocol, + hostname: params.hostname, + port: params.port, + path: params.path, + headers: form.getHeaders({ + cookie: self.jar.map(function(c) { + return c.key + "=" + encodeURIComponent(c.value); + }).join("; ") + }) + }, + function (err, response) { + response.on("data", function(data) { + if (err) { + return self.ionic.fail('Error setting Webhook URL: ' + err); + } + + if (response.statusCode == '200') { + console.log("Webhook URL set to", webhook_url); + } else { + return self.ionic.fail('App not found'); + } + }) + } + ); + + return true; +}; + + +IonicTask.prototype.set_google_api_key = function(self, google_api_key) { + var project = IonicProject.load(); + + if (!project.get('app_id')) { + console.log("You need to upload your app first!".bold.red); + return false; + } + + var url = self.ionic.IONIC_DASH + self.ionic.IONIC_API + 'app/' + project.get('app_id') + '/push/google-api-key'; + var params = parseUrl(url); + + var form = new FormData(); + form.append('csrfmiddlewaretoken', self.getCsrfToken(self.jar)); + form.append('android_api_key', google_api_key); + + form.submit({ + protocol: params.protocol, + hostname: params.hostname, + port: params.port, + path: params.path, + headers: form.getHeaders({ + cookie: self.jar.map(function(c) { + return c.key + "=" + encodeURIComponent(c.value); + }).join("; ") + }) + }, + function (err, response) { + response.on("data", function(data) { + if (err) { + return self.ionic.fail('Error setting Google API Key: ' + err); + } + + if (response.statusCode == '200') { + console.log("Google API Key Saved".green); + } else { + return self.ionic.fail('App not found'); + } + }) + } + ); + + return true; +}; + + +IonicTask.prototype.send_push = function(self) { + var project = IonicProject.load(); + + if (!project.get('app_id')) { + console.log("You need to upload your app first!".bold.red); + return false; + } + + // So, we need the push API key to send notifications. + var promptProperties = {}; + + promptProperties['push-api-key'] = { + name: 'push-api-key', + description: 'Your private API key'.yellow.bold, + required: true, + isFile: false + }; + + promptProperties['device-token'] = { + name: 'device-token', + description: 'Device token'.yellow.bold, + required: true, + isFile: false + } + + promptProperties['alert'] = { + name: 'alert', + description: 'Notification alert message'.yellow.bold, + required: false, + isFile: false + }; + + promptProperties['badge'] = { + name: 'badge', + description: 'Badge count'.yellow.bold, + required: false, + isFile: false + } + + promptProperties['sound'] = { + name: 'sound', + description: 'Sound file name'.yellow.bold, + required: false, + isFile: false + } + + prompt.start(); + + prompt.get({properties: promptProperties}, function(err, promptResult) { + if (err) { + return self.ionic.fail('Error: ' + err); + } + + var api_key = promptResult['push-api-key']; + + var notification = { + platform: 'ios', + tokens: [promptResult['device-token']], + notification: { + alert: promptResult['alert'], + ios: { + badge: promptResult['badge'], + sound: promptResult['sound'] + } + } + } + + var options = { + url: 'https://push.ionic.io/api/v1/push', + method: 'POST', + json: true, + headers: { + 'X-Ionic-Application-Id': project.get('app_id'), + 'authorization': 'Basic ' + new Buffer(api_key + ':').toString('base64') + }, + body: notification + }; + + request(options, function(err, response, body) { + if (!err && response.statusCode == 202) { + console.log("Successfully queued push notification"); + } else { + console.log("Error queueing push notification", err); + } + }); + }); + +}; + +IonicTask.prototype.set_production_mode = function(self) { + var project = IonicProject.load(); + + if (!project.get('app_id')) { + console.log("You need to upload your app first!".bold.red); + return false; + } + + if (self.inputValues['production-mode'] === true) { + console.log("You need to specify a value [y/n] to set the production mode!".bold.red); + return false; + } + + self.inputValues['production-mode'] = self.inputValues['production-mode'].toLowerCase(); + + if (self.inputValues['production-mode'] != 'y' && self.inputValues['production-mode'] != 'n') { + console.log("You need to specify a value [y/n] to set the production mode!".bold.red); + return false; + } + + var url = self.ionic.IONIC_DASH + self.ionic.IONIC_API + 'app/' + project.get('app_id') + '/push/set-production-mode'; + var params = parseUrl(url); + + var form = new FormData(); + form.append('csrfmiddlewaretoken', self.getCsrfToken(self.jar)); + form.append('production_mode', self.inputValues['production-mode']); + + form.submit({ + protocol: params.protocol, + hostname: params.hostname, + port: params.port, + path: params.path, + headers: form.getHeaders({ + cookie: self.jar.map(function(c) { + return c.key + "=" + encodeURIComponent(c.value); + }).join("; ") + }) + }, + function (err, response) { + if (err) { + return self.ionic.fail('Error uploading certificate: ' + err); + } + + response.setEncoding('utf8'); + response.on("data", function(data) { + if (err) { + return self.ionic.fail('Error setting mode: ' + err); + } + + try { + var d = JSON.parse(data); + if (d.errors && d.errors.length) { + for (var j = 0; j < d.errors.length; j++) { + console.log((d.errors[j]).bold.error); + } + return self.ionic.fail('Unable to set mode'); + } + + if (self.inputValues['production-mode'] == 'y') { + console.log("Successfully set production mode"); + } else { + console.log("Successfully set development mode"); + } + + } catch (parseEx) { + return self.ionic.fail('Error response: ' + parseEx); + } + }); + } + ); +}; + +IonicTask.prototype.upload_cert = function(self) { + var project = IonicProject.load(); + + if (!project.get('app_id')) { + console.log("You need to upload your app first!".bold.red); + return false; + } + + + var promptProperties = {}; + var prodCert = '0'; + + if (argv['ios-dev-cert']) { + promptProperties['ios-push-cert'] = { + name: 'ios-push-cert', + description: 'iOS Dev Push Certificate File (.p12)'.yellow.bold, + required: true, + conform: fileExists, + isFile: true + }; + } else if (argv['ios-prod-cert']) { + promptProperties['ios-push-cert'] = { + name: 'ios-push-cert', + description: 'iOS Prod Push Certificate File (.p12)'.yellow.bold, + required: true, + conform: fileExists, + isFile: true + }; + + prodCert = '1'; + } + + prompt.start(); + + prompt.get({properties: promptProperties}, function(err, promptResult) { + if (err) { + return self.ionic.fail('Error: ' + err); + } + + var url = self.ionic.IONIC_DASH + self.ionic.IONIC_API + 'app/' + project.get('app_id') + '/push/upload-push-cert'; + var params = parseUrl(url); + + var form = new FormData(); + form.append('csrfmiddlewaretoken', self.getCsrfToken(self.jar)); + + var inputFile; + + try { + inputFile = promptResult['ios-push-cert'].replace(/\\ /g, ' ').trim(); + form.append('ios_push_cert', fs.createReadStream( resolvePath(inputFile) ) ); + } catch(e) { + return self.ionic.fail("Error loading " + resolvePath(inputFile)); + } + + // Set the flag for if this is a production or dev cert + form.append('prod_cert', prodCert); + + form.submit({ + protocol: params.protocol, + hostname: params.hostname, + port: params.port, + path: params.path, + headers: form.getHeaders({ + cookie: self.jar.map(function(c) { + return c.key + "=" + encodeURIComponent(c.value); + }).join("; ") + }) + }, + function (err, response) { + if (err) { + return self.ionic.fail('Error uploading certificate: ' + err); + } + + response.setEncoding('utf8'); + response.on("data", function(data) { + if (err) { + return self.ionic.fail('Error uploading certificate: ' + err); + } + + try { + var d = JSON.parse(data); + if (d.errors && d.errors.length) { + for (var j = 0; j < d.errors.length; j++) { + console.log((d.errors[j]).bold.error); + } + return self.ionic.fail('Unable to upload certificate'); + } + + console.log("Successfully uploaded certificate"); + + } catch (parseEx) { + return self.ionic.fail('Error response: ' + parseEx); + } + }); + } + ); + }); +} + +IonicTask.prototype.getCmdLineOptions = function() { + var self = this; + + function getCmdArgValue(propertyName, shortName) { + var value = argv[propertyName] || argv[shortName]; + if(value) { + self.inputValues[propertyName] = value; + self.useCmdArgs = true; + } + } + + function getCmdArgFile(propertyName, shortName) { + var value = argv[propertyName] || argv[shortName]; + if(value) { + if(!fileExists(value)) { + return self.ionic.fail("Unable to find file: " + argv[propertyName]); + } + self.inputFiles[propertyName] = value; + self.useCmdArgs = true; + } + } + + getCmdArgValue('production-mode', 'l'); + + getCmdArgFile('ios-push-dev-cert', 'd'); + getCmdArgFile('ios-push-prod-cert', 'p'); +}; + +IonicTask.prototype.getCsrfToken = function(jar) { + for (var i = 0; i < jar.length; i++) { + if (jar[i].key == 'csrftoken') { + return jar[i].value; + } + } + return ''; +} + +function fileExists(filePath) { + // check if a file exists with a relative path or absolute path + filePath = filePath.replace(/\\ /g, ' ').trim(); + return fs.existsSync(resolvePath(filePath)); +} + +function resolvePath (p) { + if (p.substr(0, 1) === '~') { + p = process.env.HOME + p.substr(1); + } + return path.resolve(p); +} + +exports.IonicTask = IonicTask; diff --git a/lib/ionic/resources.js b/lib/ionic/resources.js new file mode 100644 index 0000000000..7d80d8a254 --- /dev/null +++ b/lib/ionic/resources.js @@ -0,0 +1,39 @@ +var argv = require('optimist').argv, + Task = require('./task').Task, + IonicAppLib = require('ionic-app-lib'), + IonicResources = IonicAppLib.resources, + IonicStats = require('./stats').IonicStats, + Utils = IonicAppLib.utils; + +var Project = IonicAppLib.project; +var IonicTask = function() {}; + +IonicTask.prototype = new Task(); + +IonicTask.prototype.run = function() { + var dir = null, + project = null; + + try { + dir = process.cwd(); + project = Project.load(dir); + } catch (ex) { + return Utils.fail(ex, 'resources'); + } + + var options = argv; + options.platforms = argv._; + + IonicResources.generate(dir, options) + .catch(function(ex) { + if (ex instanceof Error) { + Utils.fail(ex, 'resources'); + } + }); + + IonicStats.t(); +}; + +module.exports = { + IonicTask: IonicTask +} diff --git a/lib/ionic/resources/docs.json b/lib/ionic/resources/docs.json new file mode 100644 index 0000000000..23b97c4ea3 --- /dev/null +++ b/lib/ionic/resources/docs.json @@ -0,0 +1,141 @@ +{ + "versions": [ + "1.0.0-beta.10", + "1.0.0-beta.11", + "1.0.0-beta.12", + "1.0.0-beta.13", + "1.0.0-beta.14" + ], + "api": { + "controller": { + "id": "controller", + "docs": [ + "ionicModal", + "ionicPopover" + ] + }, + "directive": { + "id": "directive", + "docs": [ + "collectionRepeat", + "exposeAsideWhen", + "ionCheckbox", + "ionContent", + "ionDeleteButton", + "ionFooterBar", + "ionHeaderBar", + "ionInfiniteScroll", + "ionItem", + "ionList", + "ionNavBackButton", + "ionNavBar", + "ionNavButtons", + "ionNavTitle", + "ionNavView", + "ionOptionButton", + "ionPane", + "ionRadio", + "ionRefresher", + "ionReorderButton", + "ionScroll", + "ionSideMenu", + "ionSideMenuContent", + "ionSideMenus", + "ionSlide", + "ionSlideBox", + "ionSlidePager", + "ionTab", + "ionTabs", + "ionToggle", + "ionView", + "keyboardAttach", + "menuClose", + "menuToggle", + "navClear", + "navDirection", + "navTransition", + "onDrag", + "onDragDown", + "onDragLeft", + "onDragRight", + "onDragUp", + "onHold", + "onRelease", + "onSwipe", + "onSwipeDown", + "onSwipeLeft", + "onSwipeRight", + "onSwipeUp", + "onTap", + "onTouch" + ] + }, + "ionic": { + "id": "ionic", + "docs": [ + "controller", + "directive", + "service", + "utility" + ] + }, + "module": { + "id": "module", + "docs": [ + "ionic" + ] + }, + "object": { + "id": "object", + "docs": [ + "$ionicLoadingConfig" + ] + }, + "page": { + "id": "page", + "docs": [ + "keyboard", + "tap" + ] + }, + "provider": { + "id": "provider", + "docs": [ + "$ionicConfigProvider" + ] + }, + "service": { + "id": "service", + "docs": [ + "$ionicActionSheet", + "$ionicAnimation", + "$ionicBackdrop", + "$ionicBody", + "$ionicConfig", + "$ionicGesture", + "$ionicHistory", + "$ionicListDelegate", + "$ionicLoading", + "$ionicModal", + "$ionicNavBarDelegate", + "$ionicPlatform", + "$ionicPopover", + "$ionicPopup", + "$ionicPosition", + "$ionicScrollDelegate", + "$ionicSideMenuDelegate", + "$ionicSlideBoxDelegate", + "$ionicTabsDelegate", + "$ionicTemplateCache" + ] + }, + "utility": { + "id": "utility", + "docs": [ + "ionic.DomUtil", + "ionic.EventController", + "ionic.Platform" + ] + } + } +} diff --git a/lib/ionic/security.js b/lib/ionic/security.js new file mode 100644 index 0000000000..99cbe483ed --- /dev/null +++ b/lib/ionic/security.js @@ -0,0 +1,282 @@ +var fs = require('fs'), + path = require('path'), + Q = require('q'), + expandTilde = require('expand-tilde'), + _ = require('underscore'), + moment = require('moment'), + IonicAppLib = require('ionic-app-lib'), + Utils = IonicAppLib.utils, + Login = IonicAppLib.login, + Security = IonicAppLib.security, + LoginTask = require('./login'), + Prompt = require('./prompt'), + Table = require('./table'), + Task = require('./task').Task; + +var Project = IonicAppLib.project; +var IonicTask = function() {}; + +IonicTask.prototype = new Task(); + +IonicTask.prototype.run = function(ionic, argv) { + + var cmd, + subcmd; + + if (argv._.length < 2) { + cmd = 'profiles'; + } else { + cmd = argv._[1]; + } + + var dir = null, + project = null, + appId = null; + + try { + dir = process.cwd(); + project = Project.load(dir); + appId = project.get('app_id'); + + if (!appId) { + throw new Error('Missing Ionic App ID.'); + } + } catch (ex) { + return Utils.fail(ex, 'security'); + } + + switch (cmd) { + case 'profiles': + if (argv._.length < 3) { + subcmd = 'list'; + } else { + subcmd = argv._[2]; + } + + switch (subcmd) { + case 'add': + return addSecurityProfile(ionic, argv, dir, appId); + case 'list': + return listSecurityProfiles(ionic, argv, dir, appId); + default: + return Utils.fail("Unknown subcommand 'profiles " + subcmd + "'.", 'security') + } + case 'credentials': + return addSecurityCredentials(ionic, argv, dir, appId); + } + + return Utils.fail("Unknown subcommand '" + cmd + "'.", 'security') + +}; + +function addSecurityProfile(ionic, argv, dir, appId) { + + if (argv._.length < 4) { + return Utils.fail("Specify a name for this Security Profile. Example: 'ionic security profile add \"My New Profile\"'.", 'security'); + } + + var jar, + name = argv._[3]; + + return Login.retrieveLogin() + .then(function(jar) { + if (!jar) { + console.log('No previous login existed. Attempting to log in now.'); + return LoginTask.login(argv); + } + return jar; + }) + .then(function(j) { + jar = j; + + return Security.addProfile(appId, jar, name); + }) + .then(function(body) { + console.success('Added "' + name + '".'); + }, function(err) { + if (typeof err === 'object') { + if (err.type == 'ProfileExists') { + console.error(err.message); + } else { + return Q.reject(new Error(err.message)); + } + } + }) + .catch(function(ex) { + Utils.fail(ex, 'package'); + }); + +} + +function listSecurityProfiles(ionic, argv, dir, appId) { + + var jar; + + return Login.retrieveLogin() + .then(function(jar) { + if (!jar) { + console.log('No previous login existed. Attempting to log in now.'); + return LoginTask.login(argv); + } + return jar; + }) + .then(function(j) { + jar = j; + + return Security.listProfiles(appId, jar); + }) + .then(function(body) { + if (body.data.length === 0) { + console.log('You don\'t have any Security Profiles yet!'); + console.log('Type ' + 'ionic help security'.yellow + ' to learn how to use Security Profiles.'); + } else { + var table = new Table({ head: ['name', 'tag', 'android', 'ios'] }); + + _.each(body.data, function(profile) { + table.push([ + profile.name, + profile.tag, + typeof profile.credentials.android === 'undefined' ? '✗'.red.bold : '✓'.green.bold, + typeof profile.credentials.ios === 'undefined' ? '✗'.red.bold : '✓'.green.bold + ]); + }); + + console.log(''); + console.log(table.toString()); + console.log(''); + } + }) + .catch(function(ex) { + Utils.fail(ex, 'security'); + }); +} + +function addSecurityCredentials(ionic, argv, dir, appId) { + + if (argv._.length < 3) { + return Utils.fail("Specify a valid platform (android or ios).", 'security'); + } + + if (typeof argv.profile === 'undefined') { + return Utils.fail("Specify the Security Profile on which these credentials are saved (--profile ).", 'security'); + } + + var jar, + platform = argv._[2], + profile = argv.profile; + + if (!_.contains(['android', 'ios'], platform)) { + return Utils.fail("Invalid platform '" + platform + "', please choose either 'android' or 'ios'.", 'security'); + } + + return Login.retrieveLogin() + .then(function(jar) { + if (!jar) { + console.log('No previous login existed. Attempting to log in now.'); + return LoginTask.login(argv); + } + return jar; + }) + .then(function(j) { + var q = Q.defer(); + + jar = j; + + if (platform === 'android') { + if (typeof argv.s !== 'undefined') { + argv['keystore'] = argv.s; + } + + if (typeof argv.p !== 'undefined') { + argv['keystore-password'] = argv.p; + } + + if (typeof argv.k !== 'undefined') { + argv['key-alias'] = argv.k; + } + + if (typeof argv.w !== 'undefined') { + argv['key-password'] = argv.w; + } + + Prompt.prompt([ + { name: 'keystore', description: 'Keystore File:'.prompt, required: true }, + { name: 'keystore-password', description: 'Keystore Password:'.prompt, hidden: true, required: true }, + { name: 'key-alias', description: 'Key Alias:'.prompt, required: true }, + { name: 'key-password', description: 'Key Password:'.prompt, hidden: true, required: true } + ], argv) + .then(function(result) { + q.resolve(result); + }) + .catch(function(ex) { + q.reject(ex); + }); + } else if (platform === 'ios') { + if (typeof argv.c !== 'undefined') { + argv['cert'] = argv.c; + } + + if (typeof argv.p !== 'undefined') { + argv['cert-password'] = argv.p; + } + + if (typeof argv.r !== 'undefined') { + argv['provisioning-profile'] = argv.r; + } + + Prompt.prompt([ + { name: 'cert', description: 'Certificate File:'.prompt, required: true }, + { name: 'cert-password', description: 'Certificate Password:'.prompt, hidden: true, required: true }, + { name: 'provisioning-profile', description: 'Provisioning Profile:'.prompt, required: true } + ], argv) + .then(function(result) { + q.resolve(result); + }) + .catch(function(ex) { + q.reject(ex); + }); + } else { + q.reject('Unrecognized platform: ' + platform); + } + + return q.promise; + }) + .then(function(result) { + if (platform === 'android') { + var keystoreFilePath = path.resolve(expandTilde(result['keystore'])), + keystorePassword = result['keystore-password'], + keyAlias = result['key-alias'], + keyPassword = result['key-password'], + keystoreFileStream = fs.createReadStream(keystoreFilePath); + + return Security.addAndroidCredentials(appId, jar, profile, keystoreFileStream, keystorePassword, keyAlias, keyPassword) + } else if (platform === 'ios') { + var certificateFilePath = path.resolve(expandTilde(result['cert'])), + certificatePassword = result['cert-password'], + provisioningProfileFilePath = path.resolve(expandTilde(result['provisioning-profile'])), + certificateFileStream = fs.createReadStream(certificateFilePath), + provisioningProfileFileStream = fs.createReadStream(provisioningProfileFilePath); + + return Security.addIOSCredentials(appId, jar, profile, certificateFileStream, certificatePassword, provisioningProfileFileStream) + } + + return Q.reject('Unrecognized platform.'); + }) + .then(function(body) { + console.success('Added ' + platform + ' credentials to your Security Profile.'); + }, function(err) { + if (typeof err === 'object') { + if (err.type == 'CredentialsExist') { + console.error(err.message); + } else { + return Q.reject(new Error(err.message)); + } + } + }) + .catch(function(ex) { + Utils.fail(ex, 'package'); + }); + +}; + +exports.IonicTask = IonicTask; diff --git a/lib/ionic/serve.js b/lib/ionic/serve.js new file mode 100644 index 0000000000..41f9e14549 --- /dev/null +++ b/lib/ionic/serve.js @@ -0,0 +1,91 @@ +var IonicAppLib = require('ionic-app-lib'), + IonicStats = require('./stats').IonicStats, + IonicProject = require('./project'), + events = IonicAppLib.events, + Q = require('q'), + Serve = IonicAppLib.serve, + Task = require('./task').Task, + Utils = require('./utils'); + +var DEFAULT_HTTP_PORT = 8100; +var DEFAULT_LIVE_RELOAD_PORT = 35729; +var IONIC_LAB_URL = '/ionic-lab'; + +var IonicTask = function() {}; + +IonicTask.prototype = new Task(); + +IonicTask.prototype.run = function(ionic, argv) { + var self = this; + this.ionic = ionic; + this.port = argv._[1]; + this.liveReloadPort = argv._[2]; + + if (argv._[0] == 'address') { + // reset any address configs + var ionicConfig = require('./config').load(); + ionicConfig.set('ionicServeAddress', null); + ionicConfig.set('platformServeAddress', null); + return Serve.getAddress({isAddressCmd: true}); + } + + var project = null; + + try { + project = IonicProject.load(process.cwd()); + } catch (ex) { + console.log('Error occured', ex); + return Utils.fail(ex.message); + } + + var options = Serve.loadSettings(argv, project); + options.appDirectory = process.cwd();//called from cli - ionic serve - cwd is this + + options.nogulp = argv.nogulp; + + var promise; + + try { + if (argv.all || argv.a) { + options.address = '0.0.0.0'; + promise = Q(); + } else if (argv.address) { + options.address = argv.address; + promise = Q(); + } else { + promise = Serve.getAddress(options); + } + + + return promise + .then(function() { + // console.log('options.address', options.address); + return Serve.checkPorts(true, options.port, options.address, options); + }) + .then(function() { + if(options.runLivereload) { + return Serve.checkPorts(false, options.liveReloadPort, options.address, options); + } + }) + .then(function() { + return Serve.start(options); + }) + .then(function() { + return Serve.showFinishedServeMessage(options); + }) + .then(function() { + // return Serve.listenToServeLogs(options); + events.on('serverlog', console.log); + events.on('consolelog', console.log); + }) + .catch(function(error) { + console.log('There was an error serving your Ionic application:', error); + throw error; + }); + } catch (ex) { + Utils.fail('Error with serve- ' + ex); + console.log(ex.stack); + } +}; + +exports.IonicTask = IonicTask; diff --git a/lib/ionic/service.js b/lib/ionic/service.js new file mode 100644 index 0000000000..22854a5de4 --- /dev/null +++ b/lib/ionic/service.js @@ -0,0 +1,207 @@ +require('shelljs/global') + +var Task = require('./task').Task, + IonicCordova = require('./cordova').IonicTask, + IonicStats = require('./stats').IonicStats, + argv = require('optimist').argv, + fs = require('fs'), + path = require('path'), + _ = require('underscore'), + bower = require('./bower'); + +var IonicTask = function() {}; + +IonicTask.prototype = new Task(); + +IonicTask.prototype.readIonicProjectJson = function readIonicProjectJson() { + var ionicProjectFile = path.join(process.cwd(), 'ionic.project'); + var ionicProjectJson = null; + try { + var content = fs.readFileSync(ionicProjectFile, 'utf8'); + ionicProjectJson = JSON.parse(content); + } catch (ex) { } + + return ionicProjectJson; +} + +IonicTask.prototype.addServiceToIonicJson = function addServiceToIonicJson(serviceName) { + var ionicProjectFile = path.join(process.cwd(), 'ionic.project'); + + try { + var ionicProjectJson = this.readIonicProjectJson() || {}; + + if (!ionicProjectJson.services) { + ionicProjectJson.services = []; + } + + var existingProject = _.findWhere(ionicProjectJson.services, {name: serviceName}); + if (typeof existingProject != 'undefined') { + return; + } + + ionicProjectJson.services.push({name: serviceName}); + fs.writeFileSync(ionicProjectFile, JSON.stringify(ionicProjectJson, null, 2), 'utf8'); + + } catch (ex) { + this.ionic.fail('Failed to update the ionic.project settings for the service', 'service'); + } +} + +IonicTask.prototype.installBowerComponent = function installBowerComponent(serviceName) { + // console.log('We are here now.'); + var bowerInstallCommand = 'bower link ionic-service-' + serviceName; + // var bowerInstallCommand = 'bower install ionic-service-' + serviceName; + + var result = exec(bowerInstallCommand); + + if (result.code != 0) { + //Error happened, report it. + var errorMessage = 'Failed to find the service "'.error.bold + serviceName.verbose + '"'.error.bold + '.\nAre you sure it exists?'.error.bold; + this.ionic.fail(errorMessage, 'service'); + } else { + this.addServiceToIonicJson(serviceName); + } +} + +IonicTask.prototype.uninstallBowerComponent = function uninstallBowerComponent(serviceName) { + var bowerUninstallCommand = 'bower unlink ionic-service-' + serviceName; + + var result = exec(bowerUninstallCommand); + + if (result.code != 0) { + var errorMessage = 'Failed to find the service "'.error.bold + serviceName.verbose + '"'.error.bold + '.\nAre you sure it exists?'.error.bold; + this.ionic.fail(errorMessage, 'service') + } else { + // console.log('Uninstalled Ionic service', serviceName); + } +} + +IonicTask.prototype.getBowerComponentsLocation = function getBowerComponentsLocation() { + var bowerRcFileLocation = path.join(process.cwd(), '.bowerrc'); + // console.log('location bowerrc: ' , bowerRcFileLocation) + var bowerRc = null; + + try { + var content = fs.readFileSync(bowerRcFileLocation, 'utf8'); + bowerRc = JSON.parse(content); + // console.log('bowerRc contents: ', bowerRc) + } catch (ex) { } + + var directory = 'www/lib'; //Default directory + + if (bowerRc && bowerRc.directory) { + directory = bowerRc.directory; + } + + return directory; +} + +IonicTask.prototype.getBowerJson = function getBowerJson(directory, serviceName) { + var bowerJsonLocation = path.join(process.cwd(), directory, 'ionic-service-' + serviceName, 'ionic-plugins.json'); + var packageBowerJson = require(bowerJsonLocation); + return packageBowerJson; +} + +IonicTask.prototype.installBowerPlugins = function installBowerPlugins(directory, serviceName) { + // var bowerJsonLocation = process.cwd() + '/' + directory + '/ionic-service-' + serviceName + '/bower.json'; + // var packageBowerJson = require(bowerJsonLocation); + var packageBowerJson = this.getBowerJson(directory, serviceName); + + // console.log('bowerjson = ', packageBowerJson.plugins); + // console.log('ionic - ', IonicCordova); + _.each(packageBowerJson.plugins, function(plugin) { + // var ic = new IonicCordova(); + // ic.run('ionic plugin add ' + plugin); + console.log('Installing cordova plugin - ' + plugin.name + ' (' + plugin.id + ')'); + var installPluginCmd = 'ionic plugin add ' + plugin.uri; + console.log(installPluginCmd); + var pluginInstallResult = exec(installPluginCmd); + + if (pluginInstallResult.code != 0) { + var errorMessage = 'Failed to find the plugin "'.error.bold + plugin.name.verbose + '"'.error.bold + '.'.error.bold; + this.ionic.fail(errorMessage, 'service'); + } + // console.log(pluginInstallResult); + }) +} + +IonicTask.prototype.uninstallBowerPlugins = function uninstallBowerPlugins(bowerJson) { + _.each(bowerJson.plugins, function(plugin) { + console.log('Uninstalling cordova plugin - ' + plugin.name); + var uninstallPluginCmd = 'ionic plugin rm ' + plugin.id; + console.log(uninstallPluginCmd); + var pluginRemoveResult = exec(uninstallPluginCmd) + + if (pluginRemoveResult.code != 0) { + var errorMessage = 'Failed to find the plugin to remove "'.error.bold + plugin.name.verbose + '"'.error.bold + '.'.error.bold; + this.ionic.fail(errorMessage, 'service'); + } + }) +} + +IonicTask.prototype.addService = function addService(serviceName) { + this.installBowerComponent(serviceName); + + var directory = this.getBowerComponentsLocation(); + // console.log('Directory we are searching for bower.json - ', directory); + + console.log('Checking for any plugins required by service package'); + + this.installBowerPlugins(directory, serviceName); + console.log('ionic service add completed'); +} + +IonicTask.prototype.removeService = function removeService(serviceName) { + var directory = this.getBowerComponentsLocation(); + var packageBowerJson = this.getBowerJson(directory, serviceName); + + this.uninstallBowerComponent(serviceName); + + this.uninstallBowerPlugins(packageBowerJson); + +} + +IonicTask.prototype.listServices = function listServices() { + // var directory = this.getBowerComponentsLocation(); + var bowerJson = require(process.cwd() + '/bower.json'); + // console.log(bowerJson); + +} + +// Need to look at bower.json of package just installed and look for any cordova plugins required +// Check the directory in the projects `.bowerrc` file +// Then go to /path/to/bower/components//ionic-plugins.json - 'plugins' +// For each plugins - call 'ionic add plugin ' +IonicTask.prototype.run = function run(ionic, argv, callback) { + this.ionic = ionic; + + IonicStats.t(); + + + if (!bower.IonicBower.checkForBower()) { + this.ionic.fail(bower.IonicBower.installMessage, 'service'); + return; + } + + var action = argv._[1] + var serviceName = argv._[2]; + + try { + switch (action) { + case 'add': + this.addService(serviceName); + break; + case 'remove': + this.removeService(serviceName); + break; + case 'list': + this.listServices(); + break; + } + } catch (error) { + var errorMessage = error.message ? error.message : error; + this.ionic.fail(errorMessage, 'service') + } +} + +exports.IonicTask = IonicTask; diff --git a/lib/ionic/setup.js b/lib/ionic/setup.js new file mode 100644 index 0000000000..39cd93d3f1 --- /dev/null +++ b/lib/ionic/setup.js @@ -0,0 +1,73 @@ +var Task = require('./task').Task, + IonicStats = require('./stats').IonicStats, + fs = require('fs'), + path = require('path'), + argv = require('optimist').argv, + exec = require('child_process').exec, + Q = require('q'), + IonicProject = require('./project'), + IonicAppLib = require('ionic-app-lib'), + Setup = IonicAppLib.setup, + Utils = IonicAppLib.utils, + colors = require('colors'); + +var IonicTask = function() {}; + +IonicTask.prototype = new Task(); + +IonicTask.prototype.run = function(ionic) { + if( argv._.length < 2 ) { + return ionic.fail('Missing setup task command.', 'setup'); + } + + var self = this; + var tasks = argv._.slice(1); + var promises = []; + + var promise = Q(); + + for (var x = 0; x < tasks.length; x++) { + try { + (function() { + //create closure for task + var task = self.runSetupTask(tasks[x]); + // console.log('Task: ', task); + promises.push(task); + })(); + } catch (ex) { + + } + } + + promises.forEach(function(task) { + // console.log('Task foreach:', task); + promise = promise.then(function(){ return task(process.cwd()); }); + }); + + promise + .fin(function() { + console.log('Ionic setup complete'.green.bold); + }) + .catch(function(error) { + console.log('Error from setup - ' + error); + }) + + IonicStats.t(); +}; + +IonicTask.prototype.runSetupTask = function runSetupTask(setupTask) { + switch (setupTask) { + case 'sass': + return Setup.sassSetup; + break; + case 'facebook': + return Setup.setupFacebook; + break; + default: + var errorMessage = 'Invalid setup task command: ' + setupTask; + console.log(errorMessage); + return Q(errorMessage); + } +}; + +exports.IonicTask = IonicTask; diff --git a/lib/ionic/share.js b/lib/ionic/share.js new file mode 100644 index 0000000000..8e00f146a3 --- /dev/null +++ b/lib/ionic/share.js @@ -0,0 +1,68 @@ +var path = require('path'), + parseUrl = require('url').parse, + shelljs = require('shelljs/global'), + argv = require('optimist').boolean(['no-cordova', 'sass', 'list']).argv, + Q = require('q'), + open = require('open'), + xml2js = require('xml2js'), + FormData = require('form-data'), + IonicProject = require('./project'), + IonicStore = require('./store').IonicStore, + Task = require('./task').Task, + IonicStats = require('./stats').IonicStats, + LoginTask = require('./login'), + IonicAppLib = require('ionic-app-lib'), + Share = IonicAppLib.share, + Login = IonicAppLib.login, + Utils = IonicAppLib.utils; + +var IonicTask = function() {}; + +IonicTask.prototype = new Task(); + +IonicTask.prototype.run = function(ionic, argv) { + var project; + + if(argv._.length < 2) { + return ionic.fail('Invalid command', 'share'); + } + + try { + project = IonicProject.load(); + } catch (ex) { + ionic.fail(ex.message); + return + } + + if (project.get('app_id') == '') { + return ionic.fail('You must first upload the app to share it'); + } + + var email = argv._[1]; + + if(email.indexOf('@') < 0) { + return ionic.fail('Invalid email address', 'share'); + } + + console.log(['Sharing app ', project.get('name'), ' (', project.get('app_id'), ') with ', email, '.'].join('').green); + + IonicStats.t(); + + return Login.retrieveLogin() + .then(function(jar){ + if (!jar) { + console.log('No previous login existed. Attempting to log in now.'); + return LoginTask.login(argv); + } + return jar; + }) + .then(function(jar) { + return Share.shareApp(process.cwd(), jar, email); + }) + .catch(function(ex) { + // console.log('Error', ex, ex.stack); + return Utils.fail(ex); + }); +}; + +exports.IonicTask = IonicTask; diff --git a/lib/ionic/start.js b/lib/ionic/start.js new file mode 100644 index 0000000000..5427d5ce41 --- /dev/null +++ b/lib/ionic/start.js @@ -0,0 +1,73 @@ +var fs = require('fs'), + shelljs = require('shelljs/global'), + // argv = require('optimist').boolean(['no-cordova', 'sass', 'list', 'w']).argv, + prompt = require('prompt'), + colors = require('colors'), + Q = require('q'), + open = require('open'), + IonicTemplates = require('./templates').IonicTask, + IonicStore = require('./store').IonicStore, + Task = require('./task').Task, + IonicStats = require('./stats').IonicStats, + IonicAppLib = require('ionic-app-lib'), + Start = IonicAppLib.start, + Utils = IonicAppLib.utils; + +var IonicTask = function() {}; +IonicTask.prototype = new Task(); + +IonicTask.prototype.run = function run(ionic, argv) { + + if (argv.list || argv.l) { + new IonicTemplates().run(ionic); + return; + } + + if (argv._.length < 2) { + return ionic.fail('Invalid command', 'start'); + } + + if (argv._[1] == '.') { + console.log('Please name your Ionic project something meaningful other than \'.\''.red); + return + } + + var promptPromise, + options = Utils.preprocessCliOptions(argv), + startingApp = true; + // Grab the app's relative directory name + + IonicStats.t(); + + if (fs.existsSync(options.targetPath)) { + promptPromise = Start.promptForOverwrite(options.targetPath); + } else { + promptPromise = Q(true); + } + + return promptPromise + .then(function(promptToContinue) { + if (!promptToContinue) { + startingApp = false; + console.log('\nIonic start cancelled by user.\n'.red.bold); + return; + } + return Start.startApp(options); + }) + .then(function() { + if (startingApp) { + Start.printQuickHelp(options); + return Start.promptLogin(options); + } + }) + .then(function() { + if (startingApp) { + return ionic.printNewsUpdates(true); + } + }) + .catch(function(error) { + Utils.fail(error); + }); +}; + +exports.IonicTask = IonicTask; diff --git a/lib/ionic/starter-templates.json b/lib/ionic/starter-templates.json new file mode 100644 index 0000000000..db0a21623f --- /dev/null +++ b/lib/ionic/starter-templates.json @@ -0,0 +1,66 @@ +{ + "total_count": "7", + "incomplete_results": false, + "items": [ + { + "name": "ionic-starter-maps", + "description": "An Ionic starter project using Google Maps and a side menu", + "created_at": "2014-04-28T20:16:01Z", + "updated_at": "2015-01-14T19:14:08Z", + "pushed_at": "2014-09-03T07:20:28Z", + "git_url": "git://github.com/driftyco/ionic-starter-maps.git" + }, + { + "name": "ionic-starter-tabs", + "description": "A starting project for Ionic using a simple tabbed interface", + "created_at": "2014-03-21T17:56:04Z", + "updated_at": "2015-01-11T16:54:23Z", + "pushed_at": "2014-12-29T21:09:20Z", + "git_url": "git://github.com/driftyco/ionic-starter-tabs.git" + }, + { + "name": "ionic-starter-sidemenu", + "description": "A starting project for Ionic using a side menu with navigation in the content area", + "created_at": "2014-03-24T22:31:35Z", + "updated_at": "2015-01-12T06:33:31Z", + "pushed_at": "2014-12-15T20:47:21Z", + "git_url": "git://github.com/driftyco/ionic-starter-sidemenu.git" + }, + { + "id": 17983811, + "name": "ionic-starter-blank", + "description": "A blank starter project for Ionic", + "created_at": "2014-03-21T15:14:45Z", + "updated_at": "2014-12-10T23:36:01Z", + "pushed_at": "2014-05-14T15:26:21Z", + "git_url": "git://github.com/driftyco/ionic-starter-blank.git" + }, + { + "id": 24473944, + "name": "ionic-starter-salesforce", + "description": "A starter project for Ionic and Salesforce", + "created_at": "2014-09-25T20:22:13Z", + "updated_at": "2015-01-06T10:08:01Z", + "pushed_at": "2014-11-06T18:57:36Z", + "git_url": "git://github.com/driftyco/ionic-starter-salesforce.git" + }, + { + "id": 24610601, + "name": "ionic-starter-tests", + "description": "A test of different kinds of page navigation", + "created_at": "2014-09-29T19:57:25Z", + "updated_at": "2015-01-17T17:12:18Z", + "pushed_at": "2015-01-17T17:12:17Z", + "git_url": "git://github.com/driftyco/ionic-starter-tests.git" + }, + { + "id": 27608851, + "name": "ionic-starter-complex-list", + "description": "A complex list starter template", + "created_at": "2014-12-05T20:29:34Z", + "updated_at": "2014-12-06T05:12:52Z", + "pushed_at": "2014-12-05T20:29:34Z", + "git_url": "git://github.com/driftyco/ionic-starter-complex-list.git" + } + ] +} diff --git a/lib/ionic/state.js b/lib/ionic/state.js new file mode 100644 index 0000000000..bfddfdb08b --- /dev/null +++ b/lib/ionic/state.js @@ -0,0 +1,101 @@ +// "cordovaPlatforms": [ +// "ios", +// { +// "android": { +// "id": "android", +// "locator": "https://github.com/apache/cordova-android.git" +// } +// } +// ], +// "cordovaPlugins": [ +// "org.apache.cordova.device", +// "org.apache.cordova.console", +// "com.ionic.keyboard", +// "org.apache.cordova.splashscreen", +// { +// "locator": "https://github.com/MobileChromeApps/cordova-crosswalk-engine.git", +// "id": "org.cordova.croswalk" +// }, +// { +// "locator": "/path/to/cloned/phonegap-facebook-plugin", +// "id": "", +// "variables": { +// "APP_ID": "some_id", +// "APP_NAME": "some_name" +// } +// } +// ] + +var fs = require('fs'), + path = require('path'), + // argv = require('optimist').boolean(['plugins', 'platforms']).argv, + Q = require('q'), + shelljs = require('shelljs'), + Task = require('./task').Task, + IonicStats = require('./stats').IonicStats, + _ = require('underscore'), + IonicProject = require('./project'), + IonicAppLib = require('ionic-app-lib'), + State = IonicAppLib.state, + Utils = IonicAppLib.utils, + IonicInfo = IonicAppLib.info; + +// var State = module.exports; + +shelljs.config.silent = true; + +var IonicTask = function() {}; + +IonicTask.prototype = new Task(); + +IonicTask.prototype.run = function run(ionic, argv) { + var self = this, + project, + stats, + projectPath, + options = { platforms: true, plugins: true }; + + this.ionic = ionic; + + try { + projectPath = path.resolve('ionic.project'); + stats = fs.statSync(projectPath); + } catch (ex) { + this.ionic.fail('You cannot run any state commands on a project that is not an Ionic project.\nTry adding an ionic.project file or running ionic start to get an application to save or restore'); + return; + } + + try { + project = IonicProject.load(); + } catch (ex) { + this.ionic.fail(ex.message); + return; + } + + //If either plugin or platforms is specified, set it to that value. + if (argv.platforms || argv.plugins) { + options = { platforms: argv.platforms, plugins: argv.plugins }; + } + + switch (argv._[1]) { + case 'save': + State.saveState(process.cwd(), options); + break; + case 'restore': + State.restoreState(process.cwd(), options); + break; + case 'reset': + State.resetState(process.cwd(), options); + break; + case 'clear': + State.clearState(process.cwd(), options); + break; + default: + console.log('Please specify a command [ save | restore | reset | clear ] for the state command.'.red.bold); + } + + IonicStats.t(); + +}; + +exports.IonicTask = IonicTask; diff --git a/lib/ionic/stats.js b/lib/ionic/stats.js new file mode 100644 index 0000000000..41d643da75 --- /dev/null +++ b/lib/ionic/stats.js @@ -0,0 +1,635 @@ +var MixpanelAPI, crypto, Buffer, http, querystring, util, path, fs, os, IonicConfig; +var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, + __slice = Array.prototype.slice; + +var path = require('path'), + http = require('http'), + querystring = require('querystring'), + crypto = require('crypto'), + Buffer = require('buffer').Buffer, + util = require('util'), + es = require('event-stream'), + fs = require('fs'), + os = require('os'), + IonicConfig = require('./config'); + +/* + Heavily inspired by the original js library copyright Mixpanel, Inc. + (http://mixpanel.com/) + + Copyright (c) 2012 Carl Sverre + + Released under the MIT license. +*/ + +var create_client = function(token, config) { + var metrics = {}; + + if(!token) { + throw new Error("The Mixpanel Client needs a Mixpanel token: `init(token)`"); + } + + metrics.config = { + test: false, + debug: false, + verbose: false + }; + + metrics.token = token; + + /** + send_request(data) + --- + this function sends an async GET request to mixpanel + + data:object the data to send in the request + callback:function(err:Error) callback is called when the request is + finished or an error occurs + */ + metrics.send_request = function(endpoint, data, callback) { + callback = callback || function() {}; + var event_data = new Buffer(JSON.stringify(data)); + var request_data = { + 'data': event_data.toString('base64'), + 'ip': 0, + 'verbose': metrics.config.verbose ? 1 : 0 + }; + + if (endpoint === '/import') { + var key = metrics.config.key; + if (!key) { + throw new Error("The Mixpanel Client needs a Mixpanel api key when importing old events: `init(token, { key: ... })`"); + } + request_data.api_key = key; + } + + var request_options = { + host: 'api.mixpanel.com', + port: 80, + headers: {} + }; + + if (metrics.config.test) { request_data.test = 1; } + + var query = querystring.stringify(request_data); + + request_options.path = [endpoint,"?",query].join(""); + + http.get(request_options, function(res) { + var data = ""; + res.on('data', function(chunk) { + data += chunk; + }); + + res.on('end', function() { + var e; + if(metrics.config.verbose) { + try { + var result = JSON.parse(data); + if(result.status != 1) { + e = new Error("Mixpanel Server Error: " + result.error); + } + } + catch(ex) { + e = new Error("Could not parse response from Mixpanel"); + } + } + else { + e = (data !== '1') ? new Error("Mixpanel Server Error: " + data) : undefined; + } + + callback(e); + }); + }).on('error', function(e) { + if(metrics.config.debug) { + console.log("Got Error: " + e.message); + } + callback(e); + }); + }; + + /** + track(event, properties, callback) + --- + this function sends an event to mixpanel. + + event:string the event name + properties:object additional event properties to send + callback:function(err:Error) callback is called when the request is + finished or an error occurs + */ + metrics.track = function(event, properties, callback) { + if (typeof(properties) === 'function' || !properties) { + callback = properties; + properties = {}; + } + + // if properties.time exists, use import endpoint + var endpoint = (typeof(properties.time) === 'number') ? '/import' : '/track'; + + properties.token = metrics.token; + properties.mp_lib = "node"; + + var data = { + 'event' : event, + 'properties' : properties + }; + + if (metrics.config.debug) { + console.log("Sending the following event to Mixpanel:"); + console.log(data); + } + + metrics.send_request(endpoint, data, callback); + }; + + /** + import(event, properties, callback) + --- + This function sends an event to mixpanel using the import + endpoint. The time argument should be either a Date or Number, + and should signify the time the event occurred. + + It is highly recommended that you specify the distinct_id + property for each event you import, otherwise the events will be + tied to the IP address of the sending machine. + + For more information look at: + https://mixpanel.com/docs/api-documentation/importing-events-older-than-31-days + + event:string the event name + time:date|number the time of the event + properties:object additional event properties to send + callback:function(err:Error) callback is called when the request is + finished or an error occurs + */ + metrics.import = function(event, time, properties, callback) { + if (typeof(properties) === 'function' || !properties) { + callback = properties; + properties = {}; + } + + if (time === void 0) { + throw new Error("The import method requires you to specify the time of the event"); + } else if (Object.prototype.toString.call(time) === '[object Date]') { + time = Math.floor(time.getTime() / 1000); + } + + properties.time = time; + + metrics.track(event, properties, callback); + }; + + /** + alias(distinct_id, alias) + --- + This function creates an alias for distinct_id + + For more information look at: + https://mixpanel.com/docs/integration-libraries/using-mixpanel-alias + + distinct_id:string the current identifier + alias:string the future alias + */ + metrics.alias = function(distinct_id, alias, callback) { + var properties = { + distinct_id: distinct_id, + alias: alias + }; + + metrics.track('$create_alias', properties, callback); + }; + + metrics.people = { + /** people.set_once(distinct_id, prop, to, callback) + --- + The same as people.set but in the words of mixpanel: + mixpanel.people.set_once + + " This method allows you to set a user attribute, only if + it is not currently set. It can be called multiple times + safely, so is perfect for storing things like the first date + you saw a user, or the referrer that brought them to your + website for the first time. " + + */ + set_once: function(distinct_id, prop, to, callback) { + var $set = {}, data = {}; + + if (typeof(prop) === 'object') { + callback = to; + $set = prop; + } else { + $set[prop] = to; + } + + this._set(distinct_id, $set, callback, { set_once: true }); + }, + + /** + people.set(distinct_id, prop, to, callback) + --- + set properties on an user record in engage + + usage: + + mixpanel.people.set('bob', 'gender', 'm'); + + mixpanel.people.set('joe', { + 'company': 'acme', + 'plan': 'premium' + }); + */ + set: function(distinct_id, prop, to, callback) { + var $set = {}, data = {}; + + if (typeof(prop) === 'object') { + callback = to; + $set = prop; + } else { + $set[prop] = to; + } + + this._set(distinct_id, $set, callback); + }, + + // used internally by set and set_once + _set: function(distinct_id, $set, callback, options) { + var set_key = (options && options.set_once) ? "$set_once" : "$set"; + + var data = { + '$token': metrics.token, + '$distinct_id': distinct_id + }; + data[set_key] = $set; + + if ('ip' in $set) { + data.$ip = $set.ip; + delete $set.ip; + } + + if ($set.$ignore_time) { + data.$ignore_time = $set.$ignore_time; + delete $set.$ignore_time; + } + + if(metrics.config.debug) { + console.log("Sending the following data to Mixpanel (Engage):"); + console.log(data); + } + + metrics.send_request('/engage', data, callback); + }, + + /** + people.increment(distinct_id, prop, to, callback) + --- + increment/decrement properties on an user record in engage + + usage: + + mixpanel.people.increment('bob', 'page_views', 1); + + // or, for convenience, if you're just incrementing a counter by 1, you can + // simply do + mixpanel.people.increment('bob', 'page_views'); + + // to decrement a counter, pass a negative number + mixpanel.people.increment('bob', 'credits_left', -1); + + // like mixpanel.people.set(), you can increment multiple properties at once: + mixpanel.people.increment('bob', { + counter1: 1, + counter2: 3, + counter3: -2 + }); + */ + increment: function(distinct_id, prop, by, callback) { + var $add = {}; + + if (typeof(prop) === 'object') { + callback = by; + Object.keys(prop).forEach(function(key) { + var val = prop[key]; + + if (isNaN(parseFloat(val))) { + if (metrics.config.debug) { + console.error("Invalid increment value passed to mixpanel.people.increment - must be a number"); + console.error("Passed " + key + ":" + val); + } + return; + } else { + $add[key] = val; + } + }); + } else { + if (!by) { by = 1; } + $add[prop] = by; + } + + var data = { + '$add': $add, + '$token': metrics.token, + '$distinct_id': distinct_id + }; + + if(metrics.config.debug) { + console.log("Sending the following data to Mixpanel (Engage):"); + console.log(data); + } + + metrics.send_request('/engage', data, callback); + }, + + /** + people.track_charge(distinct_id, amount, properties, callback) + --- + Record that you have charged the current user a certain + amount of money. + + usage: + + // charge a user $29.99 + mixpanel.people.track_charge('bob', 29.99); + + // charge a user $19 on the 1st of february + mixpanel.people.track_charge('bob', 19, { '$time': new Date('feb 1 2012') }); + */ + track_charge: function(distinct_id, amount, properties, callback) { + var $append = {}; + + if (!properties) { properties = {}; } + + if (typeof(amount) !== 'number') { + amount = parseFloat(amount); + if (isNaN(amount)) { + console.error("Invalid value passed to mixpanel.people.track_charge - must be a number"); + return; + } + } + + properties.$amount = amount; + + if (properties.hasOwnProperty('$time')) { + var time = properties.$time; + if (Object.prototype.toString.call(time) === '[object Date]') { + properties.$time = time.toISOString(); + } + } + + var data = { + '$append': { '$transactions': properties }, + '$token': metrics.token, + '$distinct_id': distinct_id + }; + + if(metrics.config.debug) { + console.log("Sending the following data to Mixpanel (Engage):"); + console.log(data); + } + + metrics.send_request('/engage', data, callback); + }, + + /** + people.clear_charges(distinct_id, callback) + --- + Clear all the current user's transactions. + + usage: + + mixpanel.people.clear_charges('bob'); + */ + clear_charges: function(distinct_id, callback) { + var data = { + '$set': { '$transactions': [] }, + '$token': metrics.token, + '$distinct_id': distinct_id + }; + + if(metrics.config.debug) { + console.log("Clearing this user's charges:", distinct_id); + } + + metrics.send_request('/engage', data, callback); + }, + + /** + people.delete_user(distinct_id, callback) + --- + delete an user record in engage + + usage: + + mixpanel.people.delete_user('bob'); + */ + delete_user: function(distinct_id, callback) { + var data = { + '$delete': distinct_id, + '$token': metrics.token, + '$distinct_id': distinct_id + }; + + if(metrics.config.debug) { + console.log("Deleting the user from engage:", distinct_id); + } + + metrics.send_request('/engage', data, callback); + }, + + /** + people.unset(distinct_id, prop, callback) + --- + delete a property on an user record in engage + + usage: + + mixpanel.people.unset('bob', 'page_views'); + + mixpanel.people.unset('bob', ['page_views', 'last_login']); + */ + unset: function(distinct_id, prop, callback) { + var $unset = []; + + if (util.isArray(prop)) { + $unset = prop; + } else if (typeof(prop) === 'string') { + $unset = [prop]; + } else { + if (metrics.config.debug) { + console.error("Invalid argument passed to mixpanel.people.unset - must be a string or array"); + console.error("Passed: " + prop); + } + return; + } + + var data = { + '$unset': $unset, + '$token': metrics.token, + '$distinct_id': distinct_id + }; + + if(metrics.config.debug) { + console.log("Sending the following data to Mixpanel (Engage):"); + console.log(data); + } + + metrics.send_request('/engage', data, callback); + } + }; + + /** + set_config(config) + --- + Modifies the mixpanel config + + config:object an object with properties to override in the + mixpanel client config + */ + metrics.set_config = function(config) { + for (var c in config) { + if (config.hasOwnProperty(c)) { + metrics.config[c] = config[c]; + } + } + }; + + if (config) { + metrics.set_config(config); + } + + return metrics; +}; + +// module exporting +/* +module.exports = { + Client: function(token) { + console.warn("The function `Client(token)` is deprecated. It is now called `init(token)`."); + return create_client(token); + }, + init: create_client +}; +*/ + +var mixpanel = create_client('69f7271aa8f3d43f2e1b6baf698159b7'); + +var ionicConfig = IonicConfig.load(); + + +exports.IonicStats = { + t: function(additionalData) { + try { + + if(process.argv.length < 3) return; + + if( ionicConfig.get('statsOptOut') === true ) { + return; + } + + var cmdName = process.argv[2].toLowerCase(); + var cmdArgs = (process.argv.length > 3 ? process.argv.slice(3) : []); // skip the cmdName + + var statsData = additionalData || {}; + var platforms = []; + var x, y, cmd; + + // update any aliases with the full cmd so there's a common property + var aliasMap = { + 'rm': 'remove', + 'ls': 'list', + 'up': 'update', + '-w': '--no-cordova', + '-b': '--nobrowser', + '-r': '--nolivereload', + '-x': '--noproxy', + '-l': '--livereload', + '-c': '--consolelogs', + '-s': '--serverlogs', + '-n': '--no-email', + '-s': '--sass' + }; + for(x=0; x -1) return; + } catch(e2) {} + + try { + statsData.email = ionicConfig.get('email'); + } catch (e2) {} + + try { + statsData.account_id = ionicConfig.get('id'); + } catch (e2) {} + + this.mp(cmdName, statsData); + //console.log(cmdName, statsData); + + } catch(e) { + console.log( ('Error stats: ' + e) ); + } + }, + mp: function(e, d) { + var unique_id = ionicConfig.get('ank'); + if(!unique_id) { + this.createId(); + unique_id = ionicConfig.get('ank'); + } + d.distinct_id = unique_id; + mixpanel.track(e, d, function(err, data) { + }); + }, + createId: function() { + var d = new Date().getTime(); + var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r = (d + Math.random()*16)%16 | 0; + d = Math.floor(d/16); + return (c=='x' ? r : (r&0x7|0x8)).toString(16); + }); + + ionicConfig.set('ank', uuid); + } +}; diff --git a/lib/ionic/store.js b/lib/ionic/store.js new file mode 100644 index 0000000000..18376f4d74 --- /dev/null +++ b/lib/ionic/store.js @@ -0,0 +1,57 @@ +var fs = require('fs'), + path = require('path'); + + +var IonicStore = function(fileName) { + this.data = {}; + + if(!fileName) return this; + + this.fileName = fileName; + if(fileName.indexOf('.') < 0) { + this.fileName += '.data'; + } + + this.homeDir = process.env.HOME || process.env.USERPROFILE || process.env.HOMEPATH; + + this.privateDir = path.join(this.homeDir, '.ionic'); + + if(!fs.existsSync(this.privateDir)) { + fs.mkdirSync(this.privateDir); + } + + this.filePath = path.join(this.privateDir, this.fileName); + + try { + this.data = JSON.parse(fs.readFileSync(this.filePath)); + } catch(e) {} + + return this; +}; + +IonicStore.prototype.get = function(k) { + if(k) { + return this.data[k]; + } + return this.data; +}; + +IonicStore.prototype.set = function(k, v) { + this.data[k] = v; +}; + +IonicStore.prototype.remove = function(k) { + delete this.data[k]; +}; + +IonicStore.prototype.save = function() { + try { + var dataStoredAsString = JSON.stringify(this.data, null, 2); + fs.writeFileSync(this.filePath, dataStoredAsString); + this.data = JSON.parse(dataStoredAsString); + } catch(e) { + console.error('Unable to save ionic data:', this.filePath, e); + } +}; + +exports.IonicStore = IonicStore; diff --git a/lib/ionic/table.js b/lib/ionic/table.js new file mode 100644 index 0000000000..92c38db85a --- /dev/null +++ b/lib/ionic/table.js @@ -0,0 +1,33 @@ +var _ = require('underscore'), + cliTable = require('cli-table'); + +function Table(options) { + if (!options) { + options = {}; + } + + cliTable.call(this, _.extend({ + chars: { + 'top': '', + 'top-mid': '', + 'top-left': '', + 'top-right': '', + 'bottom': '', + 'bottom-mid': '', + 'bottom-left': '', + 'bottom-right': '', + 'left': ' ', + 'left-mid': '', + 'right': '', + 'right-mid': '' + }, + style: { + compact: true, + head: ['yellow'] + } + }, options)); +} + +Table.prototype = Object.create(cliTable.prototype); + +module.exports = Table; diff --git a/lib/ionic/task.js b/lib/ionic/task.js new file mode 100644 index 0000000000..3385c4af82 --- /dev/null +++ b/lib/ionic/task.js @@ -0,0 +1,8 @@ +var Task = function() { }; + +Task.prototype = { + run: function(ionic) { + } +}; + +exports.Task = Task; diff --git a/lib/ionic/templates.js b/lib/ionic/templates.js new file mode 100644 index 0000000000..5172aa387f --- /dev/null +++ b/lib/ionic/templates.js @@ -0,0 +1,75 @@ +var colors = require('colors'), + path = require('path'), + _ = require('underscore'), + Q = require('q'), + Task = require('./task').Task, + IonicStats = require('./stats').IonicStats, + request = require('request'); + // starterTemplates = require('./starter-templates') + +var IonicTask = function() {}; + +IonicTask.prototype = new Task(); + +IonicTask.prototype.fetchStarterTemplates = function() { + var self = this; + + // console.log('About to fetch template'); + var downloadUrl = 'http://code.ionicframework.com/content/starter-templates.json'; + var starterTemplateJsonPath = path.resolve(__dirname, 'starter-templates.json'); + + // console.log('\nDownloading Starter Templates'.bold, downloadUrl, starterTemplateJsonPath); + console.log('\nDownloading Starter Templates'.bold, '-', downloadUrl); + + var q = Q.defer(); + + var proxy = process.env.PROXY || null; + request({ url: downloadUrl, proxy: proxy }, function(err, res, html) { + if(!err && res && res.statusCode === 200) { + var templatesJson = {}; + try { + templatesJson = JSON.parse(html); + }catch(ex) { + console.log('ex', ex) + q.reject('Error occured in download templates:', ex) + self.ionic.fail(ex); + return; + } + q.resolve(templatesJson); + } else { + console.log('Unable to fetch the starter templates. Please check your internet connection'.red); + q.reject(res); + } + }); + return q.promise; +}; + +IonicTask.prototype.list = function list(templates) { + //Should have array of [{ name: 'name', description: 'desc' }] + console.log('\n') + _.each(templates, function(template) { + var rightColumn = 20, dots = ''; + var shortName = template.name.replace('ionic-starter-', ''); + while( (shortName + dots).length < rightColumn + 1) { + dots += '.'; + } + var outStr = [] + console.log(shortName.green, dots, template.description); + }) +}; + +IonicTask.prototype.run = function(ionic) { + var self = this; + + self.fetchStarterTemplates() + .then(function(starterTemplates) { + var templates = _.sortBy(starterTemplates.items, function(template){ return template.name; }); + console.log('Ionic Starter templates'.green); + self.list(templates); + }); + + IonicStats.t(); + +}; + +exports.IonicTask = IonicTask; diff --git a/lib/ionic/upload.js b/lib/ionic/upload.js new file mode 100644 index 0000000000..2c31f89ab7 --- /dev/null +++ b/lib/ionic/upload.js @@ -0,0 +1,37 @@ +var IonicAppLib = require('ionic-app-lib'), + IonicLoginTask = require('./login').IonicTask, + IonicStats = require('./stats').IonicStats, + Login = IonicAppLib.login, + LoginTask = require('./login'), + Task = require('./task').Task, + Upload = IonicAppLib.upload, + Utils = IonicAppLib.utils; + +var IonicTask = function() {}; + +IonicTask.prototype = new Task(); + +IonicTask.prototype.run = function run(ionic, argv) { + var note = argv.note; + var deploy = argv.deploy || false; + + IonicStats.t(); + + return Login.retrieveLogin() + .then(function(jar){ + if (!jar) { + console.log('No previous login existed. Attempting to log in now.'); + return LoginTask.login(argv); + } + return jar; + }) + .then(function(jar) { + return Upload.doUpload(process.cwd(), jar, note, deploy); + }) + .catch(function(ex) { + // console.log('Error', ex, ex.stack); + Utils.fail(ex); + }); +}; + +exports.IonicTask = IonicTask; diff --git a/lib/ionic/utils.js b/lib/ionic/utils.js new file mode 100644 index 0000000000..641d4ca5a2 --- /dev/null +++ b/lib/ionic/utils.js @@ -0,0 +1,44 @@ +var transformCookies = function transformCookies(jar) { + return jar.map(function(c) { + return c.key + "=" + encodeURIComponent(c.value); + }).join("; "); +} + +var retrieveCsrfToken = function retrieveCsrfToken(jar) { + // console.log('retrieveCsrfToken', jar) + if(!jar || typeof jar == 'undefined' || jar.length == 0) { + // console.log('no jar folks') + return ''; + } + var csrftoken = ''; + for (var i = 0; i < jar.length; i++) { + if (jar[i].key == 'csrftoken') { + csrftoken = jar[i].value; + break; + } + } + return csrftoken; +} + +var deleteFolderRecursive = function(removePath) { + var fs = require('fs'), + path = require('path') + + if( fs.existsSync(removePath) ) { + fs.readdirSync(removePath).forEach(function(file,index){ + var curPath = path.join(removePath, file); + if(fs.lstatSync(curPath).isDirectory()) { // recurse + deleteFolderRecursive(curPath); + } else { // delete file + fs.unlinkSync(curPath); + } + }); + fs.rmdirSync(removePath); + } +}; + +module.exports = { + deleteFolderRecursive: deleteFolderRecursive, + retrieveCsrfToken: retrieveCsrfToken, + transformCookies: transformCookies +} diff --git a/lib/tasks/bumpversion.js b/lib/tasks/bumpversion.js new file mode 100644 index 0000000000..c3b7327a7c --- /dev/null +++ b/lib/tasks/bumpversion.js @@ -0,0 +1,55 @@ +var colors = require('colors'), + path = require('path'), + semver = require('semver'), + shelljs = require('shelljs'), + args = require('optimist').argv, + fs = require('fs'), + jsonPath = path.join(__dirname, '../', '../', 'package.json'), + packageJson = require(jsonPath), + version = null, + bumpLevel = null; + +// version = args.version ? args.version : semver.inc(packageJson.version, bumpLevel), + +if(args.level && args.level == 'pre') { + var id = args.identifier || 'beta'; + bumpLevel = [args.level, id].join(''); + version = semver.inc(packageJson.version, args.level, id) +} else { + bumpLevel = args.level || 'patch'; + version = args.version ? args.version : semver.inc(packageJson.version, bumpLevel) +} + +var bumpVersionMessage = ['Bumping from ', packageJson.version, ' by ', bumpLevel, ' resulting in ', version].join(''); + +console.log(bumpVersionMessage.green) + +packageJson.version = version; + +if(args.npmInstall) { + shelljs.rm('-rf', 'node_modules'); + shelljs.exec('npm install'); +} + +shelljs.rm('npm-shrinkwrap.json'); + +fs.writeFileSync(jsonPath, JSON.stringify(packageJson, null, 2)); + +shelljs.exec('npm shrinkwrap') + +if(args.git) { + console.log('git commands') + // shelljs.exec('git add npm-shrinkwrap.json'); + // shelljs.exec(['git commit -m "', bumpVersionMessage, '"'].join('')); + // shelljs.exec(['git tag ', version].join('')); + // shelljs.exec(['git push origin ', version].join('')) +} + +if(args.npmPublishTag) { + console.log('npm publish commands') + var publishTagCmd = ['npm publish --tag ', version].join(''); + console.log(publishTagCmd); + // shelljs.exec('npm publish'); +} else if(args.npmPublish) { + console.log('npm publish') +} diff --git a/lib/tasks/cliTasks.js b/lib/tasks/cliTasks.js new file mode 100644 index 0000000000..b93d1773fa --- /dev/null +++ b/lib/tasks/cliTasks.js @@ -0,0 +1,494 @@ +var resourcesSummary = [ + 'Automatically create icon and splash screen resources' + ' (beta)'.yellow, + 'Put your images in the ./resources directory, named splash or icon.', + 'Accepted file types are .png, .ai, and .psd.', + 'Icons should be 192x192 px without rounded corners.', + 'Splashscreens should be 2208x2208 px, with the image centered in the middle.\n' +].join('\n\t\t '); + +var cordovaRunEmulateOptions = { + '--livereload|-l': 'Live reload app dev files from the device' + ' (beta)'.yellow, + '--address': 'Use specific address (livereload req.)', + '--port|-p': 'Dev server HTTP port (8100 default, livereload req.)', + '--livereload-port|-r': 'Live Reload port (35729 default, livereload req.)', + '--consolelogs|-c': { + title: 'Print app console logs to Ionic CLI (livereload req.)', + boolean: true + }, + '--serverlogs|-s': { + title: 'Print dev server logs to Ionic CLI (livereload req.)', + boolean: true + }, + '--debug|--release': { + title: '', + boolean: true + }, + '--device|--emulator|--target=FOO': '' +}; + +var TASKS = [ + { + title: 'start', + name: 'start', + summary: 'Starts a new Ionic project in the specified PATH', + args: { + '[options]': 'any flags for the command', + '': 'directory for the new project', + '[template]': 'Starter templates can either come from a named template, \n' + + '(ex: tabs, sidemenu, blank),\n' + + 'a Github repo, a Codepen url, or a local directory.\n' + + 'Codepen url, ex: http://codepen.io/ionic/pen/odqCz\n' + + 'Defaults to Ionic "tabs" starter template' + }, + options: { + '--appname|-a': 'Human readable name for the app (Use quotes around the name)', + '--id|-i': 'Package name for config, ex: com.mycompany.myapp', + '--no-cordova|-w': { + title: 'Create a basic structure without Cordova requirements', + boolean: true + }, + '--sass|-s': { + title: 'Setup the project to use Sass CSS precompiling', + boolean: true + }, + '--list|-l': { + title: 'List starter templates available', + boolean: true + }, + '--io-app-id': 'The Ionic.io app ID to use', + '--template|-t': 'Project starter template', + '--zip-file|-z': 'URL to download zipfile for starter template' + }, + module: './ionic/start' + }, + { + title: 'serve', + name: 'serve', + summary: 'Start a local development server for app dev/testing', + args: { + '[options]': '' + }, + options: { + '--consolelogs|-c': { + title: 'Print app console logs to Ionic CLI', + boolean: true + }, + '--serverlogs|-s': { + title: 'Print dev server logs to Ionic CLI', + boolean: true + }, + '--port|-p': 'Dev server HTTP port (8100 default)', + '--livereload-port|-r': 'Live Reload port (35729 default)', + '--nobrowser|-b': { + title: 'Disable launching a browser', + boolean: true + }, + '--nolivereload|-d': { + title: 'Do not start live reload', + boolean: true + }, + '--noproxy|-x': { + title: 'Do not add proxies', + boolean: true + }, + '--address': 'Use specific address or return with failure', + '--all|-a': { + title: 'Have the server listen on all addresses (0.0.0.0)', + boolean: true + }, + '--browser|-w': 'Specifies the browser to use (safari, firefox, chrome)', + '--browseroption|-o': 'Specifies a path to open to (/#/tab/dash)', + '--lab|-l': { + title: 'Test your apps on multiple screen sizes and platform types', + boolean: true + }, + '--nogulp': { + title: 'Disable running gulp during serve', + boolean: true + }, + '--platform|-t': 'Start serve with a specific platform (ios/android)' + }, + module: './ionic/serve' + }, + { + title: 'platform', + name: 'platform', + alt: ['platforms'], + summary: 'Add platform target for building an Ionic app', + args: { + '[options]': '', + '': '' + }, + options: { + '--noresources|-r': { + title: 'Do not add default Ionic icons and splash screen resources', + boolean: true + }, + '--nosave|-e': { + title: 'Do not save the platform to the package.json file', + boolean: true + } + }, + module: './ionic/cordova', + alt: ['platforms'] + }, + { + title: 'run', + name: 'run', + summary: 'Run an Ionic project on a connected device', + args: { + '[options]': '', + '': '' + }, + options: cordovaRunEmulateOptions, + module: './ionic/cordova' + }, + { + title: 'emulate', + name: 'emulate', + summary: 'Emulate an Ionic project on a simulator or emulator', + args: { + '[options]': '', + '': '' + }, + options: cordovaRunEmulateOptions, + module: './ionic/cordova' + }, + { + title: 'build', + name: 'build', + summary: 'Locally build an Ionic project for a given platform', + args: { + '[options]': '', + '': '' + }, + options: { + '--nohooks|-n': { + title: 'Do not add default Ionic hooks for Cordova', + boolean: true + } + }, + module: './ionic/cordova' + }, + { + title: 'plugin add', + name: 'plugin', + summary: 'Add a Cordova plugin', + args: { + '[options]': '', + '': 'Can be a plugin ID, a local path, or a git URL.' + }, + options: { + '--searchpath ': 'When looking up plugins by ID, look in this directory\n' + + 'and subdirectories first for the plugin before\n' + + 'looking it up in the registry.', + '--nosave|-e': { + title: 'Do not save the plugin to the package.json file', + boolean: true + } + }, + alt: ['plugins'], + module: './ionic/cordova' + }, + { + title: 'prepare', + name: 'prepare', + module: './ionic/cordova' + }, + { + title: 'compile', + name: 'compile', + module: './ionic/cordova' + }, + { + title: 'resources', + name: 'resources', + summary: resourcesSummary, + options: { + '--icon|-i': { + title: 'Generate icon resources', + boolean: true + }, + '--splash|-s': { + title: 'Generate splash screen resources', + boolean: true + } + }, + module: './ionic/resources' + }, + { + title: 'upload', + name: 'upload', + summary: 'Upload an app to your Ionic account', + options: { + '--email|-e': 'Ionic account email', + '--password|-p': 'Ionic account password', + '--note': 'The note to signify the upload', + '--deploy ': 'Deploys the upload to the given channel. Defaults to the Dev channel' + }, + alt: ['up'], + module: './ionic/upload' + }, + { + title: 'share', + name: 'share', + summary: 'Share an app with a client, co-worker, friend, or customer', + args: { + '': 'The email to share the app with', + }, + module: './ionic/share' + }, + { + title: 'lib', + name: 'lib', + summary: 'Gets Ionic library version or updates the Ionic library', + args: { + '[options]': '', + '[update]': 'Updates the Ionic Framework in www/lib/ionic' + }, + options: { + '--version|-v': 'Specific Ionic version\nOtherwise it defaults to the latest version' + }, + module: './ionic/lib' + }, + { + title: 'setup', + name: 'setup', + summary: 'Configure the project with a build tool ' + '(beta)'.yellow, + args: { + '[sass]': 'Setup the project to use Sass CSS precompiling' + }, + module: './ionic/setup' + }, + { + title: 'login', + name: 'login', + module: './ionic/login' + }, + { + title: 'address', + name: 'address', + module: './ionic/serve' + }, + { + title: 'app', + name: 'app', + // summary: 'Deploy a new Ionic app version or list versions', + // options: { + // '--versions|-v': 'List recently uploaded versions of this app', + // '--deploy|-d': 'Upload the current working copy and mark it as deployed', + // '--note|-n': 'Add a note to a deploy', + // '--uuid|-u': 'Mark an already uploaded version as deployed' + // }, + module: './ionic/app' + }, + { + title: 'io', + name: 'io', + summary: 'Integrate your app with the ionic.io platform services ' + '(alpha)'.red, + args: { + '': 'init'.yellow + }, + module: './ionic/io-init' + }, + { + title: 'security', + name: 'security', + summary: 'Store your app\'s credentials for the Ionic Platform ' + '(alpha)'.red, + args: { + '': 'profiles list'.yellow + ', ' + 'profiles add ""'.yellow + ', ' + 'credentials android'.yellow + ', or ' + 'credentials ios'.yellow, + '[options]': '' + }, + options: { + '--profile ': '(' + 'credentials '.yellow + ') Specify the profile on which these credentials are saved', + '--keystore|-s ': '(' + 'credentials android'.yellow + ') Specify the location of your keystore file', + '--keystore-password|-p ': '(' + 'credentials android'.yellow + ') Specify your keystore password (exclude for prompt)', + '--key-alias|-k ': '(' + 'credentials android'.yellow + ') Specify your key alias for this app', + '--key-password|-w ': '(' + 'credentials android'.yellow + ') Specify your key password for this app (exclude for prompt)', + '--cert|-c ': '(' + 'credentials ios'.yellow + ') Specify the location of your .p12 file', + '--cert-password|-p ': '(' + 'credentials ios'.yellow + ') Specify your certificate password (exclude for prompt)', + '--provisioning-profile|-r ': '(' + 'credentials ios'.yellow + ') Specify the location of your .mobileprovision file', + }, + module: './ionic/security' + }, + { + title: 'push', + name: 'push', + summary: 'Upload APNS and GCM credentials to Ionic Push ' + '(alpha)'.red, + options: { + '--ios-dev-cert': 'Upload your development .p12 file to Ionic Push', + '--ios-prod-cert': 'Upload your production .p12 file to Ionic Push', + '--production-mode=y,n': 'Tell Ionic Push to use production (y) or sandbox (n) APNS servers', + '--google-api-key ': "Set your app's GCM API key on Ionic Push" + }, + module: './ionic/push' + }, + { + title: 'package', + name: 'package', + summary: 'Use Ionic Package to build your app ' + '(alpha)'.red, + args: { + '': 'build android'.yellow + ', ' + 'build ios'.yellow + ', ' + 'list'.yellow + ', ' + 'info'.yellow + ', or ' + 'download'.yellow, + '[options]': '' + }, + options: { + '--release': '(' + 'build '.yellow + ') Mark this build as a release', + '--profile|-p ': '(' + 'build '.yellow + ') Specify the Security Profile to use with this build', + '--noresources': '(' + 'build '.yellow + ') Do not generate icon and splash screen resources during this build', + '--destination|-d ': '(' + 'download'.yellow + ') Specify the destination directory to download your packaged app.' + }, + module: './ionic/package' + }, + { + title: 'config', + name: 'config', + summary: 'Set configuration variables for your ionic app ' + '(alpha)'.red, + args: { + '': 'set'.yellow + ', ' + 'unset'.yellow + ', ' + 'build'.yellow + ', or ' + 'info'.yellow, + '[key]': 'The key to set', + '[value]': 'The value to set' + }, + module: './ionic/io-config' + }, + { + title: 'browser', + name: 'browser', + summary: 'Add another browser for a platform ' + '(beta)'.yellow, + args: { + '': '"add remove rm info versions upgrade list ls revert"', + '[browser]': 'The browser you wish to add or remove (Crosswalk)' + }, + options: { + '--nosave|-n': { + title: 'Do not save the platforms and plugins to the package.json file', + boolean: true + } + }, + module: './ionic/browser' + }, + { + title: 'service add', + name: 'service', + summary: 'Add an Ionic service package and install any required plugins', + args: { + '[options]': '', + '': 'Can be a service name or a git url' + }, + module: './ionic/service' + }, + { + title: 'add', + name: 'add', + summary: 'Add an Ion, bower component, or addon to the project', + args: { + '[name]': 'The name of the ion, bower component, or addon you wish to install' + }, + module: './ionic/add' + }, + { + title: 'remove', + name: 'remove', + summary: 'Remove an Ion, bower component, or addon from the project', + args: { + '[name]': 'The name of the Ion, bower component, or addon you wish to remove' + }, + module: './ionic/add', + alt: ['rm'] + }, + { + title: 'list', + name: 'list', + summary: 'List Ions, bower components, or addons in the project', + module: './ionic/add', + alt: ['ls'] + }, + /* + { + title: 'ions', + name: 'ions', + summary: 'List available ions to add to your project', + module: './ionic/ions' + }, + { + title: 'templates', + name: 'templates', + summary: 'List available Ionic starter templates', + module: './ionic/templates' + }, + */ + { + title: 'info', + name: 'info', + summary: 'List information about the users runtime environment', + module: './ionic/info' + }, + { + title: 'help', + name: 'help', + summary: 'Provides help for a certain command', + args: { + '[command]': 'The command you desire help with' + }, + module: './ionic/help' + }, + { + title: 'link', + name: 'link', + summary: 'Sets your Ionic App ID for your project', + args: { + '[appId]': 'The app ID you wish to set for this project' + }, + options: { + '--reset|-r': { + title: 'This will reset the Ionic App ID', + boolean: true + } + }, + module: './ionic/link' + }, + { + + title: 'hooks', + name: 'hooks', + summary: 'Manage your Ionic Cordova hooks', + args: { + '[add|remove|permissions|perm]': 'Add, remove, or modify permissions on the default Ionic Cordova hooks' + }, + module: './ionic/hooks' + }, + { + title: 'state', + name: 'state', + summary: 'Saves or restores state of your Ionic Application using the package.json file', + args: { + '': '[ save | restore | clear | reset ]' + }, + options: { + 'save': 'Save the platforms and plugins into package.json', + 'restore': 'Restore the platforms and plugins from package.json', + 'clear': 'Clear the package.json of cordovaPlugins and cordovaPlatforms, as well as clear out the platforms and plugins folders', + 'reset': 'Clear out the platforms and plugins directories, and reinstall plugins and platforms', + '--plugins': { + title: 'Only do operations with plugins', + boolean: true + }, + '--platforms': { + title: 'Only do operations with platforms', + boolean: true + } + }, + module: './ionic/state' + }, + { + title: 'docs', + name: 'docs', + summary: 'Opens up the documentation for Ionic', + args: { + '': 'the topic to view help documentation for. Use "ls" to view all topics' + }, + module: './ionic/docs' + } +]; + + +module.exports = TASKS; diff --git a/lib/tasks/createDocsJson.js b/lib/tasks/createDocsJson.js new file mode 100644 index 0000000000..44dc396227 --- /dev/null +++ b/lib/tasks/createDocsJson.js @@ -0,0 +1,59 @@ +var fs = require('fs'), + path = require('path'), + cl = console.log; + + + +function crawlDocsCreateJson() { + + var docsJson = { + versions: [], + api: {} + } + + var docsPath = path.resolve('docs'); + var docsApiPath = path.join(docsPath, 'api'); + + var docsDirs = fs.readdirSync(docsPath); + + docsDirs.forEach(function(doc) { + var isDocVersion = !isNaN(doc[0]) + if(isDocVersion) { + docsJson.versions.push(doc) + } + }) + + var apiDocsDirs = fs.readdirSync(docsApiPath); + + apiDocsDirs.forEach(function(docs) { + console.log('docs:', docs); + if(docs.indexOf('.md') != -1) { + return + } + docsJson.api[docs] = {id: docs, docs: []}; + + var apiDocPath = path.join(docsApiPath, docs) + console.log('apiDocPath:', apiDocPath) + + var apiSubDocs = fs.readdirSync(apiDocPath) + + apiSubDocs.forEach(function(subdoc) { + console.log('subdoc: ', subdoc) + docsJson.api[docs].docs.push(subdoc); + }) + }) + + // cl('json:') + // cl(docsJson) + + // cl(docsJson.api.controller.docs) + // docsJson.api.controller.docs.forEach(function(e) { + // cl(e) + // }) + + cl(JSON.stringify(docsJson)) + +} + + +crawlDocsCreateJson(); diff --git a/lint-staged.config.base.js b/lint-staged.config.base.js deleted file mode 100644 index 37de289eb5..0000000000 --- a/lint-staged.config.base.js +++ /dev/null @@ -1,7 +0,0 @@ -const micromatch = require('micromatch'); - -module.exports = { - '*.{ts,tsx}': files => micromatch - .not(files, '**/__tests__/*.{ts,tsx}') - .map(file => 'npm run lint'), -}; diff --git a/package.json b/package.json index 1e5a033907..e18f01d307 100644 --- a/package.json +++ b/package.json @@ -1,30 +1,128 @@ { - "name": "ionic-cli", - "private": true, + "name": "ionic", + "version": "1.7.14", + "preferGlobal": true, + "description": "A tool for creating and developing Ionic Framework mobile apps.", + "homepage": "http://ionicframework.com/", + "bin": { + "ionic": "bin/ionic" + }, "scripts": { - "bootstrap": "lerna bootstrap && npm run build", - "clean": "lerna run clean", - "lint": "lerna run lint", - "lint:fix": "lerna run lint -- -- --fix", - "link": "lerna exec npm link", - "unlink": "lerna exec npm unlink", - "test": "lerna run test", - "build": "lerna run build", - "watch": "lerna run watch --parallel", - "docs": "node packages/cli-scripts/bin/ionic-cli-scripts docs", - "docs:watch": "chokidar 'packages/cli-scripts/dist/docs/**/*.js' -c 'npm run docs'", - "publish:testing": "lerna publish prerelease --preid=testing --exact --no-git-tag-version --no-push --dist-tag=testing", - "publish:ci": "lerna version -m 'chore(release): publish [skip ci]' --exact --conventional-commits --yes && lerna exec --no-private --since HEAD~ -- npm publish --provenance" + "bump": "node lib/tasks/bumpversion", + "beta-bump": "node lib/tasks/bumpversion --level pre --identifier beta", + "publish-release": "node lib/tasks/bumpversion --npmPublish", + "publish-tag": "node lib/tasks/bumpversion --level beta --npmPublishTag", + "full-release": "node lib/tasks/bumpversion --npmInstall --git --npmPublish", + "test": "npm run jasmine", + "e2e": "jasmine-node --captureExceptions ./e2e", + "jasmine": "jasmine-node --captureExceptions ./spec" }, - "devDependencies": { - "chokidar-cli": "^2.0.0", - "husky": "^4.2.0", - "lerna": "^3.13.3", - "typescript": "~4.8.0" + "keywords": [ + "ionic", + "ionic framework", + "ionicframework", + "mobile", + "app", + "hybrid", + "cordova", + "phonegap" + ], + "repository": { + "type": "git", + "url": "https://github.com/driftyco/ionic-cli.git" }, - "husky": { - "hooks": { - "pre-commit": "lerna exec --concurrency 1 --stream -- lint-staged" + "contributors": [ + { + "name": "Max Lynch", + "email": "max@drifty.com", + "web": "https://twitter.com/maxlynch" + }, + { + "name": "Peter Collins", + "email": "peter@drifty.com", + "web": "https://twitter.com/SomethingNew2_0" + }, + { + "name": "Adam Bradley", + "web": "https://twitter.com/adamdbradley" + }, + { + "name": "Josh Bavari", + "web": "https://twitter.com/jbavari", + "email": "josh@drifty.com" } - } + ], + "license": "MIT", + "dependencies": { + "async": "^0.9.0", + "cheerio": "^0.19.0", + "cli-table": "^0.3.1", + "colors": "0.6.2", + "connect": "3.1.1", + "connect-livereload": "0.5.2", + "crc": "3.2.1", + "cross-spawn": "0.2.3", + "event-stream": "3.0.x", + "expand-tilde": "^1.2.0", + "finalhandler": "0.2.0", + "form-data": "0.1.4", + "gulp": "3.8.8", + "ionic-app-lib": "0.7.0", + "moment": "^2.10.6", + "ncp": "0.4.2", + "npm": "2.1.3", + "open": "0.0.5", + "optimist": "0.6.0", + "progress": "1.1.7", + "prompt": "0.2.12", + "proxy-middleware": "^0.7.0", + "q": "1.0.1", + "request": "2.51.0", + "semver": "^4.2.0", + "serve-static": "1.7.1", + "shelljs": "0.2.6", + "tiny-lr-fork": "0.0.5", + "underscore": "~1.7.0", + "unzip": "0.1.9", + "vinyl-fs": "0.3.7", + "xml2js": "0.4.4" + }, + "devDependencies": { + "jasmine-node": "^1.14.5", + "rewire": "^2.3.4" + }, + "bundledDependencies": [ + "async", + "cheerio", + "cli-table", + "colors", + "connect", + "connect-livereload", + "crc", + "cross-spawn", + "event-stream", + "expand-tilde", + "finalhandler", + "form-data", + "gulp", + "ionic-app-lib", + "moment", + "ncp", + "npm", + "open", + "optimist", + "progress", + "prompt", + "proxy-middleware", + "q", + "request", + "semver", + "serve-static", + "shelljs", + "tiny-lr-fork", + "underscore", + "unzip", + "vinyl-fs", + "xml2js" + ] } diff --git a/packages/@ionic/cli-framework-output/.gitignore b/packages/@ionic/cli-framework-output/.gitignore deleted file mode 100644 index de4d1f007d..0000000000 --- a/packages/@ionic/cli-framework-output/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -dist -node_modules diff --git a/packages/@ionic/cli-framework-output/.npmrc b/packages/@ionic/cli-framework-output/.npmrc deleted file mode 100644 index 43c97e719a..0000000000 --- a/packages/@ionic/cli-framework-output/.npmrc +++ /dev/null @@ -1 +0,0 @@ -package-lock=false diff --git a/packages/@ionic/cli-framework-output/CHANGELOG.md b/packages/@ionic/cli-framework-output/CHANGELOG.md deleted file mode 100644 index ce325a9d25..0000000000 --- a/packages/@ionic/cli-framework-output/CHANGELOG.md +++ /dev/null @@ -1,158 +0,0 @@ -# Change Log - -All notable changes to this project will be documented in this file. -See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -## [2.2.8](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework-output@2.2.7...@ionic/cli-framework-output@2.2.8) (2023-12-19) - -**Note:** Version bump only for package @ionic/cli-framework-output - - - - - -## [2.2.7](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework-output@2.2.6...@ionic/cli-framework-output@2.2.7) (2023-11-07) - -**Note:** Version bump only for package @ionic/cli-framework-output - - - - - -## [2.2.6](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework-output@2.2.5...@ionic/cli-framework-output@2.2.6) (2023-03-29) - -**Note:** Version bump only for package @ionic/cli-framework-output - - - - - -## [2.2.5](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework-output@2.2.4...@ionic/cli-framework-output@2.2.5) (2022-06-16) - -**Note:** Version bump only for package @ionic/cli-framework-output - - - - - -## [2.2.4](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework-output@2.2.3...@ionic/cli-framework-output@2.2.4) (2022-05-09) - -**Note:** Version bump only for package @ionic/cli-framework-output - - - - - -## [2.2.3](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework-output@2.2.2...@ionic/cli-framework-output@2.2.3) (2022-03-04) - - -### Bug Fixes - -* correct time unit ([#4796](https://github.com/ionic-team/ionic-cli/issues/4796)) ([8f79cf2](https://github.com/ionic-team/ionic-cli/commit/8f79cf2069ff0b59a076ae9112fcef2dd84f8dc2)) - - - - - -## [2.2.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework-output@2.2.1...@ionic/cli-framework-output@2.2.2) (2020-09-29) - - -### Bug Fixes - -* add signal-exit dependency in correct place ([178e5e5](https://github.com/ionic-team/ionic-cli/commit/178e5e51cdc3593e3d096a5197e1dc0e17292bbd)) - - - - - -## [2.2.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework-output@2.2.0...@ionic/cli-framework-output@2.2.1) (2020-09-24) - -**Note:** Version bump only for package @ionic/cli-framework-output - - - - - -# [2.2.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework-output@2.1.0...@ionic/cli-framework-output@2.2.0) (2020-09-02) - - -### Bug Fixes - -* **formatter:** do not strip newlines when not titleizing ([3e38cd8](https://github.com/ionic-team/ionic-cli/commit/3e38cd891d0b914e03ea06d57cbb6ab067d43ac2)) - - -### Features - -* **output:** write method shortcut ([ca89ee9](https://github.com/ionic-team/ionic-cli/commit/ca89ee92a99a52bd4abd8a1cb97ba3087ec8c4e0)) - - - - - -# [2.1.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework-output@2.0.2...@ionic/cli-framework-output@2.1.0) (2020-08-29) - - -### Features - -* **logger:** add createDefaultLogger helper ([6961c33](https://github.com/ionic-team/ionic-cli/commit/6961c3377e88498d86eb0a39e5aef3f776fb3fd3)) -* **logger:** add tags option for tagged formatter ([6957104](https://github.com/ionic-team/ionic-cli/commit/695710462178f109d10504e6d9d8b0870eb0541c)) - - - - - -## [2.0.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework-output@2.0.1...@ionic/cli-framework-output@2.0.2) (2020-08-28) - -**Note:** Version bump only for package @ionic/cli-framework-output - - - - - -## [2.0.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework-output@2.0.0...@ionic/cli-framework-output@2.0.1) (2020-08-27) - - -### Bug Fixes - -* strip newlines when redrawing ([d13085f](https://github.com/ionic-team/ionic-cli/commit/d13085f8bdf372cd761f30ba97d9551721a26396)) - - - - - -# [2.0.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework-output@1.1.0...@ionic/cli-framework-output@2.0.0) (2020-08-27) - - -### Bug Fixes - -* **output:** newlines in stream output strategy ([4f9a7c9](https://github.com/ionic-team/ionic-cli/commit/4f9a7c988a0a63b21bf2a80eef065155c78545d0)) -* **output:** stream is optional ([df3e949](https://github.com/ionic-team/ionic-cli/commit/df3e949ebb092c92b84717a83bd662e283463e37)) - - -### Code Refactoring - -* **output:** replace log-update ([a90005c](https://github.com/ionic-team/ionic-cli/commit/a90005cd048a68252456da8409dedacaab54b505)) - - -### BREAKING CHANGES - -* **output:** `LogUpdateOutputStrategy` removed in favor of `TTYOutputStrategy` - - - - - -# [1.1.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework-output@1.0.1...@ionic/cli-framework-output@1.1.0) (2020-08-26) - - -### Features - -* **output:** show elapsed time for tasks ([e4525f7](https://github.com/ionic-team/ionic-cli/commit/e4525f79b549b66e25e6c01297ccc77cc6c85250)) - - - - - -## 1.0.1 (2020-08-25) - -**Note:** Version bump only for package @ionic/cli-framework-output diff --git a/packages/@ionic/cli-framework-output/LICENSE b/packages/@ionic/cli-framework-output/LICENSE deleted file mode 100644 index 62cadf753a..0000000000 --- a/packages/@ionic/cli-framework-output/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2019 Drifty Co - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/packages/@ionic/cli-framework-output/README.md b/packages/@ionic/cli-framework-output/README.md deleted file mode 100644 index 178c1517d2..0000000000 --- a/packages/@ionic/cli-framework-output/README.md +++ /dev/null @@ -1 +0,0 @@ -# @ionic/cli-framework-output diff --git a/packages/@ionic/cli-framework-output/jest.config.js b/packages/@ionic/cli-framework-output/jest.config.js deleted file mode 100644 index eace8d6d9d..0000000000 --- a/packages/@ionic/cli-framework-output/jest.config.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('../../../jest.config.base'); diff --git a/packages/@ionic/cli-framework-output/lint-staged.config.js b/packages/@ionic/cli-framework-output/lint-staged.config.js deleted file mode 100644 index 5815a72f0d..0000000000 --- a/packages/@ionic/cli-framework-output/lint-staged.config.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('../../../lint-staged.config.base'); diff --git a/packages/@ionic/cli-framework-output/package.json b/packages/@ionic/cli-framework-output/package.json deleted file mode 100644 index 76bab15ccb..0000000000 --- a/packages/@ionic/cli-framework-output/package.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "name": "@ionic/cli-framework-output", - "version": "2.2.8", - "description": "The log/tasks/spinners portion of Ionic CLI Framework", - "homepage": "https://ionicframework.com/", - "author": "Ionic Team (https://ionicframework.com)", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "engines": { - "node": ">=16.0.0" - }, - "files": [ - "dist/", - "LICENSE", - "README.md" - ], - "repository": { - "type": "git", - "url": "https://github.com/ionic-team/ionic-cli.git" - }, - "bugs": { - "url": "https://github.com/ionic-team/ionic-cli/issues" - }, - "scripts": { - "clean": "rimraf dist", - "lint": "true", - "build": "npm run clean && tsc", - "watch": "tsc -w --preserveWatchOutput", - "test": "jest --maxWorkers=4", - "prepublishOnly": "npm run build" - }, - "license": "MIT", - "dependencies": { - "@ionic/utils-terminal": "2.3.5", - "debug": "^4.0.0", - "tslib": "^2.0.1" - }, - "devDependencies": { - "@ionic/utils-stream": "3.1.7", - "@types/debug": "^4.1.1", - "@types/inquirer": "0.0.43", - "@types/jest": "^26.0.10", - "@types/node": "~16.0.0", - "jest": "^26.4.2", - "jest-cli": "^26.0.1", - "lint-staged": "^10.0.2", - "ts-jest": "~26.3.0", - "typescript": "~4.8.0" - } -} diff --git a/packages/@ionic/cli-framework-output/src/__tests__/logger.ts b/packages/@ionic/cli-framework-output/src/__tests__/logger.ts deleted file mode 100644 index 12b9e38c72..0000000000 --- a/packages/@ionic/cli-framework-output/src/__tests__/logger.ts +++ /dev/null @@ -1,396 +0,0 @@ -import { WritableStreamBuffer } from '@ionic/utils-stream'; -import { stripAnsi, wordWrap } from '@ionic/utils-terminal'; - -import { LOGGER_LEVELS, Logger, LoggerHandler, StreamHandler, createPrefixedFormatter, createTaggedFormatter } from '../logger'; - -describe('@ionic/cli-framework-output', () => { - - describe('logger', () => { - - describe('Logger', () => { - - describe('clone', () => { - - it('should clone the base set of options', () => { - const logger1 = new Logger(); - const logger2 = logger1.clone(); - expect(logger1.level).toEqual(logger2.level); - expect(logger1.handlers).not.toBe(logger2.handlers); - expect(logger2.handlers.size).toEqual(2); - - for (const handler of logger2.handlers) { - expect(handler).toBeInstanceOf(StreamHandler); - } - }); - - it('should clone the set of option overrides', () => { - const logger1 = new Logger(); - const level = 15; - const handlers: Set = new Set(); - const logger2 = logger1.clone({ level, handlers }); - expect(logger2.level).not.toEqual(logger1.level); - expect(logger2.level).toEqual(level); - expect(logger2.handlers).not.toEqual(logger1.handlers); - expect(logger2.handlers).toBe(handlers); - }); - - }); - - describe('msg', () => { - - let stream: WritableStreamBuffer; - let logger: Logger; - - beforeEach(() => { - stream = new WritableStreamBuffer(); - logger = new Logger({ handlers: new Set([new StreamHandler({ stream })]) }); - }); - - it('should write the message directly', () => { - logger.msg('hi'); - expect(stream.consume().toString()).toEqual('hi\n'); - }); - - }); - - describe('nl', () => { - - let stream: WritableStreamBuffer; - let logger: Logger; - - beforeEach(() => { - stream = new WritableStreamBuffer(); - logger = new Logger({ handlers: new Set([new StreamHandler({ stream })]) }); - }); - - it('should log for defaults', () => { - logger.nl(); - expect(stream.consume().toString()).toEqual('\n'); - }); - - it('should log multiple newlines', () => { - logger.nl(5); - expect(stream.consume().toString()).toEqual('\n'.repeat(5)); - }); - - }); - - describe('log', () => { - - let stream: WritableStreamBuffer; - let handler: StreamHandler; - let logger: Logger; - - beforeEach(() => { - stream = new WritableStreamBuffer(); - handler = new StreamHandler({ stream }); - logger = new Logger({ handlers: new Set([handler]) }); - }); - - it('should log for defaults', () => { - logger.log({ logger, msg: 'hi' }); - expect(stream.consume().toString()).toEqual('hi\n'); - }); - - it('should log with formatter', () => { - logger = new Logger({ handlers: new Set([new StreamHandler({ stream, formatter: record => record.msg.split('').reverse().join('') })]) }); - logger.log({ logger, msg: 'hello world!' }); - expect(stream.consume().toString()).toEqual('!dlrow olleh\n'); - }); - - it('should log with levels equal', () => { - logger.level = 20; - logger.log({ logger, msg: 'hi', level: 20 }); - expect(stream.consume().toString()).toEqual('hi\n'); - }); - - it('should log with record level not set', () => { - logger.level = 10; - logger.log({ logger, msg: 'hi' }); - expect(stream.consume().toString()).toEqual('hi\n'); - }); - - it('should not log when logger level exceeds record level', () => { - logger.level = 20; - logger.log({ logger, msg: 'hi', level: 10 }); - expect(stream.consume().toString()).toEqual(''); - }); - - }); - - describe('debug', () => { - - let stream: WritableStreamBuffer; - let handler: StreamHandler; - let logger: Logger; - - beforeEach(() => { - stream = new WritableStreamBuffer(); - handler = new StreamHandler({ stream }); - logger = new Logger({ handlers: new Set([handler]) }); - }); - - it('should not log debug messages by default', () => { - logger.debug('hi'); - expect(stream.consume().toString()).toEqual(''); - }); - - it('should log debug messages with adjusted level', () => { - logger.level = 0; - logger.debug('hi'); - expect(stream.consume().toString()).toEqual('hi\n'); - }); - - }); - - describe('info', () => { - - let stream: WritableStreamBuffer; - let handler: StreamHandler; - let logger: Logger; - - beforeEach(() => { - stream = new WritableStreamBuffer(); - handler = new StreamHandler({ stream }); - logger = new Logger({ handlers: new Set([handler]) }); - }); - - it('should write the message', () => { - logger.info('hi'); - expect(stream.consume().toString()).toEqual('hi\n'); - }); - - it('should not write the message at a higher level', () => { - logger.level = 30; - logger.info('hi'); - expect(stream.consume().toString()).toEqual(''); - }); - - }); - - describe('warn', () => { - - let stream: WritableStreamBuffer; - let handler: StreamHandler; - let logger: Logger; - - beforeEach(() => { - stream = new WritableStreamBuffer(); - handler = new StreamHandler({ stream }); - logger = new Logger({ handlers: new Set([handler]) }); - }); - - it('should write the message', () => { - logger.warn('hi'); - expect(stream.consume().toString()).toEqual('hi\n'); - }); - - it('should not write the message at a higher level', () => { - logger.level = 40; - logger.warn('hi'); - expect(stream.consume().toString()).toEqual(''); - }); - - }); - - describe('error', () => { - - let stream: WritableStreamBuffer; - let handler: StreamHandler; - let logger: Logger; - - beforeEach(() => { - stream = new WritableStreamBuffer(); - handler = new StreamHandler({ stream }); - logger = new Logger({ handlers: new Set([handler]) }); - }); - - it('should write the message', () => { - logger.error('hi'); - expect(stream.consume().toString()).toEqual('hi\n'); - }); - - it('should not write the message at a higher level', () => { - logger.level = 50; - logger.error('hi'); - expect(stream.consume().toString()).toEqual(''); - }); - - }); - - describe('createWriteStream', () => { - - it('should create a writable stream', () => { - const stream = new WritableStreamBuffer(); - const handler = new StreamHandler({ stream }); - const logger = new Logger({ handlers: new Set([handler]) }); - const ws = logger.createWriteStream(); - ws.write('hi'); - expect(stream.consume().toString()).toEqual('hi\n'); - }); - - }); - - }); - - describe('createTaggedFormatter', () => { - - let stream: WritableStreamBuffer; - let handler: StreamHandler; - let logger: Logger; - - beforeEach(() => { - stream = new WritableStreamBuffer(); - handler = new StreamHandler({ stream }); - logger = new Logger({ handlers: new Set([handler]) }); - }); - - it('should not format if requested', () => { - const formatter = createTaggedFormatter(); - const result = formatter({ msg: 'hi', level: LOGGER_LEVELS.INFO, format: false, logger }); - expect(result).toEqual('hi'); - }); - - it('should not tag non-leveled outputs', () => { - const formatter = createTaggedFormatter(); - const result = formatter({ msg: 'hi', logger }); - expect(result).toEqual('hi'); - }); - - it('should log multi-line message properly for non-leveled outputs', () => { - const formatter = createTaggedFormatter({ wrap: false }); - const result = formatter({ msg: 'hello world!\nThis is a message.', logger }); - expect(result).toEqual('hello world!\nThis is a message.'); - }); - - it('should log multi-line message properly for wrapped, non-leveled outputs', () => { - const formatter = createTaggedFormatter({ wrap: true }); - const result = formatter({ msg: 'hello world!\nThis is a message.', logger }); - expect(result).toEqual('hello world!\nThis is a message.'); - }); - - it('should tag leveled outputs', () => { - const formatter = createTaggedFormatter(); - const result = formatter({ msg: 'hi', level: LOGGER_LEVELS.INFO, logger }); - expect(stripAnsi(result)).toEqual('[INFO] hi'); - }); - - it('should not wrap by default', () => { - const formatter = createTaggedFormatter(); - const msg = 'A '.repeat(1000); - const result = formatter({ msg, level: LOGGER_LEVELS.INFO, logger }); - expect(stripAnsi(result)).toEqual(`[INFO] ${msg}`); - }); - - it('should wrap words if wanted', () => { - const wordWrapOpts = { width: 50 }; - const formatter = createTaggedFormatter({ wrap: wordWrapOpts }); - const msg = 'A '.repeat(1000); - const result = formatter({ msg, level: LOGGER_LEVELS.INFO, logger }); - expect(stripAnsi(result)).toEqual(`[INFO] ${wordWrap(msg, { indentation: 7, ...wordWrapOpts })}`); - }); - - it('should not titleize by default', () => { - const formatter = createTaggedFormatter(); - const result = formatter({ msg: `Hello!\nThis is a message.\nHere's another.`, level: LOGGER_LEVELS.INFO, logger }); - expect(stripAnsi(result)).toEqual(`[INFO] Hello!\n This is a message.\n Here's another.`); - }); - - it('should not titleize for single line', () => { - const formatter = createTaggedFormatter({ titleize: true }); - const result = formatter({ msg: 'Hello!', level: LOGGER_LEVELS.INFO, logger }); - expect(stripAnsi(result)).toEqual(`[INFO] Hello!`); - }); - - it('should titleize properly for double newline after first line', () => { - const formatter = createTaggedFormatter({ titleize: true }); - const result = formatter({ msg: `Hello!\n\nThis is a message.\n\nHere's another.`, level: LOGGER_LEVELS.INFO, logger }); - expect(stripAnsi(result)).toEqual(`[INFO] Hello!\n\n This is a message.\n\n Here's another.`); - }); - - it('should titleize if wanted', () => { - const formatter = createTaggedFormatter({ titleize: true }); - const result = formatter({ msg: `Hello!\nThis is a message.\nHere's another.`, level: LOGGER_LEVELS.INFO, logger }); - expect(stripAnsi(result)).toEqual(`[INFO] Hello!\n\n This is a message.\n Here's another.`); - }); - - it('should strip the first newlines with a titleized message', () => { - const formatter = createTaggedFormatter({ titleize: true }); - const result = formatter({ msg: 'hello world!\n\n\nThis is a message.', logger }); - expect(result).toEqual('hello world!\n\nThis is a message.'); - }); - - it('should not strip the first newlines with a non-titleized message', () => { - const formatter = createTaggedFormatter({ titleize: false }); - const result = formatter({ msg: 'hello world!\n\n\nThis is a message.', logger }); - expect(result).toEqual('hello world!\n\n\nThis is a message.'); - }); - - it('should work with wrap and titleize', () => { - const wordWrapOpts = { width: 50 }; - const formatter = createTaggedFormatter({ titleize: true, wrap: wordWrapOpts }); - const msg = 'A '.repeat(1000); - const result = formatter({ msg, level: LOGGER_LEVELS.INFO, logger }); - expect(stripAnsi(result)).toEqual(`[INFO] ${wordWrap(msg, { indentation: 7, ...wordWrapOpts })}`); - }); - - it('should prefix single line without level', () => { - const now = new Date().toISOString(); - const formatter = createTaggedFormatter({ prefix: `[${now}]` }); - const result = formatter({ msg: 'hello world!', logger }); - expect(result).toEqual(`[${now}] hello world!`); - }); - - it('should prefix dynamically with a function', () => { - let count = 0; - const spy = jest.fn(() => `[${++count}]`); - const formatter = createTaggedFormatter({ prefix: spy }); - const result1 = formatter({ msg: 'hello world!', logger }); - const result2 = formatter({ msg: 'hello world!', logger }); - expect(result1).toEqual('[1] hello world!'); - expect(result2).toEqual('[2] hello world!'); - expect(spy).toHaveBeenCalledTimes(2); - }); - - }); - - describe('createPrefixedFormatter', () => { - - let stream: WritableStreamBuffer; - let handler: StreamHandler; - let logger: Logger; - - beforeEach(() => { - stream = new WritableStreamBuffer(); - handler = new StreamHandler({ stream }); - logger = new Logger({ handlers: new Set([handler]) }); - }); - - it('should not format if requested', () => { - const formatter = createPrefixedFormatter('[prefix]'); - const result = formatter({ msg: 'hello world!', format: false, logger }); - expect(result).toEqual('hello world!'); - }); - - it('should prefix message', () => { - const formatter = createPrefixedFormatter('[prefix]'); - const result = formatter({ msg: 'hello world!', logger }); - expect(result).toEqual('[prefix] hello world!'); - }); - - it('should prefix dynamically with a function', () => { - let count = 0; - const spy = jest.fn(() => `[${++count}]`); - const formatter = createPrefixedFormatter(spy); - const result1 = formatter({ msg: 'hello world!', logger }); - const result2 = formatter({ msg: 'hello world!', logger }); - expect(result1).toEqual('[1] hello world!'); - expect(result2).toEqual('[2] hello world!'); - expect(spy).toHaveBeenCalledTimes(2); - }); - - }); - - }); - -}); diff --git a/packages/@ionic/cli-framework-output/src/__tests__/tasks.ts b/packages/@ionic/cli-framework-output/src/__tests__/tasks.ts deleted file mode 100644 index ed29a211c3..0000000000 --- a/packages/@ionic/cli-framework-output/src/__tests__/tasks.ts +++ /dev/null @@ -1,292 +0,0 @@ -import { Task, TaskChain } from '../tasks'; - -describe('@ionic/cli-framework-output', () => { - - describe('tasks', () => { - - describe('Task', () => { - - let task: Task; - let handlers: { - success: jest.Mock; - failure: jest.Mock; - clear: jest.Mock; - tick: jest.Mock; - end: jest.Mock; - }; - - beforeEach(() => { - jest.useFakeTimers(); - task = new Task({ msg: '' }); - handlers = { - success: jest.fn(), - failure: jest.fn(), - clear: jest.fn(), - tick: jest.fn(), - end: jest.fn(), - }; - task.on('success', handlers.success); - task.on('failure', handlers.failure); - task.on('clear', handlers.clear); - task.on('tick', handlers.tick); - task.on('end', handlers.end); - }); - - afterEach(() => { - jest.clearAllTimers(); - }); - - it('should have an empty msg from construction', () => { - expect(task.msg).toEqual(''); - }); - - it('should tick when msg is set', () => { - const msg = 'hello world!'; - task.msg = msg; - expect(handlers.tick).toHaveBeenCalledTimes(1); - handlers.tick.mockReset(); - expect(task.msg).toEqual(msg); - expect(handlers.tick).not.toHaveBeenCalled(); - }); - - it('should allow start to be called more than once', () => { - expect(task.running).toEqual(false); - task.start(); - expect(task.running).toEqual(true); - expect(handlers.tick).not.toHaveBeenCalled(); - task.start(); - expect(task.running).toEqual(true); - expect(handlers.tick).not.toHaveBeenCalled(); - }); - - it('should end with proper events', () => { - expect(handlers.tick).not.toHaveBeenCalled(); - expect(handlers.clear).not.toHaveBeenCalled(); - expect(handlers.end).not.toHaveBeenCalled(); - task.end(); - expect(handlers.tick).toHaveBeenCalledTimes(1); - expect(handlers.clear).toHaveBeenCalledTimes(1); - expect(handlers.end).toHaveBeenCalledTimes(1); - expect(handlers.success).not.toHaveBeenCalled(); - expect(handlers.failure).not.toHaveBeenCalled(); - }); - - it('should ignore succeed if not started', () => { - expect(task.running).toEqual(false); - task.succeed(); - expect(task.running).toEqual(false); - expect(handlers.tick).not.toHaveBeenCalled(); - expect(handlers.clear).not.toHaveBeenCalled(); - expect(handlers.end).not.toHaveBeenCalled(); - expect(handlers.success).not.toHaveBeenCalled(); - expect(handlers.failure).not.toHaveBeenCalled(); - }); - - it('should ignore fail if not started', () => { - expect(task.running).toEqual(false); - task.fail(); - expect(task.running).toEqual(false); - expect(handlers.tick).not.toHaveBeenCalled(); - expect(handlers.clear).not.toHaveBeenCalled(); - expect(handlers.end).not.toHaveBeenCalled(); - expect(handlers.success).not.toHaveBeenCalled(); - expect(handlers.failure).not.toHaveBeenCalled(); - }); - - it('should succeed with proper events', () => { - task.start(); - expect(task.running).toEqual(true); - task.succeed(); - expect(task.running).toEqual(false); - expect(handlers.tick).toHaveBeenCalledTimes(1); - expect(handlers.clear).toHaveBeenCalledTimes(1); - expect(handlers.end).toHaveBeenCalledTimes(1); - expect(handlers.success).toHaveBeenCalledTimes(1); - expect(handlers.failure).not.toHaveBeenCalled(); - }); - - it('should fail with proper events', () => { - task.start(); - expect(task.running).toEqual(true); - task.fail(); - expect(task.running).toEqual(false); - expect(handlers.tick).toHaveBeenCalledTimes(1); - expect(handlers.clear).toHaveBeenCalledTimes(1); - expect(handlers.end).toHaveBeenCalledTimes(1); - expect(handlers.success).not.toHaveBeenCalled(); - expect(handlers.failure).toHaveBeenCalledTimes(1); - }); - - describe('intervaled', () => { - - const tickInterval = 50; - - beforeEach(() => { - task.tickInterval = tickInterval; - }); - - it('should not set intervalId twice', () => { - expect(task.running).toEqual(false); - expect(task.intervalId).toBeUndefined(); - task.start(); - expect(task.running).toEqual(true); - expect(task.intervalId).toBeDefined(); - const firstIntervalId = task.intervalId; - task.start(); - expect(task.running).toEqual(true); - expect(task.intervalId).toBeDefined(); - expect(task.intervalId).toBe(firstIntervalId); - }); - - it('should clear interval properly', () => { - task.start(); - const intervalId = task.intervalId; - expect(task.intervalId).toBeDefined(); - task.clear(); - expect(handlers.clear).toHaveBeenCalledTimes(1); - expect(task.intervalId).toBeUndefined(); - expect(clearInterval).toHaveBeenLastCalledWith(intervalId); - }); - - it('should tick appropriately for success case', () => { - const msg = 'hello world!'; - task.start(); - expect(setInterval).toHaveBeenCalledTimes(1); - expect(setInterval).toHaveBeenLastCalledWith(expect.any(Function), tickInterval); - expect(handlers.tick).not.toHaveBeenCalled(); - task.msg = msg; - expect(handlers.tick).toHaveBeenCalledTimes(1); - jest.advanceTimersByTime(100); - expect(handlers.tick).toHaveBeenCalledTimes(3); - task.succeed(); - expect(handlers.tick).toHaveBeenCalledTimes(4); - }); - - }); - - }); - - describe('TaskChain', () => { - - let chain: TaskChain; - let createTaskSpy: jest.SpyInstance; - let handlers: { - end: jest.Mock; - failure: jest.Mock; - next: jest.Mock; - }; - - beforeEach(() => { - chain = new TaskChain(); - createTaskSpy = jest.spyOn(chain, 'createTask'); - handlers = { - end: jest.fn(), - failure: jest.fn(), - next: jest.fn(), - }; - chain.on('end', handlers.end); - chain.on('failure', handlers.failure); - chain.on('next', handlers.next); - }); - - it('should set current task upon first next', () => { - expect((chain as any).current).toBeUndefined(); - const msg = 'hello world!'; - const task = chain.next(msg); - expect(task.msg).toEqual(msg); - expect(task.running).toEqual(true); - expect((chain as any).current).toBe(task); - }); - - it('should remove current task when complete', () => { - expect((chain as any).current).toBeUndefined(); - const task = chain.next('hello world!'); - const spy = jest.fn(); - task.on('success', spy); - expect((chain as any).current).toBe(task); - chain.end(); - expect((chain as any).current).toBeUndefined(); - expect(spy).toHaveBeenCalledTimes(1); - }); - - it('should fail current task', () => { - expect((chain as any).current).toBeUndefined(); - const task = chain.next('hello world!'); - const spy = jest.fn(); - task.on('failure', spy); - expect((chain as any).current).toBe(task); - chain.fail(); - expect((chain as any).current).toBe(task); - expect(spy).toHaveBeenCalledTimes(1); - }); - - it('should pass msg to create task function', () => { - const msg = 'hello world!'; - chain.next(msg); - expect(createTaskSpy).toHaveBeenCalledTimes(1); - expect(createTaskSpy).toHaveBeenLastCalledWith({ msg }); - }); - - it('should cleanup tasks', () => { - const task1 = chain.next('hello world!'); - const task2 = chain.next('hello world!'); - const clearSpy1 = jest.fn(); - const failureSpy1 = jest.fn(); - const clearSpy2 = jest.fn(); - const failureSpy2 = jest.fn(); - task1.on('clear', clearSpy1); - task1.on('failure', failureSpy1); - task2.on('clear', clearSpy2); - task2.on('failure', failureSpy2); - chain.cleanup(); - expect(failureSpy1).not.toHaveBeenCalled(); - expect(clearSpy1).toHaveBeenCalledTimes(1); - expect(failureSpy2).toHaveBeenCalledTimes(1); - expect(clearSpy2).toHaveBeenCalledTimes(1); - }); - - it('should call end event with no task for task-list chain', () => { - expect(handlers.end).not.toHaveBeenCalled(); - chain.end(); - expect(handlers.end).toHaveBeenCalledTimes(1); - expect(handlers.end).toHaveBeenCalledWith(undefined); - }); - - it('should call end event with a task', () => { - const task = chain.next('hello world!'); - expect(handlers.end).not.toHaveBeenCalled(); - chain.end(); - expect(handlers.end).toHaveBeenCalledTimes(1); - expect(handlers.end).toHaveBeenCalledWith(task); - }); - - it('should call failure event with no task for task-list chain', () => { - expect(handlers.failure).not.toHaveBeenCalled(); - chain.fail(); - expect(handlers.failure).toHaveBeenCalledTimes(1); - expect(handlers.failure).toHaveBeenCalledWith(undefined); - }); - - it('should call failure event with a task', () => { - const task = chain.next('hello world!'); - expect(handlers.failure).not.toHaveBeenCalled(); - chain.fail(); - expect(handlers.failure).toHaveBeenCalledTimes(1); - expect(handlers.failure).toHaveBeenCalledWith(task); - }); - - it('should call next event', () => { - expect(handlers.next).not.toHaveBeenCalled(); - const task1 = chain.next('hello world!'); - expect(handlers.next).toHaveBeenCalledTimes(1); - expect(handlers.next).toHaveBeenCalledWith(task1); - const task2 = chain.next('hello world!'); - expect(handlers.next).toHaveBeenCalledTimes(2); - expect(handlers.next).toHaveBeenCalledWith(task2); - }); - - }); - - }); - -}); diff --git a/packages/@ionic/cli-framework-output/src/__tests__/utils.ts b/packages/@ionic/cli-framework-output/src/__tests__/utils.ts deleted file mode 100644 index dab6028ce5..0000000000 --- a/packages/@ionic/cli-framework-output/src/__tests__/utils.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { dropWhile, enforceLF } from '../utils'; - -describe('@ionic/cli-framework-output', () => { - - describe('utils', () => { - - describe('dropWhile', () => { - - it('should not return the original array', () => { - const input: string[] = []; - const result = dropWhile(input); - expect(result).not.toBe(input); - expect(result).toEqual([]); - }); - - it('should filter truthy with default predicate', () => { - const result = dropWhile([1, 2, 3]); - expect(result).toEqual([]); - }); - - it('should filter truthy elements with default predicate', () => { - const result = dropWhile([2, 1, 0]); - expect(result).toEqual([0]); - }); - - it('should only filter truthy elements from the beginning with default predicate', () => { - const result = dropWhile([1, 0, 1]); - expect(result).toEqual([0, 1]); - }); - - it('should only filter elements from the beginning', () => { - const result = dropWhile(['a', 'b', 'c', 'b', 'a'], item => item === 'a'); - expect(result).toEqual(['b', 'c', 'b', 'a']); - }); - - it('should filter elements from the beginning by type', () => { - const result = dropWhile(['a', 'b', 0, 'c', 'd', 1], item => typeof item === 'string'); - expect(result).toEqual([0, 'c', 'd', 1]); - }); - - }); - - describe('enforceLF', () => { - - it('should convert empty string to newline', () => { - const result = enforceLF(''); - expect(result).toBe('\n'); - }); - - it('should keep multiple newlines', () => { - const result = enforceLF('\n\n\n'); - expect(result).toBe('\n\n\n'); - }); - - it('should do nothing to a single newline', () => { - const result = enforceLF('\n'); - expect(result).toBe('\n'); - }); - - it('should add newline to text without newline', () => { - const result = enforceLF('some text'); - expect(result).toBe('some text\n'); - }); - - it('should do nothing to multiline text with newline at end', () => { - const result = enforceLF('some text\nsome more\n'); - expect(result).toBe('some text\nsome more\n'); - }); - - it('should add newline to multiline text without newline at end', () => { - const result = enforceLF('some text\nsome more'); - expect(result).toBe('some text\nsome more\n'); - }); - - }); - - }); - -}); diff --git a/packages/@ionic/cli-framework-output/src/colors.ts b/packages/@ionic/cli-framework-output/src/colors.ts deleted file mode 100644 index d73107d4d9..0000000000 --- a/packages/@ionic/cli-framework-output/src/colors.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { LoggerLevel } from './logger'; -import { identity } from './utils'; - -export type ColorFunction = (...text: string[]) => string; -export type LoggerColors = { [L in LoggerLevel]: ColorFunction; }; - -export interface Colors { - /** - * Used to mark text as important. Comparable to HTML's . - */ - strong: ColorFunction; - - /** - * Used to mark text as less important. - */ - weak: ColorFunction; - - /** - * Used to mark text as input such as commands, inputs, options, etc. - */ - input: ColorFunction; - - /** - * Used to mark text as successful. - */ - success: ColorFunction; - - /** - * Used to mark text as failed. - */ - failure: ColorFunction; - - /** - * Used to mark text as ancillary or supportive. - */ - ancillary: ColorFunction; - - log: LoggerColors; -} - -export const NO_COLORS: Colors = Object.freeze({ - strong: identity, - weak: identity, - input: identity, - success: identity, - failure: identity, - ancillary: identity, - log: Object.freeze({ - DEBUG: identity, - INFO: identity, - WARN: identity, - ERROR: identity, - }), -}); diff --git a/packages/@ionic/cli-framework-output/src/index.ts b/packages/@ionic/cli-framework-output/src/index.ts deleted file mode 100644 index 1f7da8dadc..0000000000 --- a/packages/@ionic/cli-framework-output/src/index.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { TTY_WIDTH, indent, sliceAnsi, stringWidth, stripAnsi, wordWrap } from '@ionic/utils-terminal'; - -import { NO_COLORS } from './colors'; -import { Logger, createTaggedFormatter, StreamHandler, CreateTaggedFormatterOptions } from './logger'; -import { OutputStrategy, StreamOutputStrategy } from './output'; - -export { TTY_WIDTH, indent, sliceAnsi, stringWidth, stripAnsi, wordWrap }; - -export * from './colors'; -export * from './logger'; -export * from './output'; -export * from './tasks'; - -export interface CreateDefaultLoggerOptions { - /** - * Specify a custom output strategy to use for the logger. - * - * By default, the logger uses a output strategy with process.stdout and no colors. - */ - output?: OutputStrategy; - - /** - * Specify custom logger formatter options. - */ - formatterOptions?: CreateTaggedFormatterOptions; -} - -/** - * Creates a logger instance with good defaults. - */ -export function createDefaultLogger({ output = new StreamOutputStrategy({ colors: NO_COLORS, stream: process.stdout }), formatterOptions }: CreateDefaultLoggerOptions = {}): Logger { - const { weak } = output.colors; - const prefix = process.argv.includes('--log-timestamps') ? () => `${weak('[' + new Date().toISOString() + ']')}` : ''; - const formatter = createTaggedFormatter({ colors: output.colors, prefix, titleize: true, wrap: true, ...formatterOptions }); - const handlers = new Set([new StreamHandler({ stream: output.stream, formatter })]); - - return new Logger({ handlers }); -} diff --git a/packages/@ionic/cli-framework-output/src/logger.ts b/packages/@ionic/cli-framework-output/src/logger.ts deleted file mode 100644 index 2dd98d0fb7..0000000000 --- a/packages/@ionic/cli-framework-output/src/logger.ts +++ /dev/null @@ -1,281 +0,0 @@ -import { WordWrapOptions, stringWidth, wordWrap } from '@ionic/utils-terminal'; -import { Writable } from 'stream'; -import * as util from 'util'; - -import { ColorFunction, Colors, NO_COLORS } from './colors'; -import { dropWhile, enforceLF } from './utils'; - -export interface LogRecord { - msg: string; - logger: Logger; - level?: LoggerLevelWeight; - format?: boolean; -} - -export type LoggerLevel = 'DEBUG' | 'INFO' | 'WARN' | 'ERROR'; -export type LoggerLevelWeight = number; -export type LoggerFormatter = (record: LogRecord) => string; - -export const LOGGER_LEVELS: { readonly [L in LoggerLevel]: LoggerLevelWeight; } = Object.freeze({ - DEBUG: 10, - INFO: 20, - WARN: 30, - ERROR: 40, -}); - -export const LOGGER_LEVEL_NAMES: ReadonlyMap = new Map([ - [LOGGER_LEVELS.DEBUG, 'DEBUG'], - [LOGGER_LEVELS.INFO, 'INFO'], - [LOGGER_LEVELS.WARN, 'WARN'], - [LOGGER_LEVELS.ERROR, 'ERROR'], -]); - -export function getLoggerLevelName(level?: LoggerLevelWeight): LoggerLevel | undefined { - if (level) { - const levelName = LOGGER_LEVEL_NAMES.get(level); - - if (levelName) { - return levelName; - } - } -} - -export function getLoggerLevelColor(colors: Colors, level?: LoggerLevelWeight): ColorFunction | undefined { - const levelName = getLoggerLevelName(level); - - if (levelName) { - return colors.log[levelName]; - } -} - -export interface LoggerHandler { - formatter?: LoggerFormatter; - clone(): LoggerHandler; - handle(record: LogRecord): void; -} - -export interface StreamHandlerOptions { - readonly stream: NodeJS.WritableStream; - readonly filter?: (record: LogRecord) => boolean; - readonly formatter?: LoggerFormatter; -} - -export class StreamHandler implements LoggerHandler { - readonly stream: NodeJS.WritableStream; - readonly filter?: (record: LogRecord) => boolean; - formatter?: LoggerFormatter; - - constructor({ stream, filter, formatter }: StreamHandlerOptions) { - this.stream = stream; - this.filter = filter; - this.formatter = formatter; - } - - clone(opts?: Partial): StreamHandler { - const { stream, filter, formatter } = this; - return new StreamHandler({ stream, filter, formatter, ...opts }); - } - - handle(record: LogRecord): void { - if (this.filter && !this.filter(record)) { - return; - } - - const msg = this.formatter && record.format !== false ? this.formatter(record) : record.msg; - this.stream.write(enforceLF(msg)); - } -} - -const stdoutLogRecordFilter = (record: LogRecord) => !record.level || record.level === LOGGER_LEVELS.INFO; -const stderrLogRecordFilter = (record: LogRecord) => !!record.level && record.level !== LOGGER_LEVELS.INFO; - -export const DEFAULT_LOGGER_HANDLERS: ReadonlySet = new Set([ - new StreamHandler({ stream: process.stdout, filter: stdoutLogRecordFilter }), - new StreamHandler({ stream: process.stderr, filter: stderrLogRecordFilter }), -]); - -export interface LoggerOptions { - readonly handlers?: Set; - readonly level?: LoggerLevelWeight; -} - -export class Logger { - handlers: Set; - level: LoggerLevelWeight; - - constructor({ level = LOGGER_LEVELS.INFO, handlers }: LoggerOptions = {}) { - this.level = level; - this.handlers = handlers ? handlers : Logger.cloneHandlers(DEFAULT_LOGGER_HANDLERS); - } - - static cloneHandlers(handlers: ReadonlySet): Set { - return new Set([...handlers].map(handler => handler.clone())); - } - - /** - * Clone this logger, optionally overriding logger options. - * - * @param opts Logger options to override from this logger. - */ - clone(opts: Partial = {}): Logger { - const { level, handlers } = this; - return new Logger({ level, handlers: Logger.cloneHandlers(handlers), ...opts }); - } - - /** - * Log a message as-is. - * - * @param msg The string to log. - */ - msg(msg: string): void { - this.log(this.createRecord(msg)); - } - - /** - * Log a message using the `debug` logger level. - * - * @param msg The string to log. - */ - debug(msg: string): void { - this.log(this.createRecord(msg, LOGGER_LEVELS.DEBUG)); - } - - /** - * Log a message using the `info` logger level. - * - * @param msg The string to log. - */ - info(msg: string): void { - this.log(this.createRecord(msg, LOGGER_LEVELS.INFO)); - } - - /** - * Log a message using the `warn` logger level. - * - * @param msg The string to log. - */ - warn(msg: string): void { - this.log(this.createRecord(msg, LOGGER_LEVELS.WARN)); - } - - /** - * Log a message using the `error` logger level. - * - * @param msg The string to log. - */ - error(msg: string): void { - this.log(this.createRecord(msg, LOGGER_LEVELS.ERROR)); - } - - createRecord(msg: string, level?: LoggerLevelWeight, format?: boolean): LogRecord { - return { - // If the logger is used to quickly print something, let's pretty-print - // it into a string. - msg: util.format(msg), - level, - logger: this, - format, - }; - } - - /** - * Log newlines using a logger output found via `level`. - * - * @param num The number of newlines to log. - * @param level The logger level. If omitted, the default output is used. - */ - nl(num = 1, level?: LoggerLevelWeight): void { - this.log({ ...this.createRecord('\n'.repeat(num), level), format: false }); - } - - /** - * Log a record using a logger output found via `level`. - */ - log(record: LogRecord): void { - if (typeof record.level === 'number' && this.level > record.level) { - return; - } - - for (const handler of this.handlers) { - handler.handle(record); - } - } - - createWriteStream(level?: LoggerLevelWeight, format?: boolean): NodeJS.WritableStream { - const self = this; - - return new class extends Writable { - _write(chunk: any, encoding: string, callback: () => void) { - self.log(self.createRecord(chunk.toString(), level, format)); - callback(); - } - }(); - } -} - -export interface CreateTaggedFormatterOptions { - prefix?: string | (() => string); - titleize?: boolean; - wrap?: boolean | WordWrapOptions; - colors?: Colors; - tags?: ReadonlyMap; -} - -export function createTaggedFormatter({ colors = NO_COLORS, prefix = '', tags, titleize, wrap }: CreateTaggedFormatterOptions = {}): LoggerFormatter { - const { strong, weak } = colors; - - const getLevelTag = (level?: LoggerLevelWeight): string => { - if (!level) { - return ''; - } - - if (tags) { - const tag = tags.get(level); - - return tag ? tag : ''; - } - - const levelName = getLoggerLevelName(level); - - if (!levelName) { - return ''; - } - - const levelColor = getLoggerLevelColor(colors, level); - - return `${weak('[')}\x1b[40m${strong(levelColor ? levelColor(levelName) : levelName)}\x1b[49m${weak(']')}`; - }; - - return ({ msg, level, format }) => { - if (format === false) { - return msg; - } - - const [ firstLine, ...lines ] = msg.split('\n'); - - const levelColor = getLoggerLevelColor(colors, level); - - const tag = (typeof prefix === 'function' ? prefix() : prefix) + getLevelTag(level); - const title = titleize && lines.length > 0 ? `${strong(levelColor ? levelColor(firstLine) : firstLine)}\n` : firstLine; - const indentation = tag ? stringWidth(tag) + 1 : 0; - const pulledLines = titleize ? dropWhile(lines, l => l === '') : lines; - - return ( - (tag ? `${tag} ` : '') + - (wrap - ? wordWrap([title, ...pulledLines].join('\n'), { indentation, ...(typeof wrap === 'object' ? wrap : {}) }) - : [title, ...pulledLines.map(l => l ? ' '.repeat(indentation) + l : '')].join('\n') - ) - ); - }; -} - -export function createPrefixedFormatter(prefix: string | (() => string)): LoggerFormatter { - return ({ msg, format }) => { - if (format === false) { - return msg; - } - - return `${typeof prefix === 'function' ? prefix() : prefix} ${msg}`; - }; -} diff --git a/packages/@ionic/cli-framework-output/src/output.ts b/packages/@ionic/cli-framework-output/src/output.ts deleted file mode 100644 index 83e1180980..0000000000 --- a/packages/@ionic/cli-framework-output/src/output.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { Cursor, EscapeCode } from '@ionic/utils-terminal'; - -import { Colors, NO_COLORS } from './colors'; -import { ICON_FAILURE, ICON_SUCCESS, Spinner, TaskChain } from './tasks'; -import { formatHrTime } from './utils'; - -export interface OutputStrategy { - readonly stream: NodeJS.WritableStream; - readonly colors: Colors; - write(msg: string): boolean; - createTaskChain(): TaskChain; -} - -export interface StreamOutputStrategyOptions { - readonly stream?: NodeJS.WritableStream; - readonly colors?: Colors; -} - -export class StreamOutputStrategy implements OutputStrategy { - readonly stream: NodeJS.WritableStream; - readonly colors: Colors; - - constructor({ stream = process.stdout, colors = NO_COLORS }: StreamOutputStrategyOptions) { - this.stream = stream; - this.colors = colors; - } - - write(msg: string): boolean { - return this.stream.write(msg); - } - - createTaskChain(): TaskChain { - const { failure, success, weak } = this.colors; - const chain = new TaskChain(); - - chain.on('next', task => { - task.on('end', result => { - if (result.success) { - this.write(`${success(ICON_SUCCESS)} ${task.msg} ${weak(`in ${formatHrTime(result.elapsedTime)}`)}\n`); - } else { - this.write(`${failure(ICON_FAILURE)} ${task.msg} ${failure(weak('- failed!'))}\n`); - } - }); - }); - - return chain; - } -} - -export interface TTYOutputStrategyOptions extends StreamOutputStrategyOptions { - readonly stream?: NodeJS.WriteStream; -} - -export class TTYOutputStrategy extends StreamOutputStrategy implements OutputStrategy { - readonly stream: NodeJS.WriteStream; - - protected readonly redrawer: TTYOutputRedrawer; - - constructor({ stream = process.stdout, colors = NO_COLORS }: TTYOutputStrategyOptions = {}) { - super({ stream, colors }); - this.stream = stream; - this.redrawer = new TTYOutputRedrawer({ stream }); - } - - createTaskChain(): TaskChain { - const { failure, strong, success, weak } = this.colors; - const chain = new TaskChain({ taskOptions: { tickInterval: 50 } }); - - chain.on('next', task => { - task.on('end', result => { - if (result.success) { - this.write(`${success(ICON_SUCCESS)} ${task.msg} ${weak(`in ${formatHrTime(result.elapsedTime)}`)}\n`); - } else { - this.write(`${failure(ICON_FAILURE)} ${task.msg} ${failure(weak('- failed!'))}\n`); - } - }); - - const spinner = new Spinner(); - - task.on('tick', () => { - const progress = task.progressRatio ? (task.progressRatio * 100).toFixed(2) : ''; - const frame = spinner.frame(); - - this.redrawer.redraw(`${strong(frame)} ${task.msg}${progress ? ' (' + strong(String(progress) + '%') + ')' : ''} `); - }); - - task.on('clear', () => { - this.redrawer.clear(); - }); - }); - - chain.on('end', () => { - this.redrawer.end(); - }); - - return chain; - } -} - -export interface TTYOutputRedrawerOptions { - readonly stream?: NodeJS.WriteStream; -} - -export class TTYOutputRedrawer { - readonly stream: NodeJS.WriteStream; - - constructor({ stream = process.stdout }: TTYOutputRedrawerOptions) { - this.stream = stream; - } - - get width(): number { - return this.stream.columns || 80; - } - - redraw(msg: string) { - Cursor.hide(); - this.stream.write(EscapeCode.eraseLines(1) + msg.replace(/[\r\n]+$/, '')); - } - - clear() { - this.stream.write(EscapeCode.eraseLines(1)); - } - - end() { - Cursor.show(); - } -} diff --git a/packages/@ionic/cli-framework-output/src/tasks.ts b/packages/@ionic/cli-framework-output/src/tasks.ts deleted file mode 100644 index 25e97c61ee..0000000000 --- a/packages/@ionic/cli-framework-output/src/tasks.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { EventEmitter } from 'events'; - -const isWindows = process.platform === 'win32'; - -export const ICON_SUCCESS = isWindows ? '√' : '✔'; -export const ICON_FAILURE = isWindows ? '×' : '✖'; - -const SPINNER_FRAMES = isWindows ? - ['-', '\\', '|', '/'] : - ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; - -export class Spinner { - i = 0; - - constructor(public frames: string[] = SPINNER_FRAMES) {} - - frame(): string { - return this.frames[this.i = ++this.i % this.frames.length]; - } -} - -export interface TaskOptions { - readonly msg?: string; - readonly tickInterval?: number; -} - -export interface TaskResult { - /** - * Elapsed time from process.hrtime() - */ - elapsedTime: [number, number]; - - /** - * Whether the task succeeded or not - */ - success: boolean; -} - -export interface Task extends EventEmitter { - on(name: 'success', handler: () => void): this; - on(name: 'failure', handler: () => void): this; - on(name: 'clear', handler: () => void): this; - on(name: 'tick', handler: () => void): this; - on(name: 'end', handler: (result: TaskResult) => void): this; - emit(name: 'success'): boolean; - emit(name: 'failure'): boolean; - emit(name: 'clear'): boolean; - emit(name: 'tick'): boolean; - emit(name: 'end', result: TaskResult): boolean; -} - -export class Task extends EventEmitter { - tickInterval?: number; - - intervalId?: NodeJS.Timer; - running = false; - progressRatio?: number; - - protected _msg = ''; - protected _startTime?: [number, number]; // hrtime - protected _success = false; - - constructor({ msg = '', tickInterval }: TaskOptions = {}) { - super(); - this.msg = msg; - this.tickInterval = tickInterval; - } - - get msg(): string { - return this._msg; - } - - set msg(msg: string) { - this._msg = msg; - this.tick(); - } - - start(): this { - if (!this.running && this.tickInterval) { - this.intervalId = setInterval(() => { this.tick(); }, this.tickInterval); - } - - this.running = true; - this._startTime = process.hrtime(); - - return this; - } - - tick(): this { - this.emit('tick'); - return this; - } - - progress(prog: number, total: number): this { - this.progressRatio = prog / total; - this.tick(); - - return this; - } - - clear(): this { - if (this.intervalId) { - clearInterval(this.intervalId); - this.intervalId = undefined; - } - - this.emit('clear'); - - return this; - } - - end(): this { - this.running = false; - this.tick(); - this.clear(); - this.emit('end', { - elapsedTime: process.hrtime(this._startTime), - success: this._success, - }); - - return this; - } - - succeed(): this { - if (this.running) { - this._success = true; - this.end(); - this.emit('success'); - } - - return this; - } - - fail(): this { - if (this.running) { - this._success = false; - this.end(); - this.emit('failure'); - } - - return this; - } -} - -export interface TaskChain extends EventEmitter { - on(name: 'end', handler: (lastTask?: Task) => void): this; - on(name: 'failure', handler: (failedTask?: Task) => void): this; - on(name: 'next', handler: (task: Task) => void): this; - emit(name: 'end', lastTask?: Task): boolean; - emit(name: 'failure', failedTask?: Task): boolean; - emit(name: 'next', task: Task): boolean; -} - -export interface TaskChainOptions { - readonly taskOptions?: Partial; -} - -export class TaskChain extends EventEmitter { - protected current?: Task; - protected readonly tasks: Task[]; - protected readonly taskOptions: Partial; - - constructor({ taskOptions = {} }: TaskChainOptions = {}) { - super(); - this.tasks = []; - this.taskOptions = taskOptions; - } - - next(msg: string): Task { - return this.nextTask(this.createTask({ msg, ...this.taskOptions })); - } - - createTask(options: TaskOptions): Task { - return new Task(options); - } - - nextTask(task: Task): Task { - if (this.current) { - this.current.succeed(); - } - - this.tasks.push(task); - this.current = task; - - task.start(); - - this.emit('next', task); - - return task; - } - - end(): this { - const task = this.current; - - if (task) { - task.succeed(); - } - - this.current = undefined; - this.emit('end', task); - - return this; - } - - fail(): this { - const task = this.current; - - if (task) { - task.fail(); - } - - this.emit('failure', task); - - return this; - } - - cleanup(): this { - for (const task of this.tasks) { - if (task.running) { - task.fail(); - } else { - task.clear(); - } - } - - return this; - } -} diff --git a/packages/@ionic/cli-framework-output/src/utils.ts b/packages/@ionic/cli-framework-output/src/utils.ts deleted file mode 100644 index 636c435fe9..0000000000 --- a/packages/@ionic/cli-framework-output/src/utils.ts +++ /dev/null @@ -1,39 +0,0 @@ -export function identity(v: T): T { - return v; -} - -export function enforceLF(str: string): string { - return str.match(/[\r\n]$/) ? str : str + '\n'; -} - -export function dropWhile(array: readonly T[], predicate: (item: T) => boolean = v => !!v): T[] { - let done = false; - - return array.filter(item => { - if (done) { - return true; - } - - if (predicate(item)) { - return false; - } else { - done = true; - return true; - } - }); -} - -const TIME_UNITS = ['s', 'ms', 'μs']; - -export function formatHrTime(hrtime: [number, number]): string { - let time = hrtime[0] + hrtime[1] / 1e9; - let index = 0; - - for (; index < TIME_UNITS.length - 1; index++, time *= 1000) { - if (time >= 1) { - break; - } - } - - return time.toFixed(2) + TIME_UNITS[index]; -} diff --git a/packages/@ionic/cli-framework-output/tsconfig.json b/packages/@ionic/cli-framework-output/tsconfig.json deleted file mode 100644 index 46cccd1031..0000000000 --- a/packages/@ionic/cli-framework-output/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "types": [ - "node" - ] - }, - "include": [ - "src/**/*.ts" - ], - "exclude": [ - "node_modules", - "src/**/__tests__/*.ts" - ] -} diff --git a/packages/@ionic/cli-framework-prompts/.gitignore b/packages/@ionic/cli-framework-prompts/.gitignore deleted file mode 100644 index de4d1f007d..0000000000 --- a/packages/@ionic/cli-framework-prompts/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -dist -node_modules diff --git a/packages/@ionic/cli-framework-prompts/.npmrc b/packages/@ionic/cli-framework-prompts/.npmrc deleted file mode 100644 index 43c97e719a..0000000000 --- a/packages/@ionic/cli-framework-prompts/.npmrc +++ /dev/null @@ -1 +0,0 @@ -package-lock=false diff --git a/packages/@ionic/cli-framework-prompts/CHANGELOG.md b/packages/@ionic/cli-framework-prompts/CHANGELOG.md deleted file mode 100644 index 4a0bd1e5ba..0000000000 --- a/packages/@ionic/cli-framework-prompts/CHANGELOG.md +++ /dev/null @@ -1,185 +0,0 @@ -# Change Log - -All notable changes to this project will be documented in this file. -See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -## [2.1.13](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework-prompts@2.1.12...@ionic/cli-framework-prompts@2.1.13) (2023-12-19) - - -### Bug Fixes - -* **cli:** resolve vm2 security vulnerability ([#5070](https://github.com/ionic-team/ionic-cli/issues/5070)) ([4050419](https://github.com/ionic-team/ionic-cli/commit/4050419bef70fb92e58b0a83cd4b68b48090e596)) - - - - - -## [2.1.12](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework-prompts@2.1.11...@ionic/cli-framework-prompts@2.1.12) (2023-11-07) - -**Note:** Version bump only for package @ionic/cli-framework-prompts - - - - - -## [2.1.11](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework-prompts@2.1.10...@ionic/cli-framework-prompts@2.1.11) (2023-03-29) - -**Note:** Version bump only for package @ionic/cli-framework-prompts - - - - - -## [2.1.10](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework-prompts@2.1.9...@ionic/cli-framework-prompts@2.1.10) (2022-06-16) - -**Note:** Version bump only for package @ionic/cli-framework-prompts - - - - - -## [2.1.9](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework-prompts@2.1.8...@ionic/cli-framework-prompts@2.1.9) (2022-05-09) - -**Note:** Version bump only for package @ionic/cli-framework-prompts - - - - - -## [2.1.8](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework-prompts@2.1.7...@ionic/cli-framework-prompts@2.1.8) (2020-09-29) - -**Note:** Version bump only for package @ionic/cli-framework-prompts - - - - - -## [2.1.7](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework-prompts@2.1.6...@ionic/cli-framework-prompts@2.1.7) (2020-09-24) - -**Note:** Version bump only for package @ionic/cli-framework-prompts - - - - - -## [2.1.6](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework-prompts@2.1.5...@ionic/cli-framework-prompts@2.1.6) (2020-08-28) - -**Note:** Version bump only for package @ionic/cli-framework-prompts - - - - - -## [2.1.5](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework-prompts@2.1.4...@ionic/cli-framework-prompts@2.1.5) (2020-08-27) - -**Note:** Version bump only for package @ionic/cli-framework-prompts - - - - - -## [2.1.4](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework-prompts@2.1.3...@ionic/cli-framework-prompts@2.1.4) (2020-08-25) - -**Note:** Version bump only for package @ionic/cli-framework-prompts - - - - - -## [2.1.3](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework-prompts@2.1.2...@ionic/cli-framework-prompts@2.1.3) (2020-05-12) - - -### Bug Fixes - -* pin tslib to avoid "Cannot set property pathExists" error ([689e1f0](https://github.com/ionic-team/ionic-cli/commit/689e1f038b907356ef855a067a76d4822e7072a8)) - - - - - -## [2.1.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework-prompts@2.1.1...@ionic/cli-framework-prompts@2.1.2) (2020-05-06) - -**Note:** Version bump only for package @ionic/cli-framework-prompts - - - - - -## [2.1.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework-prompts@2.1.0...@ionic/cli-framework-prompts@2.1.1) (2020-03-03) - -**Note:** Version bump only for package @ionic/cli-framework-prompts - - - - - -# 2.1.0 (2020-02-11) - - -### Features - -* **start:** add new list starter option ([#4315](https://github.com/ionic-team/ionic-cli/issues/4315)) ([1df44c1](https://github.com/ionic-team/ionic-cli/commit/1df44c1591f37b89f2b672857740edd6cb2aea67)) - - - - - -## [2.0.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework-prompts@2.0.1...@ionic/cli-framework-prompts@2.0.2) (2020-02-10) - -**Note:** Version bump only for package @ionic/cli-framework-prompts - - - - - -## [2.0.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework-prompts@2.0.0...@ionic/cli-framework-prompts@2.0.1) (2020-02-03) - -**Note:** Version bump only for package @ionic/cli-framework-prompts - - - - - -# [2.0.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework-prompts@1.0.4...@ionic/cli-framework-prompts@2.0.0) (2020-01-25) - - -### chore - -* require Node 10 ([5a47874](https://github.com/ionic-team/ionic-cli/commit/5a478746c074207b6dc96aa8771f04a606deb1ef)) - - -### BREAKING CHANGES - -* A minimum of Node.js 10.3.0 is required. - - - - - -## [1.0.4](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework-prompts@1.0.3...@ionic/cli-framework-prompts@1.0.4) (2019-12-10) - -**Note:** Version bump only for package @ionic/cli-framework-prompts - - - - - -## [1.0.3](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework-prompts@1.0.2...@ionic/cli-framework-prompts@1.0.3) (2019-12-05) - -**Note:** Version bump only for package @ionic/cli-framework-prompts - - - - - -## [1.0.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework-prompts@1.0.1...@ionic/cli-framework-prompts@1.0.2) (2019-11-21) - -**Note:** Version bump only for package @ionic/cli-framework-prompts - - - - - -## 1.0.1 (2019-10-14) - -**Note:** Version bump only for package @ionic/cli-framework-prompts diff --git a/packages/@ionic/cli-framework-prompts/LICENSE b/packages/@ionic/cli-framework-prompts/LICENSE deleted file mode 100644 index 62cadf753a..0000000000 --- a/packages/@ionic/cli-framework-prompts/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2019 Drifty Co - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/packages/@ionic/cli-framework-prompts/README.md b/packages/@ionic/cli-framework-prompts/README.md deleted file mode 100644 index 7a60b4f8d3..0000000000 --- a/packages/@ionic/cli-framework-prompts/README.md +++ /dev/null @@ -1 +0,0 @@ -# @ionic/cli-framework-prompts diff --git a/packages/@ionic/cli-framework-prompts/jest.config.js b/packages/@ionic/cli-framework-prompts/jest.config.js deleted file mode 100644 index eace8d6d9d..0000000000 --- a/packages/@ionic/cli-framework-prompts/jest.config.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('../../../jest.config.base'); diff --git a/packages/@ionic/cli-framework-prompts/lint-staged.config.js b/packages/@ionic/cli-framework-prompts/lint-staged.config.js deleted file mode 100644 index 5815a72f0d..0000000000 --- a/packages/@ionic/cli-framework-prompts/lint-staged.config.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('../../../lint-staged.config.base'); diff --git a/packages/@ionic/cli-framework-prompts/package.json b/packages/@ionic/cli-framework-prompts/package.json deleted file mode 100644 index ce4aacfe5b..0000000000 --- a/packages/@ionic/cli-framework-prompts/package.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "name": "@ionic/cli-framework-prompts", - "version": "2.1.13", - "description": "The interactive prompts portion of Ionic CLI Framework", - "homepage": "https://ionicframework.com/", - "author": "Ionic Team (https://ionicframework.com)", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "engines": { - "node": ">=16.0.0" - }, - "files": [ - "dist/", - "LICENSE", - "README.md" - ], - "repository": { - "type": "git", - "url": "https://github.com/ionic-team/ionic-cli.git" - }, - "bugs": { - "url": "https://github.com/ionic-team/ionic-cli/issues" - }, - "scripts": { - "clean": "rimraf dist", - "lint": "true", - "build": "npm run clean && tsc", - "watch": "tsc -w --preserveWatchOutput", - "test": "jest --maxWorkers=4", - "prepublishOnly": "npm run build" - }, - "license": "MIT", - "dependencies": { - "@ionic/utils-terminal": "2.3.5", - "debug": "^4.0.0", - "inquirer": "^7.0.0", - "tslib": "^2.0.1" - }, - "devDependencies": { - "@types/debug": "^4.1.1", - "@types/inquirer": "0.0.43", - "@types/jest": "^26.0.10", - "@types/node": "~16.0.0", - "jest": "^26.4.2", - "jest-cli": "^26.0.1", - "lint-staged": "^10.0.2", - "ts-jest": "~26.3.0", - "typescript": "~4.8.0" - } -} diff --git a/packages/@ionic/cli-framework-prompts/src/__tests__/index.ts b/packages/@ionic/cli-framework-prompts/src/__tests__/index.ts deleted file mode 100644 index f01e7b0322..0000000000 --- a/packages/@ionic/cli-framework-prompts/src/__tests__/index.ts +++ /dev/null @@ -1,198 +0,0 @@ -describe('@ionic/cli-framework-prompts', () => { - - describe('createPromptModule', () => { - - const mockMute = jest.fn(); - const mockClose = jest.fn(); - const mockLogStream = {}; - - function setupPromptMocks({ value, tty }: { value?: any; tty: boolean; }) { - const mocktty = tty; - const mockCreatePromptModule = () => async (question: { name: string; }) => ({ [question.name]: value }); - mockMute.mockReset(); - mockClose.mockReset(); - jest.resetModules(); - jest.mock('@ionic/utils-terminal', () => ({ TERMINAL_INFO: { tty: mocktty } })); - jest.mock('inquirer', () => ({ - ui: { BottomBar: class { close = mockClose; log = mockLogStream; rl = { output: { mute: mockMute } } } }, - createPromptModule: mockCreatePromptModule, - })); - - return require('..'); - } - - it('should provide empty string for no value', async () => { - const prompts = setupPromptMocks({ value: undefined, tty: true }); - const prompt = await prompts.createPromptModule(); - const result = await prompt({ type: 'input' }); - expect(result).toEqual(''); - }); - - it('should provide empty string for null value', async () => { - const prompts = setupPromptMocks({ value: null, tty: true }); - const prompt = await prompts.createPromptModule(); - const result = await prompt({ type: 'input' }); - expect(result).toEqual(''); - }); - - it('should provide empty string', async () => { - const prompts = setupPromptMocks({ value: '', tty: true }); - const prompt = await prompts.createPromptModule(); - const result = await prompt({ type: 'input' }); - expect(result).toEqual(''); - }); - - it('should provide string', async () => { - const prompts = setupPromptMocks({ value: 'hello', tty: true }); - const prompt = await prompts.createPromptModule(); - const result = await prompt({ type: 'input' }); - expect(result).toEqual('hello'); - }); - - it('should provide boolean true', async () => { - const prompts = setupPromptMocks({ value: true, tty: true }); - const prompt = await prompts.createPromptModule(); - const result = await prompt({ type: 'confirm' }); - expect(result).toEqual(true); - }); - - it('should provide boolean false', async () => { - const prompts = setupPromptMocks({ value: false, tty: true }); - const prompt = await prompts.createPromptModule(); - const result = await prompt({ type: 'confirm' }); - expect(result).toEqual(false); - }); - - it('should provide array of choices', async () => { - const prompts = setupPromptMocks({ value: ['a', 'b', 'c'], tty: true }); - const prompt = await prompts.createPromptModule(); - const result = await prompt({ type: 'checkbox' }); - expect(result).toEqual(['a', 'b', 'c']); - }); - - it('should provide string cast from number value', async () => { - const prompts = setupPromptMocks({ value: 42, tty: true }); - const prompt = await prompts.createPromptModule(); - const result = await prompt({ type: 'input' }); - expect(result).toEqual('42'); - }); - - it('should provide empty string for input prompt for no value in non-tty mode', async () => { - const prompts = setupPromptMocks({ value: undefined, tty: false }); - const prompt = await prompts.createPromptModule(); - const result = await prompt({ type: 'input' }); - expect(result).toEqual(''); - }); - - it('should provide false for confirm prompt for no value in non-tty mode', async () => { - const prompts = setupPromptMocks({ value: undefined, tty: false }); - const prompt = await prompts.createPromptModule(); - const result = await prompt({ type: 'confirm' }); - expect(result).toEqual(false); - }); - - it('should provide empty array for checkbox prompt for no value in non-tty mode', async () => { - const prompts = setupPromptMocks({ value: undefined, tty: false }); - const prompt = await prompts.createPromptModule(); - const result = await prompt({ type: 'checkbox' }); - expect(result).toEqual([]); - }); - - it('should provide default value for no value in non-tty mode', async () => { - const prompts = setupPromptMocks({ value: undefined, tty: false }); - const prompt = await prompts.createPromptModule(); - const result = await prompt({ type: 'input', default: 'hello' }); - expect(result).toEqual('hello'); - }); - - it('should provide fallback value for no value and default value in non-tty mode', async () => { - const prompts = setupPromptMocks({ value: undefined, tty: false }); - const prompt = await prompts.createPromptModule(); - const result = await prompt({ type: 'input', fallback: 'foo', default: 'bar' }); - expect(result).toEqual('foo'); - }); - - it('should provide onFallback value for no value and no fallback and default value in non-tty mode', async () => { - const prompts = setupPromptMocks({ value: undefined, tty: false }); - const prompt = await prompts.createPromptModule({ onFallback: () => 'foo' }); - const result = await prompt({ type: 'input', default: 'bar' }); - expect(result).toEqual('foo'); - }); - - it('should pass question into provided onFallback in non-tty', async () => { - const prompts = setupPromptMocks({ value: undefined, tty: false }); - const onFallbackSpy = jest.fn().mockImplementation(() => 'foo'); - const prompt = await prompts.createPromptModule({ onFallback: onFallbackSpy }); - const question = { type: 'input', default: 'bar' }; - const result = await prompt(question); - expect(result).toEqual('foo'); - expect(onFallbackSpy).toHaveBeenCalledTimes(1); - expect(onFallbackSpy).toHaveBeenCalledWith(question); - }); - - it('should provide empty string for input prompt for no value in non-interactive mode', async () => { - const prompts = setupPromptMocks({ value: undefined, tty: true }); - const prompt = await prompts.createPromptModule({ interactive: false }); - const result = await prompt({ type: 'input' }); - expect(result).toEqual(''); - }); - - it('should provide false for confirm prompt for no value in non-interactive mode', async () => { - const prompts = setupPromptMocks({ value: undefined, tty: true }); - const prompt = await prompts.createPromptModule({ interactive: false }); - const result = await prompt({ type: 'confirm' }); - expect(result).toEqual(false); - }); - - it('should provide empty array for checkbox prompt for no value in non-interactive mode', async () => { - const prompts = setupPromptMocks({ value: undefined, tty: true }); - const prompt = await prompts.createPromptModule({ interactive: false }); - const result = await prompt({ type: 'checkbox' }); - expect(result).toEqual([]); - }); - - it('should provide default value for no value in non-interactive mode', async () => { - const prompts = setupPromptMocks({ value: undefined, tty: true }); - const prompt = await prompts.createPromptModule({ interactive: false }); - const result = await prompt({ type: 'input', default: 'hello' }); - expect(result).toEqual('hello'); - }); - - it('should provide fallback value for no value and default value in non-interactive mode', async () => { - const prompts = setupPromptMocks({ value: undefined, tty: true }); - const prompt = await prompts.createPromptModule({ interactive: false }); - const result = await prompt({ type: 'input', fallback: 'foo', default: 'bar' }); - expect(result).toEqual('foo'); - }); - - it('should provide onFallback value for no value and no fallback and default value in non-interactive mode', async () => { - const prompts = setupPromptMocks({ value: undefined, tty: true }); - const prompt = await prompts.createPromptModule({ interactive: false, onFallback: () => 'foo' }); - const result = await prompt({ type: 'input', default: 'bar' }); - expect(result).toEqual('foo'); - }); - - it('should pass question into provided onFallback in non-interactive', async () => { - const prompts = setupPromptMocks({ value: undefined, tty: true }); - const onFallbackSpy = jest.fn().mockImplementation(() => 'foo'); - const prompt = await prompts.createPromptModule({ interactive: false, onFallback: onFallbackSpy }); - const question = { type: 'input', default: 'bar' }; - const result = await prompt(question); - expect(result).toEqual('foo'); - expect(onFallbackSpy).toHaveBeenCalledTimes(1); - expect(onFallbackSpy).toHaveBeenCalledWith(question); - }); - - describe('_inquirer', () => { - - it('should get _inquirer module from prompt module', async () => { - const prompts = setupPromptMocks({ tty: true }); - const prompt = await prompts.createPromptModule(); - expect(prompt._inquirer).toBeDefined(); - }); - - }); - - }); - -}); diff --git a/packages/@ionic/cli-framework-prompts/src/index.ts b/packages/@ionic/cli-framework-prompts/src/index.ts deleted file mode 100644 index a46722f693..0000000000 --- a/packages/@ionic/cli-framework-prompts/src/index.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { TERMINAL_INFO } from '@ionic/utils-terminal'; -import { debug as Debug } from 'debug'; - -const debug = Debug('ionic:cli-framework-prompts'); - -export type Inquirer = import('inquirer').Inquirer; -export type Question = import('inquirer').Question; -export type Separator = import('inquirer').objects.Separator; - -let _inquirer: Inquirer | undefined; - -export interface PromptQuestionBase extends Question { - /** - * The prompt type for this question. - * - 'confirm': Y/n - * - 'checkbox': Multi-value selection. - * - 'input': Text input. - * - 'password': Masked text input. - * - 'list': Single-value selection. - */ - type: PromptType; - - /** - * The question to print. - */ - message: string; - - /** - * The fallback value to use in non-TTY mode. - */ - fallback?: PromptValue; - - /** - * Default value to use if nothing is entered. - */ - default?: PromptValue; -} - -export type PromptTypeConfirm = 'confirm'; -export type PromptTypeCheckbox = 'checkbox'; -export type PromptTypeOther = 'input' | 'password' | 'list'; -export type PromptType = PromptTypeConfirm | PromptTypeCheckbox | PromptTypeOther; - -export type PromptValueConfirm = boolean; -export type PromptValueCheckbox = string[]; -export type PromptValueOther = string; -export type PromptValue = PromptValueConfirm | PromptValueCheckbox | PromptValueOther; - -export interface PromptQuestionConfirm extends PromptQuestionBase { - type: PromptTypeConfirm; - fallback?: PromptValueConfirm; - default?: PromptValueConfirm; -} - -export interface PromptQuestionCheckbox extends PromptQuestionBase { - type: PromptTypeCheckbox; - fallback?: PromptValueCheckbox; - default?: PromptValueCheckbox; -} - -export interface PromptQuestionOther extends PromptQuestionBase { - type: PromptTypeOther; - fallback?: PromptValueOther; - default?: PromptValueOther; -} - -export type PromptQuestion = PromptQuestionConfirm | PromptQuestionCheckbox | PromptQuestionOther; - -export interface PromptModule { - readonly _inquirer: Inquirer; - - (question: PromptQuestionConfirm): Promise; - (question: PromptQuestionCheckbox): Promise; - (question: PromptQuestionOther): Promise; -} - -async function loadInquirer(): Promise { - if (!_inquirer) { - _inquirer = await import('inquirer'); - } - - return _inquirer; -} - -export interface CreatePromptModuleOptions { - readonly interactive?: boolean; - readonly onFallback?: (question: PromptQuestion) => PromptValue | void; -} - -/** - * Create a reusable CLI prompt module. - * - * A prompt module is a function that generates prompts. A prompt opens an - * interactive session with the user to gather input. When a prompt is - * resolved, the user has finished providing input. - * - * If non-TTY mode is detected, a system of fallbacks goes into effect: - * 1. If the question provided 'fallback', the fallback value is resolved. - * 2. If the prompt module has 'onFallback', it is used to generate a - * fallback for the question. - * 3. If the question provided 'default', the default value is resolved. - * 4. Finally, a falsy value suitable for the question type is resolved. - * - * @param options.interactive Force non-TTY mode by providing 'false'. TTY mode - * cannot be forced if non-TTY mode is detected. - * @param options.onFallback Generate a non-TTY fallback for a question without - * a 'fallback'. - */ -export async function createPromptModule({ interactive, onFallback }: CreatePromptModuleOptions = {}): Promise { - const inquirer = await loadInquirer(); - const { createPromptModule: createInquirerPromptModule } = inquirer; - const promptModule = createInquirerPromptModule(); - - async function createPrompter(question: PromptQuestionConfirm): Promise; - async function createPrompter(question: PromptQuestionCheckbox): Promise; - async function createPrompter(question: PromptQuestionOther): Promise; - async function createPrompter(question: PromptQuestion): Promise { - const { fallback, ...promptQuestion } = question; - - if (!TERMINAL_INFO.tty || interactive === false) { - if (typeof fallback !== 'undefined') { - debug('Answering with provided fallback value for non-tty mode: %o', fallback); - return fallback; - } else if (onFallback) { - const generatedFallback = onFallback(question); - - if (typeof generatedFallback !== 'undefined') { - debug(`Answering with fallback value from 'onFallback' for non-tty mode: %o`, generatedFallback); - return generatedFallback; - } - } - - if (typeof promptQuestion.default !== 'undefined') { - return promptQuestion.default; - } - - if (question.type === 'confirm') { - return false; - } else if (question.type === 'checkbox') { - return []; - } - - return ''; - } - - const name = 'name'; - const prompt = promptModule({ ...promptQuestion, name }); - const result = (await prompt)[name]; - - if (typeof result === 'undefined' || result === null) { - return ''; - } - - if (typeof result !== 'string' && typeof result !== 'boolean' && !Array.isArray(result)) { - return String(result); - } - - return result; - } - - Object.defineProperties(createPrompter, { - _inquirer: { value: inquirer }, - }); - - return createPrompter as any as PromptModule; -} - -export function createPromptChoiceSeparator(): Separator { - if (!_inquirer) { - throw new Error(`Prompt module not initialized. Call 'createPromptModule' first.`); - } - - return new _inquirer.Separator(); -} diff --git a/packages/@ionic/cli-framework-prompts/tsconfig.json b/packages/@ionic/cli-framework-prompts/tsconfig.json deleted file mode 100644 index 46cccd1031..0000000000 --- a/packages/@ionic/cli-framework-prompts/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "types": [ - "node" - ] - }, - "include": [ - "src/**/*.ts" - ], - "exclude": [ - "node_modules", - "src/**/__tests__/*.ts" - ] -} diff --git a/packages/@ionic/cli-framework/.gitignore b/packages/@ionic/cli-framework/.gitignore deleted file mode 100644 index 988c2bd32f..0000000000 --- a/packages/@ionic/cli-framework/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -*.js -!tslint.js -!jest.config.js -!lint-staged.config.js -*.d.ts diff --git a/packages/@ionic/cli-framework/.npmignore b/packages/@ionic/cli-framework/.npmignore deleted file mode 100644 index 56aac2308d..0000000000 --- a/packages/@ionic/cli-framework/.npmignore +++ /dev/null @@ -1,5 +0,0 @@ -src -jest.config.js -lint-staged.config.js -tsconfig.json -tslint.js diff --git a/packages/@ionic/cli-framework/.npmrc b/packages/@ionic/cli-framework/.npmrc deleted file mode 100644 index 43c97e719a..0000000000 --- a/packages/@ionic/cli-framework/.npmrc +++ /dev/null @@ -1 +0,0 @@ -package-lock=false diff --git a/packages/@ionic/cli-framework/CHANGELOG.md b/packages/@ionic/cli-framework/CHANGELOG.md deleted file mode 100644 index 26ddfa9909..0000000000 --- a/packages/@ionic/cli-framework/CHANGELOG.md +++ /dev/null @@ -1,713 +0,0 @@ -# Change Log - -All notable changes to this project will be documented in this file. -See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -## [6.0.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@6.0.0...@ionic/cli-framework@6.0.1) (2023-12-19) - - -### Bug Fixes - -* **cli:** resolve vm2 security vulnerability ([#5070](https://github.com/ionic-team/ionic-cli/issues/5070)) ([4050419](https://github.com/ionic-team/ionic-cli/commit/4050419bef70fb92e58b0a83cd4b68b48090e596)) - - - - - -# [6.0.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@5.1.7...@ionic/cli-framework@6.0.0) (2023-11-08) - - -### Bug Fixes - -* use native ES2022 error cause ([#5010](https://github.com/ionic-team/ionic-cli/issues/5010)) ([a97ba2b](https://github.com/ionic-team/ionic-cli/commit/a97ba2bcac4556017ba010692f71fed2bef3f77b)) - - -### BREAKING CHANGES - -* `message`, `stack`, and `error` properties removed from `BaseError` and `SubprocessError` - - - - - -## [5.1.7](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@5.1.6...@ionic/cli-framework@5.1.7) (2023-11-08) - - -### Reverts - -* use native ES2022 error cause ([#5060](https://github.com/ionic-team/ionic-cli/issues/5060)) ([1e64a1a](https://github.com/ionic-team/ionic-cli/commit/1e64a1ada60545adf8e7c99fbd1f8766cf2416f9)) - - - - - -## [5.1.6](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@5.1.5...@ionic/cli-framework@5.1.6) (2023-11-07) - -**Note:** Version bump only for package @ionic/cli-framework - - - - - -## [5.1.5](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@5.1.4...@ionic/cli-framework@5.1.5) (2023-11-07) - - -### Bug Fixes - -* use native ES2022 error cause ([#5010](https://github.com/ionic-team/ionic-cli/issues/5010)) ([0c4cd0f](https://github.com/ionic-team/ionic-cli/commit/0c4cd0f47e00b43e8c0ce4eef072351a846b566c)) - - - - - -## [5.1.4](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@5.1.3...@ionic/cli-framework@5.1.4) (2023-03-29) - -**Note:** Version bump only for package @ionic/cli-framework - - - - - -## [5.1.3](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@5.1.2...@ionic/cli-framework@5.1.3) (2022-06-16) - -**Note:** Version bump only for package @ionic/cli-framework - - - - - -## [5.1.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@5.1.1...@ionic/cli-framework@5.1.2) (2022-05-09) - -**Note:** Version bump only for package @ionic/cli-framework - - - - - -## [5.1.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@5.1.0...@ionic/cli-framework@5.1.1) (2022-03-04) - -**Note:** Version bump only for package @ionic/cli-framework - - - - - -# [5.1.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@5.0.6...@ionic/cli-framework@5.1.0) (2020-12-10) - - -### Features - -* **config:** add spaces option when writing JSON config ([#4612](https://github.com/ionic-team/ionic-cli/issues/4612)) ([fdd9bb2](https://github.com/ionic-team/ionic-cli/commit/fdd9bb26098441238adc56ea7a67b385a3b9a964)) - - - - - -## [5.0.6](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@5.0.5...@ionic/cli-framework@5.0.6) (2020-09-29) - -**Note:** Version bump only for package @ionic/cli-framework - - - - - -## [5.0.5](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@5.0.4...@ionic/cli-framework@5.0.5) (2020-09-24) - -**Note:** Version bump only for package @ionic/cli-framework - - - - - -## [5.0.4](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@5.0.3...@ionic/cli-framework@5.0.4) (2020-09-02) - -**Note:** Version bump only for package @ionic/cli-framework - - - - - -## [5.0.3](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@5.0.2...@ionic/cli-framework@5.0.3) (2020-08-29) - -**Note:** Version bump only for package @ionic/cli-framework - - - - - -## [5.0.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@5.0.1...@ionic/cli-framework@5.0.2) (2020-08-28) - -**Note:** Version bump only for package @ionic/cli-framework - - - - - -## [5.0.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@5.0.0...@ionic/cli-framework@5.0.1) (2020-08-27) - -**Note:** Version bump only for package @ionic/cli-framework - - - - - -# [5.0.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@4.2.2...@ionic/cli-framework@5.0.0) (2020-08-27) - - -### Code Refactoring - -* do not re-export from @ionic/cli-framework-output ([a91b5a4](https://github.com/ionic-team/ionic-cli/commit/a91b5a4cb76570154e560bdea3138a425833ce8c)) - - -### BREAKING CHANGES - -* Install `@ionic/cli-framework-output` and import from it directly. - - - - - -## [4.2.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@4.2.1...@ionic/cli-framework@4.2.2) (2020-08-26) - -**Note:** Version bump only for package @ionic/cli-framework - - - - - -## [4.2.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@4.2.0...@ionic/cli-framework@4.2.1) (2020-08-25) - -**Note:** Version bump only for package @ionic/cli-framework - - - - - -# [4.2.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@4.1.5...@ionic/cli-framework@4.2.0) (2020-06-02) - - -### Features - -* **help:** formatBeforeInputSummary and formatAfterInputSummary ([70ac3ab](https://github.com/ionic-team/ionic-cli/commit/70ac3ab633dc7dd65a2ac344c81a01f7914597b8)) - - - - - -## [4.1.5](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@4.1.4...@ionic/cli-framework@4.1.5) (2020-05-12) - - -### Bug Fixes - -* pin tslib to avoid "Cannot set property pathExists" error ([689e1f0](https://github.com/ionic-team/ionic-cli/commit/689e1f038b907356ef855a067a76d4822e7072a8)) - - - - - -## [4.1.4](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@4.1.3...@ionic/cli-framework@4.1.4) (2020-05-06) - -**Note:** Version bump only for package @ionic/cli-framework - - - - - -## [4.1.3](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@4.1.2...@ionic/cli-framework@4.1.3) (2020-04-29) - -**Note:** Version bump only for package @ionic/cli-framework - - - - - -## [4.1.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@4.1.1...@ionic/cli-framework@4.1.2) (2020-03-30) - -**Note:** Version bump only for package @ionic/cli-framework - - - - - -## [4.1.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@4.1.0...@ionic/cli-framework@4.1.1) (2020-03-03) - -**Note:** Version bump only for package @ionic/cli-framework - - - - - -# 4.1.0 (2020-02-11) - - -### Features - -* **start:** add new list starter option ([#4315](https://github.com/ionic-team/ionic-cli/issues/4315)) ([1df44c1](https://github.com/ionic-team/ionic-cli/commit/1df44c1591f37b89f2b672857740edd6cb2aea67)) - - - - - -## [4.0.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@4.0.1...@ionic/cli-framework@4.0.2) (2020-02-10) - -**Note:** Version bump only for package @ionic/cli-framework - - - - - -## [4.0.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@4.0.0...@ionic/cli-framework@4.0.1) (2020-02-03) - - -### Bug Fixes - -* **help:** use proper full command/namespace name, not alias ([e42efc6](https://github.com/ionic-team/ionic-cli/commit/e42efc6ad6ce48545fe3f106720a9aa14af478f5)) - - - - - -# [4.0.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@3.0.6...@ionic/cli-framework@4.0.0) (2020-01-25) - - -### chore - -* require Node 10 ([5a47874](https://github.com/ionic-team/ionic-cli/commit/5a478746c074207b6dc96aa8771f04a606deb1ef)) - - -### BREAKING CHANGES - -* A minimum of Node.js 10.3.0 is required. - - - - - -## [3.0.6](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@3.0.5...@ionic/cli-framework@3.0.6) (2020-01-13) - -**Note:** Version bump only for package @ionic/cli-framework - - - - - -## [3.0.5](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@3.0.4...@ionic/cli-framework@3.0.5) (2019-12-10) - - -### Bug Fixes - -* **help:** make namespace/command groups unique when displaying ([32215b2](https://github.com/ionic-team/ionic-cli/commit/32215b27d4511bd8966bb07ba1663392f790336f)) - - - - - -## [3.0.4](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@3.0.3...@ionic/cli-framework@3.0.4) (2019-12-05) - -**Note:** Version bump only for package @ionic/cli-framework - - - - - -## [3.0.3](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@3.0.2...@ionic/cli-framework@3.0.3) (2019-11-25) - -**Note:** Version bump only for package @ionic/cli-framework - - - - - -## [3.0.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@3.0.1...@ionic/cli-framework@3.0.2) (2019-11-24) - - -### Bug Fixes - -* fix parsing of command line options ([060f67c](https://github.com/ionic-team/ionic-cli/commit/060f67cf63d37662ae44c4ae952161464a5d553c)) - - - - - -## [3.0.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@3.0.0...@ionic/cli-framework@3.0.1) (2019-11-21) - -**Note:** Version bump only for package @ionic/cli-framework - - - - - -# [3.0.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@2.1.8...@ionic/cli-framework@3.0.0) (2019-10-14) - - -### Code Refactoring - -* **prompts:** remove prompt support ([82241ef](https://github.com/ionic-team/ionic-cli/commit/82241ef7ddf8fdc6fc0db051a9acd0ed100c3fa8)) - - -### BREAKING CHANGES - -* **prompts:** This package no longer supports interactive prompts. See `@ionic/cli-framework-prompts`. - - - - - -## [2.1.8](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@2.1.7...@ionic/cli-framework@2.1.8) (2019-10-14) - -**Note:** Version bump only for package @ionic/cli-framework - - - - - -## [2.1.7](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@2.1.6...@ionic/cli-framework@2.1.7) (2019-09-18) - -**Note:** Version bump only for package @ionic/cli-framework - - - - - -## [2.1.6](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@2.1.5...@ionic/cli-framework@2.1.6) (2019-08-28) - -**Note:** Version bump only for package @ionic/cli-framework - - - - - -## [2.1.5](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@2.1.4...@ionic/cli-framework@2.1.5) (2019-08-23) - -**Note:** Version bump only for package @ionic/cli-framework - - - - - -## [2.1.4](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@2.1.3...@ionic/cli-framework@2.1.4) (2019-08-14) - - -### Bug Fixes - -* **help:** custom colors are not passed to SchemaHelpFormatter ([#4115](https://github.com/ionic-team/ionic-cli/issues/4115)) ([5634e9f](https://github.com/ionic-team/ionic-cli/commit/5634e9f)) - - - - - -## [2.1.3](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@2.1.2...@ionic/cli-framework@2.1.3) (2019-08-07) - -**Note:** Version bump only for package @ionic/cli-framework - - - - - -## [2.1.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@2.1.1...@ionic/cli-framework@2.1.2) (2019-07-09) - -**Note:** Version bump only for package @ionic/cli-framework - - - - - -## [2.1.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@2.1.0...@ionic/cli-framework@2.1.1) (2019-06-28) - -**Note:** Version bump only for package @ionic/cli-framework - - - - - -# [2.1.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@2.0.3...@ionic/cli-framework@2.1.0) (2019-06-21) - - -### Features - -* **validators:** `combine` for combining validators into one ([bcef698](https://github.com/ionic-team/ionic-cli/commit/bcef698)) - - - - - -## [2.0.3](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@2.0.2...@ionic/cli-framework@2.0.3) (2019-06-18) - -**Note:** Version bump only for package @ionic/cli-framework - - - - - -## [2.0.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@2.0.1...@ionic/cli-framework@2.0.2) (2019-06-10) - -**Note:** Version bump only for package @ionic/cli-framework - - - - - -## [2.0.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@2.0.0...@ionic/cli-framework@2.0.1) (2019-06-05) - -**Note:** Version bump only for package @ionic/cli-framework - - - - - -# [2.0.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@1.7.0...@ionic/cli-framework@2.0.0) (2019-05-29) - - -### chore - -* **output:** remove unused BottomBar stuff ([e2023d1](https://github.com/ionic-team/ionic-cli/commit/e2023d1)) -* require Node 8 ([5670e68](https://github.com/ionic-team/ionic-cli/commit/5670e68)) - - -### Features - -* Command-Line Completions ([9f66512](https://github.com/ionic-team/ionic-cli/commit/9f66512)) -* **help:** color refactor ([5938429](https://github.com/ionic-team/ionic-cli/commit/5938429)) - - -### BREAKING CHANGES - -* **output:** Remove `BottomBarOutputStrategy` -* A minimum of Node.js 8.9.4 is required. -* **help:** option/command/namespace groups are now `MetadataGroup` - - - - - - -# [1.7.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@1.6.3...@ionic/cli-framework@1.7.0) (2019-03-12) - - -### Features - -* **help:** add flair for paid content ([e7978e2](https://github.com/ionic-team/ionic-cli/commit/e7978e2)) - - - - - -## [1.6.3](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@1.6.2...@ionic/cli-framework@1.6.3) (2019-03-06) - - - - -**Note:** Version bump only for package @ionic/cli-framework - - -## [1.6.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@1.6.1...@ionic/cli-framework@1.6.2) (2019-02-27) - - - - -**Note:** Version bump only for package @ionic/cli-framework - - -## [1.6.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@1.6.0...@ionic/cli-framework@1.6.1) (2019-02-15) - - - - -**Note:** Version bump only for package @ionic/cli-framework - - -# [1.6.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@1.5.3...@ionic/cli-framework@1.6.0) (2019-01-23) - - -### Features - -* **help:** text/link footnotes for descriptions ([2c74d53](https://github.com/ionic-team/ionic-cli/commit/2c74d53)) - - - - - -## [1.5.3](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@1.5.2...@ionic/cli-framework@1.5.3) (2019-01-08) - - - - -**Note:** Version bump only for package @ionic/cli-framework - - -## [1.5.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@1.5.1...@ionic/cli-framework@1.5.2) (2019-01-07) - - - - -**Note:** Version bump only for package @ionic/cli-framework - - -## [1.5.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@1.5.0...@ionic/cli-framework@1.5.1) (2018-12-19) - - - - -**Note:** Version bump only for package @ionic/cli-framework - - -# [1.5.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@1.4.0...@ionic/cli-framework@1.5.0) (2018-11-27) - - -### Features - -* **help:** weaken the color of deprecated commands and options ([66f48d8](https://github.com/ionic-team/ionic-cli/commit/66f48d8)) -* **shell:** error for subprocesses that exit via signal ([3af1cee](https://github.com/ionic-team/ionic-cli/commit/3af1cee)) - - - - - -# [1.4.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@1.3.0...@ionic/cli-framework@1.4.0) (2018-11-20) - - -### Bug Fixes - -* **config:** rewrite config for not found & syntax errors ([13298f1](https://github.com/ionic-team/ionic-cli/commit/13298f1)) -* **terminal:** expand upon and fix Windows shell detection ([c3aa5a1](https://github.com/ionic-team/ionic-cli/commit/c3aa5a1)), closes [/github.com/ionic-team/ionic-cli/commit/dcc912d889011afd5944eaf4829bc8e5b4d0f4b6#commitcomment-31280556](https://github.com//github.com/ionic-team/ionic-cli/commit/dcc912d889011afd5944eaf4829bc8e5b4d0f4b6/issues/commitcomment-31280556) - - -### Features - -* **help:** spec for customizing option help schema ([73830bc](https://github.com/ionic-team/ionic-cli/commit/73830bc)) - - - - - -# [1.3.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@1.2.0...@ionic/cli-framework@1.3.0) (2018-11-04) - - -### Bug Fixes - -* **start:** fix stdio freezing issue on Windows ([#3725](https://github.com/ionic-team/ionic-cli/issues/3725)) ([a570770](https://github.com/ionic-team/ionic-cli/commit/a570770)) - - -### Features - -* **fn:** add function for resolving values from function series ([ac97f78](https://github.com/ionic-team/ionic-cli/commit/ac97f78)) - - - - - -# [1.2.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@1.1.1...@ionic/cli-framework@1.2.0) (2018-10-31) - - -### Bug Fixes - -* **config:** write json file with ending newline ([dc3f2a5](https://github.com/ionic-team/ionic-cli/commit/dc3f2a5)) -* **help:** filter out unnecessary global options ([7809c99](https://github.com/ionic-team/ionic-cli/commit/7809c99)) -* **process:** keep node running for `sleepForever()` ([ea08f8d](https://github.com/ionic-team/ionic-cli/commit/ea08f8d)) - - -### Features - -* **namespace:** add `useAliases` option to `locate()` ([489617b](https://github.com/ionic-team/ionic-cli/commit/489617b)) -* **object:** add `keysWithoutAliases()` method to `AliasedMap` ([f4807f4](https://github.com/ionic-team/ionic-cli/commit/f4807f4)) -* **terminal:** detect windows shell ([dcc912d](https://github.com/ionic-team/ionic-cli/commit/dcc912d)) - - - - - -## [1.1.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@1.1.0...@ionic/cli-framework@1.1.1) (2018-10-05) - - - - -**Note:** Version bump only for package @ionic/cli-framework - - -# [1.1.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@1.0.7...@ionic/cli-framework@1.1.0) (2018-10-03) - - -### Features - -* **shell:** accept pathed name ([7e6c713](https://github.com/ionic-team/ionic-cli/commit/7e6c713)) -* **shell:** provide which utility for finding binaries ([4dbe922](https://github.com/ionic-team/ionic-cli/commit/4dbe922)) - - - - - -## [1.0.7](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@1.0.6...@ionic/cli-framework@1.0.7) (2018-09-05) - - - - -**Note:** Version bump only for package @ionic/cli-framework - - -## [1.0.6](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@1.0.5...@ionic/cli-framework@1.0.6) (2018-08-20) - - -### Bug Fixes - -* **process:** catch and log errors in exit queue ([f3cd886](https://github.com/ionic-team/ionic-cli/commit/f3cd886)) - - - - - -## [1.0.5](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@1.0.4...@ionic/cli-framework@1.0.5) (2018-08-09) - - -### Bug Fixes - -* **serve:** fix unclosed connection issue again ([#3500](https://github.com/ionic-team/ionic-cli/issues/3500)) ([1f0ef3b](https://github.com/ionic-team/ionic-cli/commit/1f0ef3b)) - - - - - -## [1.0.4](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@1.0.3...@ionic/cli-framework@1.0.4) (2018-08-07) - - - - -**Note:** Version bump only for package @ionic/cli-framework - - -## [1.0.3](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@1.0.2...@ionic/cli-framework@1.0.3) (2018-08-06) - - -### Bug Fixes - -* **serve:** properly cleanup child processes ([#3481](https://github.com/ionic-team/ionic-cli/issues/3481)) ([38217bf](https://github.com/ionic-team/ionic-cli/commit/38217bf)) -* **serve:** use 127.0.0.1 to attempt connections ([#3476](https://github.com/ionic-team/ionic-cli/issues/3476)) ([12c3f35](https://github.com/ionic-team/ionic-cli/commit/12c3f35)) - - - - - -## [1.0.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@1.0.1...@ionic/cli-framework@1.0.2) (2018-08-02) - - -### Bug Fixes - -* **serve:** check all network interfaces for an available port ([30fd6ef](https://github.com/ionic-team/ionic-cli/commit/30fd6ef)) - - - - - -## [1.0.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@1.0.0...@ionic/cli-framework@1.0.1) (2018-07-30) - - - - -**Note:** Version bump only for package @ionic/cli-framework - - -# [1.0.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@1.0.0-rc.13...@ionic/cli-framework@1.0.0) (2018-07-25) - - - - -**Note:** Version bump only for package @ionic/cli-framework diff --git a/packages/@ionic/cli-framework/LICENSE b/packages/@ionic/cli-framework/LICENSE deleted file mode 100644 index 7c5808ced6..0000000000 --- a/packages/@ionic/cli-framework/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2017 Drifty Co - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/packages/@ionic/cli-framework/README.md b/packages/@ionic/cli-framework/README.md deleted file mode 100644 index a83dedf9c0..0000000000 --- a/packages/@ionic/cli-framework/README.md +++ /dev/null @@ -1,56 +0,0 @@ -# CLI Framework, by Ionic - -The foundation framework of the Ionic CLI. - -## Getting Started - -`index.js`: -```js -const { Command, CommandMap, Namespace, execute } = require('@ionic/cli-framework'); - -class VersionCommand extends Command { - async getMetadata() { - return { - name: 'version', - summary: 'Print CLI version', - }; - } - - async run() { - console.log('1.0.0'); - } -} - -class RootNamespace extends Namespace { - async getMetadata() { - return { - name: 'mynewcli', - summary: 'A cool CLI that prints its own version', - }; - } - - async getCommands() { - return new CommandMap([['version', async () => new VersionCommand(this)]]); - } -} - -module.exports = async function(argv, env) { - await execute({ namespace: new RootNamespace(), argv, env }); -} -``` - -`bin/mynewcli`: -```javascript -#!/usr/bin/env node - -const run = require('../'); -run(process.argv.slice(2), process.env); -``` - -command line: - -```bash -$ ./bin/mynewcli -$ ./bin/mynewcli version -$ ./bin/mynewcli version --help -``` diff --git a/packages/@ionic/cli-framework/jest.config.js b/packages/@ionic/cli-framework/jest.config.js deleted file mode 100644 index eace8d6d9d..0000000000 --- a/packages/@ionic/cli-framework/jest.config.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('../../../jest.config.base'); diff --git a/packages/@ionic/cli-framework/lint-staged.config.js b/packages/@ionic/cli-framework/lint-staged.config.js deleted file mode 100644 index 5815a72f0d..0000000000 --- a/packages/@ionic/cli-framework/lint-staged.config.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('../../../lint-staged.config.base'); diff --git a/packages/@ionic/cli-framework/package.json b/packages/@ionic/cli-framework/package.json deleted file mode 100644 index d3b7b99dfe..0000000000 --- a/packages/@ionic/cli-framework/package.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "name": "@ionic/cli-framework", - "version": "6.0.1", - "description": "The foundation framework of the Ionic CLI", - "homepage": "https://ionicframework.com/", - "author": "Ionic Team (https://ionicframework.com) ", - "main": "./index.js", - "types": "./index.d.ts", - "engines": { - "node": ">=16.0.0" - }, - "repository": { - "type": "git", - "url": "https://github.com/ionic-team/ionic-cli.git" - }, - "bugs": { - "url": "https://github.com/ionic-team/ionic-cli/issues" - }, - "scripts": { - "clean": "rimraf index.* definitions.* errors.* guards.* lib utils", - "lint": "true", - "build": "npm run clean && tsc", - "watch": "tsc -w --preserveWatchOutput", - "test": "jest --maxWorkers=4", - "prepublishOnly": "npm run build" - }, - "license": "MIT", - "dependencies": { - "@ionic/cli-framework-output": "2.2.8", - "@ionic/utils-array": "2.1.6", - "@ionic/utils-fs": "3.1.7", - "@ionic/utils-object": "2.1.6", - "@ionic/utils-process": "2.1.12", - "@ionic/utils-stream": "3.1.7", - "@ionic/utils-subprocess": "3.0.1", - "@ionic/utils-terminal": "2.3.5", - "chalk": "^4.0.0", - "debug": "^4.0.0", - "lodash": "^4.17.5", - "minimist": "^1.2.0", - "rimraf": "^3.0.0", - "tslib": "^2.0.1", - "write-file-atomic": "^3.0.0" - }, - "devDependencies": { - "@types/debug": "^4.1.1", - "@types/jest": "^26.0.10", - "@types/lodash": "^4.14.104", - "@types/minimist": "^1.2.0", - "@types/node": "~16.0.0", - "@types/write-file-atomic": "^3.0.0", - "jest": "^26.4.2", - "jest-cli": "^26.0.1", - "lint-staged": "^10.0.2", - "ts-jest": "~26.3.0", - "typescript": "~4.8.0" - } -} diff --git a/packages/@ionic/cli-framework/src/definitions.ts b/packages/@ionic/cli-framework/src/definitions.ts deleted file mode 100644 index c135392d5f..0000000000 --- a/packages/@ionic/cli-framework/src/definitions.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { Opts as ParseArgsOptions, ParsedArgs } from 'minimist'; - -export type ParsedArg = string | boolean | null | undefined | string[]; -export type Validator = (input?: string, key?: string) => true | string; - -export type CommandLineInputs = string[]; - -export interface CommandLineOptions extends ParsedArgs { - [arg: string]: ParsedArg; -} - -export type CommandOptionType = StringConstructor | BooleanConstructor; - -export interface CommandMetadataInput { - name: string; - summary: string; - validators?: Validator[]; -} - -export interface TextFootnote { - id: string | number; - text: string; -} - -export interface LinkFootnote { - id: string | number; - url: string; - shortUrl?: string; -} - -export type Footnote = TextFootnote | LinkFootnote; - -export const enum MetadataGroup { - ADVANCED = 'advanced', - BETA = 'beta', - DEPRECATED = 'deprecated', - EXPERIMENTAL = 'experimental', - HIDDEN = 'hidden', - PAID = 'paid', -} - -export interface Metadata { - name: string; - summary: string; - description?: string; - footnotes?: Footnote[]; - groups?: string[]; -} - -export interface CommandMetadataOption extends Metadata { - type?: CommandOptionType; - default?: string | boolean; - aliases?: string[]; - spec?: { - value?: string; - }; -} - -export { ParseArgsOptions }; - -export interface HydratedParseArgsOptions extends ParseArgsOptions { - string: string[]; - boolean: string[]; - alias: { [key: string]: string[]; }; - default: { [key: string]: string | boolean; }; -} - -export interface CommandMetadata extends Metadata { - exampleCommands?: string[]; - inputs?: I[]; - options?: O[]; -} - -export interface CommandInstanceInfo, N extends INamespace, M extends CommandMetadata, I extends CommandMetadataInput, O extends CommandMetadataOption> { - location: NamespaceLocateResult; - env: NodeJS.ProcessEnv; - executor: IExecutor; -} - -export interface ICommand, N extends INamespace, M extends CommandMetadata, I extends CommandMetadataInput, O extends CommandMetadataOption> { - namespace: N; - - getMetadata(runinfo?: Partial>): Promise; - run(inputs: CommandLineInputs, options: CommandLineOptions, runinfo?: Partial>): Promise; - validate(argv: CommandLineInputs): Promise; -} - -export type CommandMapGetter, N extends INamespace, M extends CommandMetadata, I extends CommandMetadataInput, O extends CommandMetadataOption> = () => Promise; -export type NamespaceMapGetter, N extends INamespace, M extends CommandMetadata, I extends CommandMetadataInput, O extends CommandMetadataOption> = () => Promise; -export type ICommandMap, N extends INamespace, M extends CommandMetadata, I extends CommandMetadataInput, O extends CommandMetadataOption> = import('@ionic/utils-object').AliasedMap>; -export type INamespaceMap, N extends INamespace, M extends CommandMetadata, I extends CommandMetadataInput, O extends CommandMetadataOption> = import('@ionic/utils-object').AliasedMap>; - -export interface NamespaceLocateOptions { - useAliases?: boolean; -} - -export interface INamespace, N extends INamespace, M extends CommandMetadata, I extends CommandMetadataInput, O extends CommandMetadataOption> { - root: N; - parent: N | undefined; - - locate(argv: readonly string[], options?: NamespaceLocateOptions): Promise>; - getMetadata(): Promise; - getNamespaces(): Promise>; - - getCommands(): Promise>; - getCommandMetadataList(): Promise[]>; - groupCommandsByNamespace(commands: readonly HydratedCommandMetadata[]): Promise & { commands: readonly HydratedCommandMetadata[]; })[]>; -} - -export type CommandPathItem, N extends INamespace, M extends CommandMetadata, I extends CommandMetadataInput, O extends CommandMetadataOption> = [string, C | N]; - -export interface NamespaceLocateResult, N extends INamespace, M extends CommandMetadata, I extends CommandMetadataInput, O extends CommandMetadataOption> { - readonly obj: C | N; - readonly args: readonly string[]; - readonly path: readonly CommandPathItem[]; -} - -export type HydratedCommandMetadata, N extends INamespace, M extends CommandMetadata, I extends CommandMetadataInput, O extends CommandMetadataOption> = M & { - readonly command: C; - readonly namespace: N; - readonly path: readonly CommandPathItem[]; - readonly aliases: readonly string[]; -}; - -export interface NamespaceMetadata extends Metadata {} - -export interface HydratedNamespaceMetadata, N extends INamespace, M extends CommandMetadata, I extends CommandMetadataInput, O extends CommandMetadataOption> extends Required> { - readonly namespace: N; - readonly aliases: readonly string[]; -} - -export interface IExecutor, N extends INamespace, M extends CommandMetadata, I extends CommandMetadataInput, O extends CommandMetadataOption> { - readonly namespace: N; - - locate(argv: readonly string[]): Promise>; - execute(location: NamespaceLocateResult): Promise; - execute(argv: readonly string[], env: NodeJS.ProcessEnv): Promise; - run(command: C, cmdargs: readonly string[], runinfo?: Partial>): Promise; -} - -export interface PackageJson { - name: string; - version: string; - main?: string; - description?: string; - bin?: { [key: string]: string | undefined; }; - scripts?: { [key: string]: string | undefined; }; - dependencies?: { [key: string]: string | undefined; }; - devDependencies?: { [key: string]: string | undefined; }; -} - -export interface Validators { - required: Validator; - email: Validator; - numeric: Validator; - url: Validator; - slug: Validator; -} - -export interface ValidationError { - key: string; - message: string; - validator: Validator; -} diff --git a/packages/@ionic/cli-framework/src/errors.ts b/packages/@ionic/cli-framework/src/errors.ts deleted file mode 100644 index cef93d4469..0000000000 --- a/packages/@ionic/cli-framework/src/errors.ts +++ /dev/null @@ -1,44 +0,0 @@ -import * as util from 'util'; - -import { ValidationError } from './definitions'; - -export const ERROR_INPUT_VALIDATION = 'ERR_ICF_INPUT_VALIDATION'; -export const ERROR_COMMAND_NOT_FOUND = 'ERR_ICF_COMMAND_NOT_FOUND'; -export const ERROR_IPC_MODULE_INACCESSIBLE = 'ERR_ICF_IPC_MODULE_INACCESSIBLE'; -export const ERROR_IPC_UNKNOWN_PROCEDURE = 'ERR_ICF_IPC_UNKNOWN_PROCEDURE'; - -export abstract class BaseError extends Error { - abstract readonly name: string; - code?: string; - exitCode?: number; - - toString(): string { - return util.inspect(this); - } - - inspect(): string { - return this.toString(); - } -} - -export class InputValidationError extends BaseError { - readonly name = 'InputValidationError'; - code = ERROR_INPUT_VALIDATION; - - constructor(message: string, public errors: ValidationError[]) { - super(message); - } -} - -export class CommandNotFoundError extends BaseError { - readonly name = 'CommandNotFoundError'; - code = ERROR_COMMAND_NOT_FOUND; - - constructor(message: string, public args: string[]) { - super(message); - } -} - -export class IPCError extends BaseError { - readonly name = 'IPCError'; -} diff --git a/packages/@ionic/cli-framework/src/guards.ts b/packages/@ionic/cli-framework/src/guards.ts deleted file mode 100644 index 22b3a082ce..0000000000 --- a/packages/@ionic/cli-framework/src/guards.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { CommandMetadata, CommandMetadataInput, CommandMetadataOption, HydratedCommandMetadata, ICommand, INamespace, LinkFootnote, PackageJson } from './definitions'; - -export function isNamespace, N extends INamespace, M extends CommandMetadata, I extends CommandMetadataInput, O extends CommandMetadataOption>(obj: any): obj is N { - return obj && - typeof obj.getMetadata === 'function' && - typeof obj.getNamespaces === 'function' && - typeof obj.getCommands === 'function'; -} - -export function isCommand, N extends INamespace, M extends CommandMetadata, I extends CommandMetadataInput, O extends CommandMetadataOption>(obj: any): obj is C { - return obj && isNamespace(obj.namespace) && - typeof obj.getMetadata === 'function' && - typeof obj.run === 'function' && - typeof obj.validate === 'function'; -} - -export function isHydratedCommandMetadata, N extends INamespace, M extends CommandMetadata, I extends CommandMetadataInput, O extends CommandMetadataOption>(obj: any): obj is HydratedCommandMetadata { - return obj && - typeof obj.name === 'string' && - isCommand(obj.command) && - isNamespace(obj.namespace) && - Array.isArray(obj.path) && - Array.isArray(obj.aliases); -} - -export function isPackageJson(obj: any): obj is PackageJson { - return obj && typeof obj.name === 'string'; -} - -export function isLinkFootnote(obj: any): obj is LinkFootnote { - return obj && - (typeof obj.id === 'number' || typeof obj.id === 'string') && - typeof obj.url === 'string'; -} diff --git a/packages/@ionic/cli-framework/src/index.ts b/packages/@ionic/cli-framework/src/index.ts deleted file mode 100644 index eae7e3b383..0000000000 --- a/packages/@ionic/cli-framework/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './definitions'; -export * from './errors'; -export * from './lib'; diff --git a/packages/@ionic/cli-framework/src/lib/__tests__/command.ts b/packages/@ionic/cli-framework/src/lib/__tests__/command.ts deleted file mode 100644 index 7226d3b25c..0000000000 --- a/packages/@ionic/cli-framework/src/lib/__tests__/command.ts +++ /dev/null @@ -1,385 +0,0 @@ -import { CommandMetadata } from '../../definitions'; -import { Command, CommandMap, CommandMapDefault, Namespace, NamespaceMap, generateCommandPath } from '../command'; - -class MyNamespace extends Namespace { - async getMetadata(): Promise { - return { - name: 'my', - summary: '', - }; - } - - async getNamespaces(): Promise { - return new NamespaceMap([ - ['foo', async () => new FooNamespace(this)], - ['defns', async () => new NamespaceWithDefault(this)], - ['f', 'foo'], - ]); - } -} - -class NamespaceWithDefault extends Namespace { - async getMetadata(): Promise { - return { - name: 'defns', - summary: '', - }; - } - - async getCommands(): Promise { - return new CommandMap([ - [CommandMapDefault, async () => new DefaultCommand(this)], - ]); - } -} - -class FooNamespace extends Namespace { - async getMetadata(): Promise { - return { - name: 'foo', - summary: '', - }; - } - - async getCommands(): Promise { - return new CommandMap([ - ['bar', async () => new BarCommand(this)], - ['baz', async () => new BazCommand(this)], - ['b', 'bar'], - ]); - } -} - -class EmptyNamespace extends Namespace { - async getMetadata(): Promise { - return { - name: 'empty', - summary: '', - }; - } -} - -class DefaultCommand extends Command { - async getMetadata(): Promise { - return { - name: 'def', - summary: '', - }; - } - - async run() {} -} - -class BarCommand extends Command { - async getMetadata(): Promise { - return { - name: 'bar', - summary: '', - }; - } - - async run() {} -} - -class BazCommand extends Command { - async getMetadata(): Promise { - return { - name: 'baz', - summary: '', - }; - } - - async run() {} -} - -describe('@ionic/cli-framework', () => { - - describe('lib/command', () => { - - describe('Namespace', () => { - - describe('root and parent', () => { - - it('should have root attribute', async () => { - const testNamespace = async (ns: Namespace) => { - const namespaces = await ns.getNamespaces(); - - for (let [ , nsgetter ] of namespaces.entries()) { - if (typeof nsgetter !== 'string' && typeof nsgetter !== 'symbol') { - const namespace = await nsgetter(); - expect(namespace.root).toBe(myns); - await testNamespace(namespace); - } - } - }; - - const myns = new MyNamespace(); - await testNamespace(myns); - }); - - it('should have parent attribute', async () => { - const testNamespace = async (ns: Namespace) => { - const namespaces = await ns.getNamespaces(); - - for (let [ , nsgetter ] of namespaces.entries()) { - if (typeof nsgetter !== 'string' && typeof nsgetter !== 'symbol') { - const namespace = await nsgetter(); - expect(namespace.parent).toBe(ns); - await testNamespace(namespace); - } - } - }; - - const myns = new MyNamespace(); - await testNamespace(myns); - }); - - }); - - describe('getNamespaces', () => { - - it('should get subnamespaces', async () => { - const testNamespace = async (ns: Namespace) => { - const commands = await ns.getCommands(); - const namespaces = await ns.getNamespaces(); - - for (let [ , cmdgetter ] of commands.entries()) { - if (typeof cmdgetter === 'function') { - const cmd = await cmdgetter(); - expect(cmd.namespace).toBe(ns); - } - } - - for (let [ , nsgetter ] of namespaces.entries()) { - if (typeof nsgetter !== 'string' && typeof nsgetter !== 'symbol') { - const namespace = await nsgetter(); - expect(namespace.parent).toBe(ns); - await testNamespace(namespace); - } - } - }; - - const myns = new MyNamespace(); - await testNamespace(myns); - }); - - }); - - describe('locate', () => { - - it('should locate root namespace with no args', async () => { - const ns = new MyNamespace(); - const { args, obj, path } = await ns.locate([]); - expect(args).toEqual([]); - expect(ns).toBe(obj); - expect(path.length).toEqual(1); - const [ ns1 ] = path; - expect(ns1[0]).toEqual('my'); - expect(ns1[1]).toBe(ns); - }); - - it('should locate root namespace with args with no commands or namespaces', async () => { - const ns = new MyNamespace(); - const { args, obj, path } = await ns.locate(['x', 'y', 'z']); - expect(args).toEqual(['x', 'y', 'z']); - expect(ns).toBe(obj); - expect(path.length).toEqual(1); - const [ ns1 ] = path; - expect(ns1[0]).toEqual('my'); - expect(ns1[1]).toBe(ns); - }); - - it('should locate foo namespace', async () => { - const ns = new MyNamespace(); - const { args, obj, path } = await ns.locate(['foo']); - expect(args).toEqual([]); - expect(obj).toBeInstanceOf(FooNamespace); - const metadata = await obj.getMetadata(); - expect(metadata.name).toEqual('foo'); - expect(path.length).toEqual(2); - const [ ns1, ns2 ] = path; - expect(ns1[0]).toEqual('my'); - expect(ns1[1]).toBe(ns); - expect(ns2[0]).toEqual('foo'); - expect(ns2[1]).toBeInstanceOf(FooNamespace); - }); - - it('should locate default command', async () => { - const ns = new MyNamespace(); - const { args, obj, path } = await ns.locate(['defns']); - expect(args).toEqual([]); - expect(obj).toBeInstanceOf(DefaultCommand); - const metadata = await obj.getMetadata(); - expect(metadata.name).toEqual('def'); - expect(path.length).toEqual(2); - const [ ns1, cmd2 ] = path; - expect(ns1[0]).toEqual('my'); - expect(ns1[1]).toBe(ns); - expect(cmd2[0]).toEqual('defns'); - expect(cmd2[1]).toBeInstanceOf(DefaultCommand); - }); - - it('should locate bar command in foo namespace', async () => { - const ns = new MyNamespace(); - const { args, obj, path } = await ns.locate(['foo', 'bar', 'arg1', 'arg2']); - expect(args).toEqual(['arg1', 'arg2']); - expect(obj).toBeInstanceOf(BarCommand); - expect(path.length).toEqual(3); - const [ ns1, ns2, cmd3 ] = path; - expect(ns1[0]).toEqual('my'); - expect(ns1[1]).toBe(ns); - expect(ns2[0]).toEqual('foo'); - expect(ns2[1]).toBeInstanceOf(FooNamespace); - expect(cmd3[0]).toEqual('bar'); - expect(cmd3[1]).toBeInstanceOf(BarCommand); - }); - - it('should locate bar command in foo namespace by alias', async () => { - const ns = new MyNamespace(); - const { args, obj, path } = await ns.locate(['foo', 'b', 'arg1']); - expect(args).toEqual(['arg1']); - expect(obj).toBeInstanceOf(BarCommand); - expect(path.length).toEqual(3); - const [ ns1, ns2, cmd3 ] = path; - expect(ns1[0]).toEqual('my'); - expect(ns1[1]).toBe(ns); - expect(ns2[0]).toEqual('foo'); - expect(ns2[1]).toBeInstanceOf(FooNamespace); - expect(cmd3[0]).toEqual('b'); - expect(cmd3[1]).toBeInstanceOf(BarCommand); - }); - - it('should locate bar command in foo namespace by aliases', async () => { - const ns = new MyNamespace(); - const { args, obj, path } = await ns.locate(['f', 'b', 'arg1']); - expect(args).toEqual(['arg1']); - expect(obj).toBeInstanceOf(BarCommand); - expect(path.length).toEqual(3); - const [ ns1, ns2, cmd3 ] = path; - expect(ns1[0]).toEqual('my'); - expect(ns1[1]).toBe(ns); - expect(ns2[0]).toEqual('f'); - expect(ns2[1]).toBeInstanceOf(FooNamespace); - expect(cmd3[0]).toEqual('b'); - expect(cmd3[1]).toBeInstanceOf(BarCommand); - }); - - }); - - describe('getCommandMetadataList', () => { - - it('should return empty array for empty namespace', async () => { - const ns = new EmptyNamespace(); - const result = await ns.getCommandMetadataList(); - expect(result).toEqual([]); - }); - - it('should have namespace path without command path for default command', async () => { - const ns = new NamespaceWithDefault(); - const result = await ns.getCommandMetadataList(); - expect(result.length).toEqual(1); - expect(result[0].command).toBeInstanceOf(DefaultCommand); - expect(result[0].namespace).toBe(ns); - expect(result[0].path).toEqual([['defns', ns]]); - }); - - it('should have correct path for command', async () => { - const ns = new FooNamespace(); - const result = await ns.getCommandMetadataList(); - expect(result.length).toEqual(2); - const [ barcmd, bazcmd ] = result; - expect(barcmd.command).toBeInstanceOf(BarCommand); - expect(barcmd.namespace).toBe(ns); - expect(barcmd.path.length).toEqual(2); - expect(barcmd.path[0][0]).toEqual('foo'); - expect(barcmd.path[0][1]).toBe(ns); - expect(barcmd.path[1][0]).toEqual('bar'); - expect(barcmd.path[1][1]).toBeInstanceOf(BarCommand); - expect(bazcmd.command).toBeInstanceOf(BazCommand); - expect(bazcmd.namespace).toBe(ns); - expect(bazcmd.path.length).toEqual(2); - expect(bazcmd.path[0][0]).toEqual('foo'); - expect(barcmd.path[0][1]).toBe(ns); - expect(bazcmd.path[1][0]).toEqual('baz'); - expect(bazcmd.path[1][1]).toBeInstanceOf(BazCommand); - }); - - it('should have correct path for command in sub-namespace', async () => { - const ns = new MyNamespace(); - const result = await ns.getCommandMetadataList(); - const bar = result.find(c => c.command instanceof BarCommand); - if (!bar) { - throw new Error('bar not defined'); - } - expect(bar.path.length).toEqual(3); - expect(bar.path[0][0]).toEqual('my'); - expect(bar.path[0][1]).toBe(ns); - expect(bar.path[1][0]).toEqual('foo'); - expect(bar.path[1][1]).toBeInstanceOf(FooNamespace); - expect(bar.path[2][0]).toEqual('bar'); - expect(bar.path[2][1]).toBeInstanceOf(BarCommand); - }); - - it('should work', async () => { - const ns = new MyNamespace(); - const result = await ns.getCommandMetadataList(); - expect(result.length).toEqual(3); - const bar = result.find(c => c.command instanceof BarCommand); - if (!bar) { - throw new Error('bar not defined'); - } - const baz = result.find(c => c.command instanceof BazCommand); - if (!baz) { - throw new Error('baz not defined'); - } - const def = result.find(c => c.command instanceof DefaultCommand); - if (!def) { - throw new Error('def not defined'); - } - expect(bar.aliases).toEqual(['my foo b']); - expect(def.aliases).toEqual([]); - expect(baz.aliases).toEqual([]); - }); - - }); - - }); - - describe('generateCommandPath', () => { - - // TODO: default commands? - - it('should get path for single namespace and command', async () => { - const ns = new FooNamespace(); - const { obj } = await ns.locate(['bar']); - const result = await generateCommandPath(obj as any); - expect(result.length).toEqual(2); - const [ foons, barcmd ] = result; - expect(foons).toBeDefined(); - expect(foons[0]).toEqual('foo'); - expect(foons[1]).toBe(ns); - expect(barcmd).toBeDefined(); - expect(barcmd[0]).toEqual('bar'); - expect(barcmd[1]).toBeInstanceOf(BarCommand); - }); - - it('should work back through nested namespace', async () => { - const ns = new MyNamespace(); - const { obj } = await ns.locate(['foo', 'bar']); - const result = await generateCommandPath(obj as any); - expect(result.length).toEqual(3); - const [ rootns, foons, barcmd ] = result; - expect(rootns).toEqual(['my', ns]); - expect(foons).toBeDefined(); - expect(foons[0]).toEqual('foo'); - expect(foons[1]).toBeInstanceOf(FooNamespace); - expect(barcmd).toBeDefined(); - expect(barcmd[0]).toEqual('bar'); - expect(barcmd[1]).toBeInstanceOf(BarCommand); - }); - - }); - - }); - -}); diff --git a/packages/@ionic/cli-framework/src/lib/__tests__/completion.ts b/packages/@ionic/cli-framework/src/lib/__tests__/completion.ts deleted file mode 100644 index 3928abcc90..0000000000 --- a/packages/@ionic/cli-framework/src/lib/__tests__/completion.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { Command, CommandMap, CommandMapDefault, Namespace, NamespaceMap } from '../command'; -import { CommandMetadata, NamespaceMetadata } from '../../definitions'; - -import { getCompletionWords } from '../completion'; - -class MyNamespace extends Namespace { - async getMetadata(): Promise { - return { - name: 'my', - summary: '', - }; - } - - async getNamespaces(): Promise { - return new NamespaceMap([ - ['foo', async () => new FooNamespace(this)], - ['defns', async () => new NamespaceWithDefault(this)], - ['f', 'foo'], - ]); - } -} - -class NamespaceWithDefault extends Namespace { - async getMetadata(): Promise { - return { - name: 'defns', - summary: '', - }; - } - - async getCommands(): Promise { - return new CommandMap([ - [CommandMapDefault, async () => new DefaultCommand(this)], - ]); - } -} - -class FooNamespace extends Namespace { - async getMetadata(): Promise { - return { - name: 'foo', - summary: '', - }; - } - - async getCommands(): Promise { - return new CommandMap([ - ['bar', async () => new BarCommand(this)], - ['baz', async () => new BazCommand(this)], - ['b', 'bar'], - ]); - } -} - -class EmptyNamespace extends Namespace { - async getMetadata(): Promise { - return { - name: 'empty', - summary: '' - }; - } -} - -class DefaultCommand extends Command { - async getMetadata(): Promise { - return { - name: 'def', - summary: '', - options: [ - { - name: 'str-opt', - summary: '', - }, - { - name: 'bool-opt', - summary: '', - type: Boolean, - default: true, - }, - ], - }; - } - - async run() {} -} - -class BarCommand extends Command { - async getMetadata(): Promise { - return { - name: 'bar', - summary: '', - options: [ - { - name: 'str-opt', - summary: '', - }, - { - name: 'bool-opt', - summary: '', - type: Boolean, - default: true, - }, - ], - }; - } - - async run() {} -} - -class BazCommand extends Command { - async getMetadata(): Promise { - return { - name: 'baz', - summary: '', - }; - } - - async run() {} -} - -describe('@ionic/cli-framework', () => { - - describe('lib/completion', () => { - - describe('getCompletionWords', () => { - - it('should have no words for empty namespace', async () => { - const ns = new EmptyNamespace(); - const words = await getCompletionWords(ns, []); - expect(words).toEqual([]); - }); - - it('should return command words for a namespace', async () => { - const ns = new FooNamespace(); - const words = await getCompletionWords(ns, []); - expect(words).toEqual(['bar', 'baz']); - }); - - it('should return command and namespace words for a namespace', async () => { - const ns = new MyNamespace(); - const words = await getCompletionWords(ns, []); - expect(words).toEqual(['defns', 'foo']); - }); - - it('should return options from a default namespace', async () => { - const ns = new MyNamespace(); - debugger; - const words = await getCompletionWords(ns, ['defns']); - expect(words).toEqual(['--no-bool-opt', '--str-opt']); - }); - - it('should return options from a command', async () => { - const ns = new MyNamespace(); - debugger; - const words = await getCompletionWords(ns, ['foo', 'bar']); - expect(words).toEqual(['--no-bool-opt', '--str-opt']); - }); - - it('should return unique options from a command', async () => { - const ns = new MyNamespace(); - debugger; - const words = await getCompletionWords(ns, ['foo', 'bar', '--str-opt']); - expect(words).toEqual(['--no-bool-opt']); - }); - - }) - - }); - -}); diff --git a/packages/@ionic/cli-framework/src/lib/__tests__/config.ts b/packages/@ionic/cli-framework/src/lib/__tests__/config.ts deleted file mode 100644 index fa1c0e5759..0000000000 --- a/packages/@ionic/cli-framework/src/lib/__tests__/config.ts +++ /dev/null @@ -1,306 +0,0 @@ -describe('@ionic/cli-framework', () => { - - describe('BaseConfig', () => { - - let Config: any; - let mockReadFileSync: jest.Mock; - let mockMakeDirSync: jest.Mock; - let mockWriteFileAtomicSync: jest.Mock; - - beforeEach(async () => { - mockReadFileSync = jest.fn(); - mockMakeDirSync = jest.fn(); - mockWriteFileAtomicSync = jest.fn(); - jest.resetModules(); - jest.mock('fs', () => ({ readFileSync: mockReadFileSync })); - jest.mock('@ionic/utils-fs', () => ({ mkdirpSync: mockMakeDirSync })); - jest.mock('write-file-atomic', () => ({ sync: mockWriteFileAtomicSync })); - - const { BaseConfig } = await import('../config'); - - Config = class extends BaseConfig { - provideDefaults() { - return { - defaulted: 5, - }; - } - }; - }); - - it('should store config file path', () => { - const config = new Config('/path/to/file'); - expect(config.p).toEqual('/path/to/file'); - }); - - it('should call mkdirpSync upon setting c', () => { - const config = new Config('/path/to/file'); - config.c = {}; - expect(mockMakeDirSync).toHaveBeenCalledTimes(1); - expect(mockMakeDirSync).toHaveBeenCalledWith('/path/to'); - }); - - it('should call writeFileAtomicSync upon setting c', () => { - const config = new Config('/path/to/file'); - config.c = {}; - expect(mockWriteFileAtomicSync).toHaveBeenCalledTimes(1); - expect(mockWriteFileAtomicSync).toHaveBeenCalledWith('/path/to/file', '{}\n'); - }); - - it('should call readFileSync upon getting c', () => { - const config = new Config('/path/to/file'); - mockReadFileSync.mockImplementationOnce(() => '{}'); - config.c; - expect(mockReadFileSync).toHaveBeenCalledTimes(1); - expect(mockReadFileSync).toHaveBeenCalledWith('/path/to/file', 'utf8'); - }); - - it('should provide defaults upon getting c', () => { - const config = new Config('/path/to/file'); - mockReadFileSync.mockImplementationOnce(() => '{}'); - const result = config.c; - expect(result).toEqual({ defaulted: 5 }); - }); - - it('should not override values with defaults upon getting c', () => { - const config = new Config('/path/to/file'); - mockReadFileSync.mockImplementationOnce(() => '{"defaulted":10}'); - const result = config.c; - expect(result).toEqual({ defaulted: 10 }); - }); - - it('should return defaults upon ENOENT', () => { - const config = new Config('/path/to/file'); - mockReadFileSync.mockImplementationOnce(() => { throw { code: 'ENOENT' } }); - const result = config.c; - expect(result).toEqual({ defaulted: 5 }); - }); - - it('should return defaults upon JSON syntax error', () => { - const config = new Config('/path/to/file'); - mockReadFileSync.mockImplementationOnce(() => { throw { name: 'SyntaxError' } }); - const result = config.c; - expect(result).toEqual({ defaulted: 5 }); - }); - - it('should rewrite file with defaults upon JSON syntax error', () => { - const config = new Config('/path/to/file'); - mockReadFileSync.mockImplementationOnce(() => { throw { name: 'SyntaxError' } }); - config.c; - expect(mockWriteFileAtomicSync).toHaveBeenCalledTimes(1); - expect(mockWriteFileAtomicSync).toHaveBeenCalledWith('/path/to/file', JSON.stringify({ defaulted: 5 }, undefined, 2) + '\n'); - }); - - it('should throw upon unknown error', () => { - const config = new Config('/path/to/file'); - mockReadFileSync.mockImplementationOnce(() => { throw new Error('unknown') }); - expect(() => config.c).toThrow('unknown'); - }); - - it('should get value from file', () => { - const config = new Config('/path/to/file'); - mockReadFileSync.mockImplementationOnce(() => '{"foo":5}'); - const result = config.get('foo'); - expect(result).toEqual(5); - }); - - it('should get object value from file', () => { - const config = new Config('/path/to/file'); - mockReadFileSync.mockImplementationOnce(() => '{"foo":{"bar":"baz"}}'); - const result = config.get('foo'); - expect(result).toEqual({ bar: 'baz' }); - }); - - it('should get value from provideDefaults', () => { - const config = new Config('/path/to/file'); - mockReadFileSync.mockImplementationOnce(() => '{}'); - const result = config.get('defaulted'); - expect(result).toEqual(5); - }); - - it('should get unknown value as undefined', () => { - const config = new Config('/path/to/file'); - mockReadFileSync.mockImplementationOnce(() => '{}'); - const result = config.get('unknown'); - expect(result).not.toBeDefined(); - }); - - it('should get value from file even with default provided', () => { - const config = new Config('/path/to/file'); - mockReadFileSync.mockImplementationOnce(() => '{"foo":5}'); - const result = config.get('foo', 10); - expect(result).toEqual(5); - }); - - it('should get default value with default provided if value in file does not exist', () => { - const config = new Config('/path/to/file'); - mockReadFileSync.mockImplementationOnce(() => '{}'); - const result = config.get('foo', 10); - expect(result).toEqual(10); - }); - - it('should set value in file', () => { - const config = new Config('/path/to/file'); - mockReadFileSync.mockImplementationOnce(() => '{}'); - jest.spyOn(config, 'provideDefaults').mockImplementation(() => ({})); - config.set('foo', 5); - expect(mockWriteFileAtomicSync).toHaveBeenCalledTimes(1); - expect(mockWriteFileAtomicSync).toHaveBeenCalledWith('/path/to/file', '{\n "foo": 5\n}\n'); - }); - - it('should unset value in file', () => { - const config = new Config('/path/to/file'); - mockReadFileSync.mockImplementationOnce(() => '{"foo":5}'); - jest.spyOn(config, 'provideDefaults').mockImplementation(() => ({})); - config.unset('foo'); - expect(mockWriteFileAtomicSync).toHaveBeenCalledTimes(1); - expect(mockWriteFileAtomicSync).toHaveBeenCalledWith('/path/to/file', '{}\n'); - }); - - }); - - describe('BaseConfig with pathPrefix', () => { - - let Config: any; - let mockReadFileSync: jest.Mock; - let mockMakeDirSync: jest.Mock; - let mockWriteFileAtomicSync: jest.Mock; - - beforeEach(async () => { - mockReadFileSync = jest.fn(); - mockMakeDirSync = jest.fn(); - mockWriteFileAtomicSync = jest.fn(); - jest.resetModules(); - jest.mock('fs', () => ({ readFileSync: mockReadFileSync })); - jest.mock('@ionic/utils-fs', () => ({ mkdirpSync: mockMakeDirSync })); - jest.mock('write-file-atomic', () => ({ sync: mockWriteFileAtomicSync })); - - const { BaseConfig } = await import('../config'); - - Config = class extends BaseConfig { - provideDefaults() { - return { - defaulted: 5, - }; - } - }; - }); - - it('should provide defaults upon getting c', () => { - const config = new Config('/path/to/file', { pathPrefix: ['sub'] }); - mockReadFileSync.mockImplementationOnce(() => '{}'); - const result = config.c; - expect(result).toEqual({ defaulted: 5 }); - }); - - it('should not override values with defaults upon getting c', () => { - const config = new Config('/path/to/file', { pathPrefix: ['sub'] }); - mockReadFileSync.mockImplementationOnce(() => '{"sub":{"defaulted":10}}'); - const result = config.c; - expect(result).toEqual({ defaulted: 10 }); - }); - - it('should return defaults upon ENOENT', () => { - const config = new Config('/path/to/file', { pathPrefix: ['sub'] }); - mockReadFileSync.mockImplementationOnce(() => { throw { code: 'ENOENT' } }); - const result = config.c; - expect(result).toEqual({ defaulted: 5 }); - }); - - it('should return defaults upon JSON syntax error', () => { - const config = new Config('/path/to/file'); - mockReadFileSync.mockImplementationOnce(() => { throw { name: 'SyntaxError' } }); - const result = config.c; - expect(result).toEqual({ defaulted: 5 }); - }); - - it('should rewrite file with defaults upon JSON syntax error', () => { - const config = new Config('/path/to/file', { pathPrefix: ['sub'] }); - mockReadFileSync.mockImplementationOnce(() => { throw { name: 'SyntaxError' } }); - config.c; - expect(mockWriteFileAtomicSync).toHaveBeenCalledTimes(1); - expect(mockWriteFileAtomicSync).toHaveBeenCalledWith('/path/to/file', JSON.stringify({ sub: { defaulted: 5 } }, undefined, 2) + '\n'); - }); - - it('should throw upon unknown error', () => { - const config = new Config('/path/to/file', { pathPrefix: ['sub'] }); - mockReadFileSync.mockImplementationOnce(() => { throw new Error('unknown') }); - expect(() => config.c).toThrow('unknown'); - }); - - it('should get value from file', () => { - const config = new Config('/path/to/file', { pathPrefix: ['sub'] }); - mockReadFileSync.mockImplementationOnce(() => '{"sub":{"foo":5}}'); - const result = config.get('foo'); - expect(result).toEqual(5); - }); - - it('should get value from file with many nested paths', () => { - const config = new Config('/path/to/file', { pathPrefix: ['foo', 'bar', 'baz'] }); - mockReadFileSync.mockImplementationOnce(() => '{"foo":{"bar":{"baz":{"prop":5}}}}'); - const result = config.get('prop'); - expect(result).toEqual(5); - }); - - it('should get object value from file', () => { - const config = new Config('/path/to/file', { pathPrefix: ['sub'] }); - mockReadFileSync.mockImplementationOnce(() => '{"sub":{"foo":{"bar":"baz"}}}'); - const result = config.get('foo'); - expect(result).toEqual({ bar: 'baz' }); - }); - - it('should get value from provideDefaults', () => { - const config = new Config('/path/to/file', { pathPrefix: ['sub'] }); - mockReadFileSync.mockImplementationOnce(() => '{"sub":{}}'); - const result = config.get('defaulted'); - expect(result).toEqual(5); - }); - - it('should get value from provideDefaults if path prefix does not exist', () => { - const config = new Config('/path/to/file', { pathPrefix: ['sub'] }); - mockReadFileSync.mockImplementationOnce(() => '{}'); - const result = config.get('defaulted'); - expect(result).toEqual(5); - }); - - it('should get unknown value as undefined', () => { - const config = new Config('/path/to/file', { pathPrefix: ['sub'] }); - mockReadFileSync.mockImplementationOnce(() => '{"sub":{}}'); - const result = config.get('unknown'); - expect(result).not.toBeDefined(); - }); - - it('should get value from file even with default provided', () => { - const config = new Config('/path/to/file', { pathPrefix: ['sub'] }); - mockReadFileSync.mockImplementationOnce(() => '{"sub":{"foo":5}}'); - const result = config.get('foo', 10); - expect(result).toEqual(5); - }); - - it('should get default value with default provided if value in file does not exist', () => { - const config = new Config('/path/to/file', { pathPrefix: ['sub'] }); - mockReadFileSync.mockImplementationOnce(() => '{"sub":{}}'); - const result = config.get('foo', 10); - expect(result).toEqual(10); - }); - - it('should set value in file', () => { - const config = new Config('/path/to/file', { pathPrefix: ['sub'] }); - mockReadFileSync.mockImplementation(() => '{"sub":{}}'); - jest.spyOn(config, 'provideDefaults').mockImplementation(() => ({})); - config.set('foo', 5); - expect(mockWriteFileAtomicSync).toHaveBeenCalledTimes(1); - expect(mockWriteFileAtomicSync).toHaveBeenCalledWith('/path/to/file', '{\n "sub": {\n "foo": 5\n }\n}\n'); - }); - - it('should unset value in file', () => { - const config = new Config('/path/to/file', { pathPrefix: ['sub'] }); - mockReadFileSync.mockImplementation(() => '{"sub":{"foo":5}}'); - jest.spyOn(config, 'provideDefaults').mockImplementation(() => ({})); - config.unset('foo'); - expect(mockWriteFileAtomicSync).toHaveBeenCalledTimes(1); - expect(mockWriteFileAtomicSync).toHaveBeenCalledWith('/path/to/file', '{\n "sub": {}\n}\n'); - }); - - }); - -}); diff --git a/packages/@ionic/cli-framework/src/lib/__tests__/executor.ts b/packages/@ionic/cli-framework/src/lib/__tests__/executor.ts deleted file mode 100644 index 8112beb451..0000000000 --- a/packages/@ionic/cli-framework/src/lib/__tests__/executor.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { Command, CommandMap, CommandMapDefault, Namespace, NamespaceMap } from '../command'; -import { CommandMetadata, NamespaceMetadata } from '../../definitions'; - -import { Executor } from '../executor'; - -export const superspy = jest.fn(); - -class MyNamespace extends Namespace { - async getMetadata(): Promise { - return { - name: 'my', - summary: '', - }; - } - - async getNamespaces(): Promise { - return new NamespaceMap([ - ['foo', async () => new FooNamespace(this)], - ['defns', async () => new NamespaceWithDefault(this)], - ]); - } -} - -class NamespaceWithDefault extends Namespace { - async getMetadata(): Promise { - return { - name: 'defns', - summary: '', - }; - } - - async getCommands(): Promise { - return new CommandMap([ - [CommandMapDefault, async () => new DefaultCommand(this)], - ]); - } -} - -class FooNamespace extends Namespace { - async getMetadata(): Promise { - return { - name: 'foo', - summary: '', - }; - } - - async getCommands(): Promise { - return new CommandMap([ - ['bar', async () => new BarCommand(this)], - ['baz', async () => new BazCommand(this)], - ['b', 'bar'], - ]); - } -} - -class DefaultCommand extends Command { - async getMetadata(): Promise { - return { - name: 'def', - summary: '', - }; - } - - async run() {} -} - -class BarCommand extends Command { - async getMetadata(): Promise { - return { - name: 'bar', - summary: '', - options: [ - { - name: 'flag', - summary: 'bool flag', - type: Boolean, - }, - ], - }; - } - - async validate() { - superspy('bar:validate', [...arguments]); - } - - async run() { - superspy('bar:run', [...arguments]); - } -} - -class BazCommand extends Command { - async getMetadata(): Promise { - return { - name: 'baz', - summary: '', - }; - } - - async run() {} -} - -describe('@ionic/cli-framework', () => { - - const spy = jest.spyOn(module.exports, 'superspy'); - - beforeEach(() => { - spy.mockReset(); - }); - - describe('lib/executor', () => { - - describe('Executor', () => { - - describe('locate', () => { - - it('should locate bar command and retain command args', async () => { - const namespace = new MyNamespace(); - const executor = new Executor({ namespace }); - const location = await executor.locate(['foo', 'bar', 'a', '--flag', 'b', '--', 'c']); - expect(location.obj).toBeInstanceOf(BarCommand); - expect(location.args).toEqual(['a', '--flag', 'b', '--', 'c']); - const runPath = location.path.map(([n]) => n); - expect(runPath).toEqual(['my', 'foo', 'bar']); - }); - - }); - - describe('execute', () => { - - const TEST_CASE_1 = { - input: ['foo', 'bar', 'a', '--flag', 'b', '--', 'c'], - validate: (mock: any) => { - expect(mock.calls.length).toEqual(2); - const [ [ validateId, validateArgs ], [ runId, runArgs ] ] = mock.calls; - expect(validateId).toEqual('bar:validate'); - expect(validateArgs).toEqual([['a', 'b']]); - expect(runId).toEqual('bar:run'); - const args: any = runArgs; - expect(args[0]).toEqual(['a', 'b']); - expect(args[1]).toEqual({ '--': ['c'], '_': ['a', 'b'], flag: true }); - expect(args[2].location.obj).toBeInstanceOf(BarCommand); - const runPath = args[2].location.path.map(([n]: any) => n); - const runPathObjs = args[2].location.path.map(([, o]: any) => o); - expect(runPath).toEqual(['my', 'foo', 'bar']); - expect(runPathObjs[0]).toBeInstanceOf(MyNamespace); - expect(runPathObjs[1]).toBeInstanceOf(FooNamespace); - expect(runPathObjs[2]).toBeInstanceOf(BarCommand); - }, - }; - - it('should call run function of found bar command using argv', async () => { - const { input, validate } = TEST_CASE_1; - const namespace = new MyNamespace(); - const executor = new Executor({ namespace }); - await executor.execute(input, {}); - validate(spy.mock); - }); - - it('should call run function of found bar command using location', async () => { - const { input, validate } = TEST_CASE_1; - const namespace = new MyNamespace(); - const executor = new Executor({ namespace }); - const location = await executor.locate(input); - await executor.execute(location, {}); - validate(spy.mock); - }); - - }); - - describe('run', () => { - - it('should call run function of bar command', async () => { - const namespace = new MyNamespace(); - const executor = new Executor({ namespace }); - const location = await namespace.locate(['foo', 'bar']); - await executor.run(location.obj as any, []); - expect(spy.mock.calls.length).toEqual(2); - const [ [ validateId, validateArgs ], [ runId, runArgs ] ] = spy.mock.calls; - expect(validateId).toEqual('bar:validate'); - expect(validateArgs).toEqual([[]]); - expect(runId).toEqual('bar:run'); - expect(runArgs).toEqual([[], { '--': [], '_': [], flag: null }, undefined]); - }); - - }); - - }); - - }); - -}); diff --git a/packages/@ionic/cli-framework/src/lib/__tests__/help.ts b/packages/@ionic/cli-framework/src/lib/__tests__/help.ts deleted file mode 100644 index 25e67226ef..0000000000 --- a/packages/@ionic/cli-framework/src/lib/__tests__/help.ts +++ /dev/null @@ -1,422 +0,0 @@ -import { stripAnsi } from '@ionic/utils-terminal'; - -import { Command, CommandMap, CommandMapDefault, Namespace, NamespaceMap } from '../command'; -import { CommandStringHelpFormatter, NamespaceStringHelpFormatter, NamespaceSchemaHelpFormatter } from '../help'; -import { MetadataGroup } from '../../definitions'; - -class MyNamespace extends Namespace { - async getMetadata() { - return { - name: 'my', - summary: 'the my namespace', - }; - } - - async getNamespaces(): Promise { - return new NamespaceMap([ - ['foo', async () => new FooNamespace(this)], - ['defns', async () => new NamespaceWithDefault(this)], - ['f', 'foo'], - ]); - } - - async getCommands(): Promise { - return new CommandMap([ - ['bar', async () => new BarCommand(this)], - ]); - } -} - -class NamespaceWithDefault extends Namespace { - async getMetadata() { - return { - name: 'defns', - summary: 'the defns namespace', - groups: [MetadataGroup.BETA], - }; - } - - async getCommands(): Promise { - return new CommandMap([ - [CommandMapDefault, async () => new DefaultCommand(this)], - ]); - } -} - -class FooNamespace extends Namespace { - async getMetadata() { - return { - name: 'foo', - summary: 'the foo namespace', - description: 'my description with this footnote[^here] and more text', - footnotes: [ - { - id: 'here', - text: 'A footnote is a thing', - }, - ], - }; - } - - async getCommands(): Promise { - return new CommandMap([ - ['bar', async () => new BarCommand(this)], - ['baz', async () => new BazCommand(this)], - ['b', 'bar'], - ['b1', 'baz'], - ['b2', 'baz'], - ]); - } -} - -class DefaultCommand extends Command { - async getMetadata() { - return { - name: 'def', - summary: 'the default command', - groups: [MetadataGroup.BETA], - }; - } - - async run() {} - async validate() {} -} - -class BarCommand extends Command { - async getMetadata() { - return { - name: 'bar', - summary: 'the bar command', - description: 'my description with a numeric link footnote[^1] and some more text', - footnotes: [ - { - id: 1, - url: 'https://ionicframework.com', - }, - ], - inputs: [ - { name: 'input1', summary: 'input1 summary' }, - { name: 'input2', summary: 'input2 summary' }, - ], - options: [ - { name: 'opt1', summary: 'opt1 summary', aliases: ['o'], spec: { value: 'optvalue' } }, - { name: 'opt2', summary: 'opt2 summary', groups: [MetadataGroup.ADVANCED] }, - { name: 'opt3', summary: 'opt3 summary', type: Boolean }, - ], - exampleCommands: ['', 'input1 input2', '--opt1 --opt2'], - }; - } - - async run() {} - async validate() {} -} - -class BazCommand extends Command { - async getMetadata() { - return { - name: 'baz', - summary: 'the baz command', - }; - } - - async run() {} - async validate() {} -} - -describe('@ionic/cli-framework', () => { - - describe('lib/help', () => { - - describe('CommandStringHelpFormatter', () => { - - it('should format a command appropriately', async () => { - const myns = new MyNamespace(); - const location = await myns.locate(['foo', 'b']); - const formatter = new CommandStringHelpFormatter({ location, command: location.obj as any }); - const result = await formatter.format(); - - expect(stripAnsi(result)).toEqual(` - my foo bar - the bar command - - my description with a numeric link footnote[1] and some more text - - [1]: https://ionicframework.com - - Usage: - - $ my foo bar [] [] [options] - - Inputs: - - input1 .......................... (optional) input1 summary - input2 .......................... (optional) input2 summary - - Options: - - --opt1=, -o=? ......... opt1 summary - --opt2= ................... opt2 summary - --opt3 .......................... opt3 summary - - Examples: - - $ my foo bar - $ my foo bar input1 input2 - $ my foo bar --opt1 --opt2 - -`); - }); - - }); - - describe('NamespaceStringHelpFormatter', () => { - - it('should format a namespace appropriately', async () => { - const myns = new MyNamespace(); - const location = await myns.locate([]); - const formatter = new NamespaceStringHelpFormatter({ location, namespace: location.obj as any }); - const result = await formatter.format(); - - expect(stripAnsi(result)).toEqual(` - my - the my namespace - - Usage: - - $ my [] [--help] [options] - - Commands: - - bar ............................. the bar command - defns .............. (beta) the defns namespace (subcommands: def) - foo ................ the foo namespace (subcommands: bar, baz) (alias: f) - -`); - }); - - it('should format a subnamespace appropriately', async () => { - const myns = new MyNamespace(); - const location = await myns.locate(['f']); - const formatter = new NamespaceStringHelpFormatter({ location, namespace: location.obj as any }); - const result = await formatter.format(); - - expect(stripAnsi(result)).toEqual(` - my foo - the foo namespace - - my description with this footnote[1] and more text - - [1]: A footnote is a thing - - Usage: - - $ my foo [] [--help] [options] - - Commands: - - bar ............................. the bar command (alias: b) - baz ............................. the baz command (aliases: b1, b2) - -`); - }); - - }); - - describe('NamespaceSchemaHelpFormatter', () => { - - it('should produce expected output for root namespace', async () => { - const expected = { - "name": "my", - "summary": "the my namespace", - "description": "", - "groups": [], - "aliases": [], - "commands": [ - { - "name": "my bar", - "namespace": [ - "my" - ], - "summary": "the bar command", - "description": "my description with a numeric link footnote[^1] and some more text", - "groups": [], - "exampleCommands": [ - "my bar ", - "my bar input1 input2", - "my bar --opt1 --opt2" - ], - "footnotes": [ - { - "type": "link", - "id": 1, - "url": "https://ionicframework.com" - } - ], - "aliases": [], - "inputs": [ - { - "name": "input1", - "summary": "input1 summary", - "required": false - }, - { - "name": "input2", - "summary": "input2 summary", - "required": false - } - ], - "options": [ - { - "name": "opt1", - "type": "string", - "summary": "opt1 summary", - "groups": [], - "aliases": [ - "o" - ], - "spec": { - "value": "optvalue" - }, - }, - { - "name": "opt2", - "type": "string", - "summary": "opt2 summary", - "aliases": [], - "groups": [ - "advanced" - ], - "spec": { - "value": "opt2" - }, - }, - { - "name": "opt3", - "type": "boolean", - "summary": "opt3 summary", - "aliases": [], - "groups": [], - "spec": { - "value": "true/false" - }, - }, - ] - }, - { - "name": "my foo bar", - "namespace": [ - "my", - "foo" - ], - "summary": "the bar command", - "description": "my description with a numeric link footnote[^1] and some more text", - "groups": [], - "exampleCommands": [ - "my foo bar ", - "my foo bar input1 input2", - "my foo bar --opt1 --opt2" - ], - "footnotes": [ - { - "type": "link", - "id": 1, - "url": "https://ionicframework.com" - } - ], - "aliases": [ - "my foo b" - ], - "inputs": [ - { - "name": "input1", - "summary": "input1 summary", - "required": false - }, - { - "name": "input2", - "summary": "input2 summary", - "required": false - } - ], - "options": [ - { - "name": "opt1", - "type": "string", - "summary": "opt1 summary", - "groups": [], - "aliases": [ - "o" - ], - "spec": { - "value": "optvalue" - }, - }, - { - "name": "opt2", - "type": "string", - "summary": "opt2 summary", - "groups": [ - "advanced" - ], - "aliases": [], - "spec": { - "value": "opt2" - }, - }, - { - "name": "opt3", - "type": "boolean", - "summary": "opt3 summary", - "aliases": [], - "groups": [], - "spec": { - "value": "true/false" - }, - }, - ] - }, - { - "name": "my foo baz", - "namespace": [ - "my", - "foo" - ], - "summary": "the baz command", - "description": "", - "groups": [], - "exampleCommands": [], - "footnotes": [], - "aliases": [ - "my foo b1", - "my foo b2" - ], - "inputs": [], - "options": [] - }, - { - "name": "my defns", - "namespace": [ - "my" - ], - "summary": "the default command", - "description": "", - "groups": [ - "beta" - ], - "exampleCommands": [], - "footnotes": [], - "aliases": [], - "inputs": [], - "options": [] - } - ] - }; - const myns = new MyNamespace(); - const location = await myns.locate([]); - const formatter = new NamespaceSchemaHelpFormatter({ location, namespace: location.obj as any }); - const result = await formatter.serialize(); - expect(result).toEqual(expected); - }); - - }); - - }); - -}); diff --git a/packages/@ionic/cli-framework/src/lib/__tests__/options.ts b/packages/@ionic/cli-framework/src/lib/__tests__/options.ts deleted file mode 100644 index 7cfdfd797b..0000000000 --- a/packages/@ionic/cli-framework/src/lib/__tests__/options.ts +++ /dev/null @@ -1,344 +0,0 @@ -import minimist from 'minimist'; - -import { CommandMetadata } from '../../definitions'; -import { OptionFilters, filterCommandLineOptions, filterCommandLineOptionsByGroup, metadataOptionsToParseArgsOptions, separateArgv, stripOptions, unparseArgs } from '../options'; - -describe('@ionic/cli-framework', () => { - - describe('lib/options', () => { - - describe('stripOptions', () => { - - it('should not affect only args', () => { - const result = stripOptions(['a', 'b'], {}); - expect(result).toEqual(['a', 'b']); - }); - - it('should strip single hyphen options', () => { - const result = stripOptions(['a', 'b', '-c'], {}); - expect(result).toEqual(['a', 'b']); - }); - - it('should strip double hyphen options', () => { - const result = stripOptions(['a', 'b', '--opt1'], {}); - expect(result).toEqual(['a', 'b']); - }); - - it('should strip double hyphen options with equal sign', () => { - const result = stripOptions(['a', 'b', '--opt1=test'], {}); - expect(result).toEqual(['a', 'b']); - }); - - it('should strip options from anywhere', () => { - const result = stripOptions(['-f', 'a', '--opt1', 'b', '--opt2'], {}); - expect(result).toEqual(['a', 'b']); - }); - - it('should preserve options after -- separator', () => { - const result = stripOptions(['-f', 'a', '--', '--opt1', 'b', '--opt2'], {}); - expect(result).toEqual(['a', '--', '--opt1', 'b', '--opt2']); - }); - - it('should remove options after -- separator if option is supplied', () => { - const result = stripOptions(['-f', 'a', '--', '--opt1', 'b', '--opt2'], { includeSeparated: false }); - expect(result).toEqual(['a']); - }); - - }); - - describe('separateArgv', () => { - - it('should work for empty array', () => { - const result = separateArgv([]); - expect(result).toEqual([[], []]); - }); - - it('should work for arg list with no separator', () => { - const result = separateArgv(['a', 'b']); - expect(result).toEqual([['a', 'b'], []]); - }); - - it('should work for arg list with no separator with options', () => { - const result = separateArgv(['a', '--foo', 'b']); - expect(result).toEqual([['a', '--foo', 'b'], []]); - }); - - it('should work for arg list with separator', () => { - const result = separateArgv(['a', 'b', '--', 'c']); - expect(result).toEqual([['a', 'b'], ['c']]); - }); - - }); - - const metadata1: Required = { - name: 'bar', - summary: '', - description: '', - footnotes: [], - exampleCommands: [], - groups: [], - inputs: [ - { - name: 'input1', - summary: '', - }, - { - name: 'input2', - summary: '', - }, - ], - options: [ - { - name: 'foo', - summary: '', - aliases: ['f'], - groups: ['a'], - }, - { - name: 'bar', - summary: '', - default: 'soup', - groups: ['b'], - }, - { - name: 'flag1', - summary: '', - type: Boolean, - }, - ], - }; - - describe('metadataOptionsToParseArgsOptions', () => { - - it('should transform metadata to minimist options', () => { - const result = metadataOptionsToParseArgsOptions(metadata1.options); - expect(result).toEqual({ - string: ['_', 'foo', 'bar'], - boolean: ['flag1'], - alias: { foo: ['f'], bar: [], flag1: [] }, - default: { foo: null, bar: 'soup', flag1: null }, - '--': true, - }); - }); - - describe('minimist arg parse', () => { - - it('should parse with empty argv', () => { - const opts = metadataOptionsToParseArgsOptions(metadata1.options); - const result = minimist([], opts); - expect(result).toEqual({ _: [], foo: null, f: null, bar: 'soup', flag1: null, '--': [] }); - }); - - it('should parse with comprehensive argv', () => { - const opts = metadataOptionsToParseArgsOptions(metadata1.options); - const result = minimist(['cat', '--foo', 'rabbit', 'dog', '--bar=salad', '--unknown', 'wow', '--flag1', 'extra', '--and-again'], opts); - expect(result).toEqual({ _: ['cat', 'dog', 'extra'], foo: 'rabbit', f: 'rabbit', bar: 'salad', flag1: true, unknown: 'wow', 'and-again': true, '--': [] }); - }); - - }); - - }); - - describe('unparseArgs', () => { - - it('should handle empty argv', () => { - const result = unparseArgs({ _: [] }, {}); - expect(result).toEqual([]); - }); - - it('should filter out arguments', () => { - const result = unparseArgs({ _: ['foo', 'bar'] }, {}); - expect(result).toEqual(['foo', 'bar']); - }); - - it('should parse out boolean option from minimist result', () => { - const result = unparseArgs({ _: ['foo', 'bar'], wow: true }, {}); - expect(result).toEqual(['foo', 'bar', '--wow']); - }); - - it('should parse out single-letter boolean option from minimist result', () => { - const result = unparseArgs({ _: ['foo', 'bar'], w: true }, {}); - expect(result).toEqual(['foo', 'bar', '-w']); - }); - - it('should parse out string option from minimist result', () => { - const result = unparseArgs({ _: [], cat: 'meow' }, {}); - expect(result).toEqual(['--cat=meow']); - }); - - it('should parse out single-letter string option from minimist result', () => { - const result = unparseArgs({ _: [], c: 'meow' }, {}); - expect(result).toEqual(['-c=meow']); - }); - - it('should parse out option list from minimist result', () => { - const result = unparseArgs({ _: [], cat: 'meow', dog: 'bark', flag1: true }, {}); - expect(result).toEqual(['--cat=meow', '--dog=bark', '--flag1']); - }); - - it('should parse out option list from minimist result without equal signs', () => { - const result = unparseArgs({ _: [], cat: 'meow', dog: 'bark', flag1: true }, { useEquals: false }); - expect(result).toEqual(['--cat', 'meow', '--dog', 'bark', '--flag1']); - }); - - it('should parse out string option from minimist result and not wrap strings with spaces in double quotes without flag', () => { - const result = unparseArgs({ _: [], cat: 'meow meow meow' }, {}); - expect(result).toEqual(['--cat=meow meow meow']); - }); - - it('should parse out string option from minimist result and wrap strings with spaces in double quotes with flag provided', () => { - const result = unparseArgs({ _: [], cat: 'meow meow meow' }, { useDoubleQuotes: true }); - expect(result).toEqual(['--cat="meow meow meow"']); - }); - - it('should account for -- separator', () => { - const result = unparseArgs({ _: [], '--': ['--claws'], cat: 'meow meow meow' }, { useDoubleQuotes: true }); - expect(result).toEqual(['--cat="meow meow meow"', '--', '--claws']); - }); - - it('should account for empty -- separator', () => { - const result = unparseArgs({ _: [], '--': [], cat: 'meow meow meow' }, { useDoubleQuotes: true }); - expect(result).toEqual(['--cat="meow meow meow"']); - }); - - it('should account for double -- separator', () => { - const result = unparseArgs({ _: [], '--': ['--', '--claws'], cat: 'meow meow meow' }, { useDoubleQuotes: true }); - expect(result).toEqual(['--cat="meow meow meow"', '--', '--', '--claws']); - }); - - it('should account for args, options, and -- args', () => { - const result = unparseArgs({ _: ['foo', 'bar'], '--': ['--', '--claws'], cat: 'meow meow meow' }, { useDoubleQuotes: true }); - expect(result).toEqual(['foo', 'bar', '--cat="meow meow meow"', '--', '--', '--claws']); - }); - - it('should ignore false values with ignoreFalse', () => { - const result = unparseArgs({ _: [], flag1: false }, { ignoreFalse: true }); - expect(result).toEqual([]); - }); - - it('should not ignore false values with ignoreFalse set to false', () => { - const result = unparseArgs({ _: [], flag1: false }, { ignoreFalse: false }); - expect(result).toEqual(['--no-flag1']); - }); - - it('should account for aliases', () => { - const result = unparseArgs({ _: [], foo: 'foo', fo: 'foo', f: 'foo', bar: 'bar', b: 'bar', baz: 'baz' }, {}, { alias: { foo: ['f', 'fo'], bar: 'b' } }); - expect(result).toEqual(['--foo=foo', '--bar=bar', '--baz=baz']); - }); - - it('should filter out unknown options if requested', () => { - const result = unparseArgs({ _: [], foo: 'foo', flag1: true }, {}, { unknown: () => false }); - expect(result).toEqual([]); - }); - - it('should keep known options', () => { - const result = unparseArgs({ _: [], foo: 'foo', bar: 'bar', flag1: true, flag2: true }, {}, { string: ['foo'], boolean: ['flag1'], unknown: () => false }); - expect(result).toEqual(['--foo=foo', '--flag1']); - }); - - it('should keep known options by alias', () => { - const result = unparseArgs({ _: [], foo: 'foo', f: 'foo', bar: 'bar', flag1: true, flag2: true }, {}, { string: ['foo'], boolean: ['flag1'], alias: { foo: ['f'] }, unknown: () => false }); - expect(result).toEqual(['--foo=foo', '--flag1']); - }); - - }); - - describe('filterCommandLineOptions', () => { - - it('should return empty object for empty input', () => { - const result = filterCommandLineOptions(metadata1.options, { _: [] }); - expect(result).toEqual({ _: [] }); - }); - - it('should return args if supplied', () => { - const result = filterCommandLineOptions(metadata1.options, { _: ['a', 'b'] }, opt => true); - expect(result).toEqual({ _: ['a', 'b'] }); - }); - - it('should include all known & supplied options using a true predicate', () => { - const result = filterCommandLineOptions(metadata1.options, { _: [], foo: 'hi', flag1: true }, opt => true); - expect(result).toEqual({ _: [], foo: 'hi', flag1: true }); - }); - - it('should include all known & supplied options without a predicate', () => { - const result = filterCommandLineOptions(metadata1.options, { _: [], foo: 'hi', flag1: true }); - expect(result).toEqual({ _: [], foo: 'hi', flag1: true }); - }); - - it('should include options with aliases', () => { - const result = filterCommandLineOptions(metadata1.options, { _: [], f: 'hi', flag1: true }); - expect(result).toEqual({ _: [], foo: 'hi', flag1: true }); - }); - - it('should return empty object for a false predicate', () => { - const result = filterCommandLineOptions(metadata1.options, { _: [] }, opt => false); - expect(result).toEqual({ _: [] }); - }); - - it('should exclude unknown options even with a true predicate', () => { - const result = filterCommandLineOptions(metadata1.options, { _: [], unknown: 'yep', bar: 'hi', flag1: true }, opt => true); - expect(result).toEqual({ _: [], bar: 'hi', flag1: true }); - }); - - it('should exclude options that do not match the predicate using opt', () => { - const result = filterCommandLineOptions(metadata1.options, { _: [], unknown: 'yep', foo: 'wow', bar: 'hi', flag1: true }, opt => opt.groups ? opt.groups.includes('a') : false); - expect(result).toEqual({ _: [], foo: 'wow' }); - }); - - it('should exclude options that do not match the predicate using value', () => { - const result = filterCommandLineOptions(metadata1.options, { _: [], unknown: 'yep', foo: 'wow', bar: 'hi', flag1: true }, (opt, value) => value === 'hi'); - expect(result).toEqual({ _: [], bar: 'hi' }); - }); - - it('should include separated args from original parsed args', () => { - const result = filterCommandLineOptions(metadata1.options, { _: [], '--': ['some', 'more', '--args'] }); - expect(result).toEqual({ _: [], '--': ['some', 'more', '--args'] }); - }); - - describe('OptionFilters.includesGroups', () => { - - it('should include single group', () => { - const result = filterCommandLineOptions(metadata1.options, { _: [], foo: 'wow', bar: 'nope', flag1: true }, OptionFilters.includesGroups('a')); - expect(result).toEqual({ _: [], foo: 'wow' }); - }); - - it('should include multiple groups', () => { - const result = filterCommandLineOptions(metadata1.options, { _: [], foo: 'wow', bar: 'nope', flag1: true }, OptionFilters.includesGroups(['a', 'b'])); - expect(result).toEqual({ _: [], foo: 'wow', bar: 'nope' }); - }); - - }); - - describe('OptionFilters.excludesGroups', () => { - - it('should exclude single groups', () => { - const result = filterCommandLineOptions(metadata1.options, { _: [], foo: 'wow', bar: 'nope', flag1: true }, OptionFilters.excludesGroups('a')); - expect(result).toEqual({ _: [], bar: 'nope', flag1: true }); - }); - - it('should exclude multiple groups', () => { - const result = filterCommandLineOptions(metadata1.options, { _: [], foo: 'wow', bar: 'nope', flag1: true }, OptionFilters.excludesGroups(['a', 'b'])); - expect(result).toEqual({ _: [], flag1: true }); - }); - - }); - - }); - - describe('filterCommandLineOptionsByGroup', () => { - - it('should exclude options not in group a', () => { - const result = filterCommandLineOptionsByGroup(metadata1.options, { _: [], unknown: 'yep', foo: 'wow', bar: 'hi', flag1: true }, 'a'); - expect(result).toEqual({ _: [], foo: 'wow' }); - }); - - it('should exclude options not in group a or b', () => { - const result = filterCommandLineOptionsByGroup(metadata1.options, { _: [], unknown: 'yep', foo: 'wow', bar: 'hi', flag1: true }, ['a', 'b']); - expect(result).toEqual({ _: [], foo: 'wow', bar: 'hi' }); - }); - - }); - - }); - -}); diff --git a/packages/@ionic/cli-framework/src/lib/__tests__/validators.ts b/packages/@ionic/cli-framework/src/lib/__tests__/validators.ts deleted file mode 100644 index 0ec5303430..0000000000 --- a/packages/@ionic/cli-framework/src/lib/__tests__/validators.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { stripAnsi } from '@ionic/utils-terminal'; - -import { contains, validators } from '../validators'; - -describe('@ionic/cli-framework', () => { - - describe('lib/validators', () => { - - describe('validators', () => { - - describe('required', () => { - - it('should return a generic message if the input is not given', () => { - const result = validators.required(); - expect(result).toEqual('Must not be empty.'); - }); - - it('should return a generic message if the input is an empty string', () => { - const result = validators.required(''); - expect(result).toEqual('Must not be empty.'); - }); - - it('should return a name-specific message if the input is not given and a name is provided', () => { - const result = validators.required(undefined, 'my_key'); - expect(stripAnsi(result as any)).toEqual('my_key must not be empty.'); - }); - - it('should return a name-specific message if the input is an empty string and a name is provided', () => { - const result = validators.required('', 'my_key'); - expect(stripAnsi(result as any)).toEqual('my_key must not be empty.'); - }); - - it('should validate falsy input', () => { - const falsy = ['0', 'false']; - - for (const v of falsy) { - const result = validators.required(v); - expect(result).toBe(true); - } - }); - - it('should validate input', () => { - const result = validators.required('some input'); - expect(result).toBe(true); - }); - - }); - - describe('email', () => { - - it('should return a generic message if the email is an empty string', () => { - const result = validators.email(''); - expect(result).toEqual('Invalid email address.'); - }); - - it('should return a name-specific message if the email is empty and a name is provided', () => { - const result = validators.email('', 'my_key'); - expect(stripAnsi(result as any)).toEqual('my_key is an invalid email address.'); - }); - - it('should return a string if email address is invalid', () => { - const r3 = validators.email('asdf'); - const r4 = validators.email('foo@foo'); - expect(stripAnsi(r3 as any)).toEqual('Invalid email address.'); - expect(stripAnsi(r4 as any)).toEqual('Invalid email address.'); - }); - - it('should validate an email', () => { - const result = validators.email('a@b.c'); - expect(result).toBe(true); - }); - - }); - - // TODO: url - // TODO: numeric - - describe('slug', () => { - - it('should return a generic message if the slug is invalid', () => { - const result = validators.slug('A'); - expect(result).toEqual('Invalid slug (machine name).'); - }); - - it('should return a name-specific message if the slug is invalid and a name is provided', () => { - const result = validators.slug('A', 'my_key'); - expect(stripAnsi(result as any)).toEqual('my_key is an invalid slug (machine name).'); - }); - - it('should validate a slug', () => { - const result = validators.slug('a-b-c'); - expect(result).toBe(true); - }); - - }); - - }); - - describe('contains', () => { - - it('should generate a validator that handles input not given', () => { - const validator = contains(['foo', 'bar', 'baz'], {}); - const result = validator(); - expect(stripAnsi(result as any)).toEqual('Must be one of: foo, bar, baz (not unset)'); - }); - - it('should generate a validator that handles input not given with the given key', () => { - const validator = contains(['foo', 'bar', 'baz'], {}); - const result = validator(undefined, 'my_key'); - expect(stripAnsi(result as any)).toEqual('my_key must be one of: foo, bar, baz (not unset)'); - }); - - it('should generate a validator that handles input not in collection', () => { - const validator = contains(['foo', 'bar', 'baz'], {}); - const result = validator('my input'); - expect(stripAnsi(result as any)).toEqual('Must be one of: foo, bar, baz (not my input)'); - }); - - it('should generate a validator that handles input not in collection with the given key', () => { - const validator = contains(['foo', 'bar', 'baz'], {}); - const result = validator('my input', 'my_key'); - expect(stripAnsi(result as any)).toEqual('my_key must be one of: foo, bar, baz (not my input)'); - }); - - it('should generate a validator that handles empty input', () => { - const validator = contains(['foo', 'bar', 'baz'], {}); - const result = validator(''); - expect(stripAnsi(result as any)).toEqual('Must be one of: foo, bar, baz (not empty)'); - }); - - it('should generate a validator that handles empty input with the given key', () => { - const validator = contains(['foo', 'bar', 'baz'], {}); - const result = validator('', 'my_key'); - expect(stripAnsi(result as any)).toEqual('my_key must be one of: foo, bar, baz (not empty)'); - }); - - it('should generate a validator that handles empty input in collection that allows unset input', () => { - const validator = contains(['foo', 'bar', 'baz', undefined], {}); - const result = validator(''); - expect(stripAnsi(result as any)).toEqual('Must be unset or one of: foo, bar, baz (not empty)'); - }); - - it('should generate a validator that handles empty input in collection that allows unset input with the given key', () => { - const validator = contains(['foo', 'bar', 'baz', undefined], {}); - const result = validator('', 'my_key'); - expect(stripAnsi(result as any)).toEqual('my_key must be unset or one of: foo, bar, baz (not empty)'); - }); - - it('should generate a validator that validates input in the collection', () => { - const validator = contains(['foo', 'bar', 'baz'], {}); - const result = validator('foo'); - expect(stripAnsi(result as any)).toBe(true); - }); - - it('should generate a validator that validates input that is not given', () => { - const validator = contains(['foo', 'bar', 'baz', undefined], {}); - const result = validator(); - expect(stripAnsi(result as any)).toBe(true); - }); - - }); - - // TODO: combine - - }); - -}); diff --git a/packages/@ionic/cli-framework/src/lib/colors.ts b/packages/@ionic/cli-framework/src/lib/colors.ts deleted file mode 100644 index cee61e2ad9..0000000000 --- a/packages/@ionic/cli-framework/src/lib/colors.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { ColorFunction, Colors as BaseColors } from '@ionic/cli-framework-output'; -import chalk from 'chalk'; -import * as lodash from 'lodash'; - -import { MetadataGroup } from '../definitions'; - -export { ColorFunction, LoggerColors } from '@ionic/cli-framework-output'; -export type HelpGroupColors = { [G in Exclude]: ColorFunction; }; - -export interface HelpColors { - /** - * Used to color the section titles in help output. - */ - title: ColorFunction; - - group: HelpGroupColors; -} - -export interface Colors extends BaseColors { - help: HelpColors; -} - -export const DEFAULT_COLORS: Colors = Object.freeze({ - strong: chalk.bold, - weak: chalk.dim, - input: chalk.cyan, - success: chalk.green, - failure: chalk.red, - ancillary: chalk.cyan, - log: Object.freeze({ - DEBUG: chalk.magenta, - INFO: chalk.white, - WARN: chalk.yellow, - ERROR: chalk.red, - }), - help: Object.freeze({ - title: chalk.bold, - group: Object.freeze({ - [MetadataGroup.DEPRECATED]: chalk.yellow, - [MetadataGroup.BETA]: chalk.magenta, - [MetadataGroup.EXPERIMENTAL]: chalk.red, - [MetadataGroup.PAID]: chalk.green, - }), - }), -}); - -export const NO_COLORS: Colors = Object.freeze({ - strong: lodash.identity, - weak: lodash.identity, - input: lodash.identity, - success: lodash.identity, - failure: lodash.identity, - ancillary: lodash.identity, - log: Object.freeze({ - DEBUG: lodash.identity, - INFO: lodash.identity, - WARN: lodash.identity, - ERROR: lodash.identity, - }), - help: Object.freeze({ - title: lodash.identity, - group: Object.freeze({ - [MetadataGroup.DEPRECATED]: lodash.identity, - [MetadataGroup.BETA]: lodash.identity, - [MetadataGroup.EXPERIMENTAL]: lodash.identity, - [MetadataGroup.PAID]: lodash.identity, - }), - }), -}); diff --git a/packages/@ionic/cli-framework/src/lib/command.ts b/packages/@ionic/cli-framework/src/lib/command.ts deleted file mode 100644 index 4f8dc2e113..0000000000 --- a/packages/@ionic/cli-framework/src/lib/command.ts +++ /dev/null @@ -1,271 +0,0 @@ -import { AliasedMap } from '@ionic/utils-object'; -import * as lodash from 'lodash'; - -import { CommandInstanceInfo, CommandLineInputs, CommandLineOptions, CommandMapGetter, CommandMetadata, CommandMetadataInput, CommandMetadataOption, CommandPathItem, HydratedCommandMetadata, HydratedNamespaceMetadata, ICommand, ICommandMap, INamespace, INamespaceMap, NamespaceLocateOptions, NamespaceLocateResult, NamespaceMapGetter, NamespaceMetadata, ValidationError } from '../definitions'; -import { InputValidationError } from '../errors'; -import { strcmp } from '../utils/string'; - -import { validate } from './validators'; - -export abstract class BaseCommand, N extends INamespace, M extends CommandMetadata, I extends CommandMetadataInput, O extends CommandMetadataOption> { - constructor(public namespace: N) {} - - abstract getMetadata(): Promise; - - abstract run(inputs: CommandLineInputs, options: CommandLineOptions, runtime?: Partial>): Promise; - - async validate(argv: CommandLineInputs): Promise { - const metadata = await this.getMetadata(); - - if (!metadata.inputs) { - return; - } - - const errors: ValidationError[][] = []; - - for (const i in metadata.inputs) { - const input = metadata.inputs[i]; - - if (input.validators && input.validators.length > 0) { - try { - validate(argv[i], input.name, [...input.validators]); - } catch (e: any) { - if (!(e instanceof InputValidationError)) { - throw e; - } - - errors.push(e.errors); - } - } - } - - if (errors.length > 0) { - throw new InputValidationError('Invalid inputs.', lodash.flatten(errors)); - } - } -} - -export const CommandMapDefault = Symbol('default'); - -export class BaseCommandMap, N extends INamespace, M extends CommandMetadata, I extends CommandMetadataInput, O extends CommandMetadataOption> extends AliasedMap> implements ICommandMap {} -export class BaseNamespaceMap, N extends INamespace, M extends CommandMetadata, I extends CommandMetadataInput, O extends CommandMetadataOption> extends AliasedMap> implements INamespaceMap {} - -export abstract class BaseNamespace, N extends INamespace, M extends CommandMetadata, I extends CommandMetadataInput, O extends CommandMetadataOption> implements INamespace { - parent: N | undefined; - - constructor(parent?: N) { - this.parent = parent; - } - - get root(): N { - let n: any = this; - - while (n.parent) { - n = n.parent; - } - - return n; - } - - abstract getMetadata(): Promise; - - // TODO: https://github.com/Microsoft/TypeScript/issues/9659 - async getNamespaces(): Promise> { - return new BaseNamespaceMap(); - } - - // TODO: https://github.com/Microsoft/TypeScript/issues/9659 - async getCommands(): Promise> { - return new BaseCommandMap(); - } - - /** - * Locate commands and namespaces given a set of inputs. - * - * Recursively walk down the tree of namespaces available within this - * namespace to find the command that we will execute or the right-most - * namespace matched if the command is not found. - * - * The resolved object looks like this: - * - * { - * obj: command or namespace, - * args: the leftover arguments, - * path: the path taken to get to the result which comprises tuples of - * } - * - * @param argv The set of command-line arguments to use to locate. - */ - async locate(argv: readonly string[], { useAliases = true }: NamespaceLocateOptions = {}): Promise> { - const _locate = async (inputs: readonly string[], parent: N, path: CommandPathItem[]): Promise> => { - const [ key ] = inputs; - const children = await parent.getNamespaces(); - const nsgetter = useAliases ? children.resolveAlias(key) : children.get(key); - - if (!nsgetter || typeof nsgetter === 'string' || typeof nsgetter === 'symbol') { - const commands = await parent.getCommands(); - const cmdgetter = useAliases ? commands.resolveAlias(key) : commands.get(key); - - if (cmdgetter && typeof cmdgetter !== 'string' && typeof cmdgetter !== 'symbol') { - const cmd = await cmdgetter(); - return { args: inputs.slice(1), obj: cmd, path: [...path, [key, cmd]] }; - } - - const defaultcmdgetter = commands.get(CommandMapDefault); - - if (defaultcmdgetter && typeof defaultcmdgetter !== 'string' && typeof defaultcmdgetter !== 'symbol') { - const cmd = await defaultcmdgetter(); - - if (path.length > 0) { - // The default command was found via the namespace, so we replace the - // previous path entry (the namespace which contains this default - // command) with the command itself. - path[path.length - 1][1] = cmd; - } - - return { args: inputs, obj: cmd, path }; - } - - return { args: inputs, obj: parent, path }; - } - - const child = await nsgetter(); - return _locate(inputs.slice(1), child, [...path, [key, child]]); - }; - - const metadata = await this.getMetadata(); - - // TODO: typescript complains about `this`. Calling this method on - // BaseNamespace would be unsafe if the class weren't abstract. Typescript - // bug? I may be wrong. - return _locate(argv, this as any, [[metadata.name, this as any]]); - } - - /** - * Get all command metadata in a flat structure. - */ - async getCommandMetadataList(): Promise[]> { - const _getCommandMetadataList = async (parent: N, path: CommandPathItem[]): Promise[]> => { - const commandsInNamespace = await parent.getCommands(); - const commandAliasesInNamespace = commandsInNamespace.getAliases(); - const commandList: HydratedCommandMetadata[] = []; - - // Gather all commands for a namespace and turn them into simple key value - // objects. Also keep a record of the namespace path. - await Promise.all([...commandsInNamespace.entries()].map(async ([k, cmdgetter]) => { - if (typeof cmdgetter === 'string' || typeof cmdgetter === 'symbol') { - return; - } - - const command = await cmdgetter(); - const commandAliases = (commandAliasesInNamespace.get(k) || []).filter((a): a is string => typeof a === 'string').map(a => [...path.map(([p]) => p), a].join(' ')); - const commandMetadata = await command.getMetadata(); - const commandPath = [...path]; - - if (typeof k === 'string') { - commandPath.push([k, command]); - } - - // TODO: can't use spread: https://github.com/Microsoft/TypeScript/pull/13288 - const result = lodash.assign({}, commandMetadata, { command, namespace: parent, aliases: commandAliases, path: commandPath }); - commandList.push(result); - })); - - commandList.sort((a, b) => strcmp(a.name, b.name)); - - const children = await parent.getNamespaces(); - const namespacedCommandList: HydratedCommandMetadata[] = []; - - // If this namespace has children then get their commands - if (children.size > 0) { - await Promise.all([...children.entries()].map(async ([k, nsgetter]) => { - if (typeof nsgetter === 'string' || typeof nsgetter === 'symbol') { - return; - } - - const ns = await nsgetter(); - const commandPath = [...path]; - - if (typeof k === 'string') { - commandPath.push([k, ns]); - } - - const cmds = await _getCommandMetadataList(ns, commandPath); - namespacedCommandList.push(...cmds); - })); - } - - return [...commandList, ...namespacedCommandList]; - }; - - const metadata = await this.getMetadata(); - - // TODO: typescript complains about `this`. Calling this method on - // BaseNamespace would be unsafe if the class weren't abstract. Typescript - // bug? I may be wrong. - return _getCommandMetadataList(this as any, [[metadata.name, this as any]]); - } - - async groupCommandsByNamespace(commands: readonly HydratedCommandMetadata[]): Promise & { commands: readonly HydratedCommandMetadata[]; })[]> { - const summaries = new Map(); - const grouped = new Map & { commands: HydratedCommandMetadata[]; }>(); - - await Promise.all(commands.map(async cmd => { - const nsmeta = await cmd.namespace.getMetadata(); - const aliases: string[] = []; - - if (cmd.namespace.parent) { - const siblings = await cmd.namespace.parent.getNamespaces(); - aliases.push(...(siblings.getAliases().get(nsmeta.name) || []).filter((a): a is string => typeof a === 'string')); - } - - summaries.set(nsmeta.name, nsmeta.summary); - let entry = grouped.get(nsmeta.name); - - if (!entry) { - entry = { - namespace: cmd.namespace, - commands: [], - aliases, - ...nsmeta, - description: nsmeta.description ? nsmeta.description : '', - footnotes: nsmeta.footnotes ? nsmeta.footnotes : [], - groups: nsmeta.groups ? nsmeta.groups : [], - }; - grouped.set(nsmeta.name, entry); - } - - entry.commands.push(cmd); - })); - - return [...grouped.values()]; - } -} - -export abstract class Command extends BaseCommand {} -export abstract class Namespace extends BaseNamespace {} -export class CommandMap extends BaseCommandMap {} -export class NamespaceMap extends BaseNamespaceMap {} - -/** - * Given a command object, backtrack through the command's namespace to compile - * a list of command path items which represents how the command was - * found/constructed. - */ -export async function generateCommandPath, N extends INamespace, M extends CommandMetadata, I extends CommandMetadataInput, O extends CommandMetadataOption>(cmd: C): Promise[]> { - const ns = cmd.namespace; - const cmdmeta = await cmd.getMetadata(); - - const _cmdpath = async (namespace: N): Promise[]> => { - const nsmeta = await namespace.getMetadata(); - const nspath: CommandPathItem = [nsmeta.name, namespace]; - - if (!namespace.parent) { - return [nspath]; - } - - return [...(await _cmdpath(namespace.parent)), nspath]; - }; - - return [...(await _cmdpath(ns)), [cmdmeta.name, cmd]]; -} diff --git a/packages/@ionic/cli-framework/src/lib/completion.ts b/packages/@ionic/cli-framework/src/lib/completion.ts deleted file mode 100644 index 1f60ac426d..0000000000 --- a/packages/@ionic/cli-framework/src/lib/completion.ts +++ /dev/null @@ -1,68 +0,0 @@ -import * as lodash from 'lodash'; - -import { CommandMetadata, CommandMetadataInput, CommandMetadataOption, ICommand, INamespace } from '../definitions'; -import { isCommand } from '../guards'; - -import { NO_COLORS } from './colors'; -import { formatOptionName } from './options'; - -export async function getCompletionWords, N extends INamespace, M extends CommandMetadata, I extends CommandMetadataInput, O extends CommandMetadataOption>(ns: N, argv: readonly string[]): Promise { - const { obj } = await ns.locate(argv, { useAliases: false }); - - if (isCommand(obj)) { - const metadata = await obj.getMetadata(); - const options = metadata.options ? metadata.options : []; - - if (options.length === 0) { - return []; - } - - const optionNames = options - .map(option => formatOptionName(option, { showAliases: false, showValueSpec: false, colors: NO_COLORS })) - .filter(name => !argv.includes(name)); - - const aliasNames = lodash.flatten(options.map(option => option.aliases ? option.aliases : [])) - .map(alias => `-${alias}`); - - return [...optionNames, ...aliasNames].sort(); - } - - return [ - ...(await obj.getCommands()).keysWithoutAliases(), - ...(await obj.getNamespaces()).keysWithoutAliases(), - ].sort(); -} - -export interface CompletionFormatterDeps, N extends INamespace, M extends CommandMetadata, I extends CommandMetadataInput, O extends CommandMetadataOption> { - readonly namespace: N; -} - -export abstract class CompletionFormatter, N extends INamespace, M extends CommandMetadata, I extends CommandMetadataInput, O extends CommandMetadataOption> { - protected readonly namespace: N; - - constructor({ namespace }: CompletionFormatterDeps) { - this.namespace = namespace; - } - - abstract format(): Promise; -} - -export class ZshCompletionFormatter, N extends INamespace, M extends CommandMetadata, I extends CommandMetadataInput, O extends CommandMetadataOption> extends CompletionFormatter { - async format(): Promise { - const { name } = await this.namespace.getMetadata(); - - return ` -###-begin-${name}-completion-### - -if type compdef &>/dev/null; then - __${name}() { - compadd -- $(${name} completion -- "$\{words[@]}" 2>/dev/null) - } - - compdef __${name} ${name} -fi - -###-end-${name}-completion-### -`; - } -} diff --git a/packages/@ionic/cli-framework/src/lib/config.ts b/packages/@ionic/cli-framework/src/lib/config.ts deleted file mode 100644 index 14023e16c2..0000000000 --- a/packages/@ionic/cli-framework/src/lib/config.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { mkdirpSync } from '@ionic/utils-fs'; -import * as fs from 'fs'; -import * as lodash from 'lodash'; -import * as path from 'path'; -import * as writeFileAtomic from 'write-file-atomic'; - -export interface BaseConfigOptions { - /** - * The number of spaces to use when writing the JSON file. - */ - spaces?: string | number; - - /** - * If specified, the class will operate on a nested object within the config - * file navigated to by this path prefix, an array of object path keys. - * - * For example, to operate on `c` object within `{ a: { b: { c: {} } } }`, - * use `pathPrefix` of `['a', 'b', 'c']`. - */ - pathPrefix?: readonly string[]; -} - -export abstract class BaseConfig { - protected readonly spaces: string | number; - protected readonly pathPrefix: readonly string[]; - - constructor(readonly p: string, { spaces = 2, pathPrefix = [] }: BaseConfigOptions = {}) { - this.spaces = spaces; - this.pathPrefix = pathPrefix; - } - - get file() { - try { - return this._getFile(); - } catch (e: any) { - return {}; - } - } - - get c(): T { - try { - const file = this._getFile(); - const navigated = this.pathPrefix.length === 0 ? file : lodash.get(file, [...this.pathPrefix]); - const config = typeof navigated === 'object' ? navigated : {}; - - return lodash.assign({}, this.provideDefaults(config), config); - } catch (e: any) { - if (e.code === 'ENOENT' || e.name === 'SyntaxError') { - const value = this.provideDefaults({}); - const v = this.pathPrefix.length === 0 ? value : lodash.set({}, [...this.pathPrefix], value); - this._setFile(v); - return value; - } - - throw e; - } - } - - set c(value: T) { - const v = this.pathPrefix.length === 0 ? value : lodash.set(this.file, [...this.pathPrefix], value); - this._setFile(v); - } - - get

(property: P): T[P]; - get

(property: P, defaultValue: NonNullable): NonNullable; - get

(property: P, defaultValue?: T[P]): T[P] { - const value: any = this.c[property]; - - if (defaultValue && typeof value === 'undefined') { - return defaultValue; - } - - return value; - } - - set

(property: P, value: T[P]): void { - const config = this.c; - config[property] = value; - this.c = config; - } - - unset

(property: P): void { - const config = this.c; - delete config[property]; - this.c = config; - } - - abstract provideDefaults(c: Partial>): T; - - private _getFile(): any { - const contents = fs.readFileSync(this.p, 'utf8'); - return JSON.parse(contents); - } - - private _setFile(value: any): void { - mkdirpSync(path.dirname(this.p)); - writeFileAtomic.sync(this.p, JSON.stringify(value, undefined, this.spaces) + '\n'); - } -} diff --git a/packages/@ionic/cli-framework/src/lib/executor.ts b/packages/@ionic/cli-framework/src/lib/executor.ts deleted file mode 100644 index 7537e048c8..0000000000 --- a/packages/@ionic/cli-framework/src/lib/executor.ts +++ /dev/null @@ -1,197 +0,0 @@ -import { EventEmitter } from 'events'; -import * as lodash from 'lodash'; - -import { CommandInstanceInfo, CommandMetadata, CommandMetadataInput, CommandMetadataOption, ICommand, IExecutor, INamespace, NamespaceLocateResult } from '../definitions'; -import { BaseError, InputValidationError } from '../errors'; -import { isCommand, isNamespace } from '../guards'; - -import { Colors, DEFAULT_COLORS } from './colors'; -import { Command, Namespace } from './command'; -import { CommandHelpSchema, CommandSchemaHelpFormatter, CommandStringHelpFormatter, HelpFormatter, NamespaceHelpSchema, NamespaceSchemaHelpFormatter, NamespaceStringHelpFormatter } from './help'; -import { metadataOptionsToParseArgsOptions, parseArgs, stripOptions } from './options'; - -export type HelpRPC = import('../utils/ipc').RPC<'help', [readonly string[]], S>; - -export interface ExecutorOperations { - readonly RPC: string; -} - -export const EXECUTOR_OPS: ExecutorOperations = Object.freeze({ - RPC: '📡', -}); - -export const HELP_FLAGS = ['--help', '-?']; - -export abstract class AbstractExecutor, N extends INamespace, M extends CommandMetadata, I extends CommandMetadataInput, O extends CommandMetadataOption> extends EventEmitter implements IExecutor { - abstract readonly namespace: N; - - abstract locate(argv: readonly string[]): Promise>; - abstract execute(location: NamespaceLocateResult): Promise; - abstract execute(argv: readonly string[], env: NodeJS.ProcessEnv): Promise; - abstract run(command: C, cmdargs: readonly string[], runinfo?: Partial>): Promise; - - async resolveExecuteInput(argvOrLocation: readonly string[] | NamespaceLocateResult): Promise<[NamespaceLocateResult, string[]]> { - if ('obj' in argvOrLocation) { - return [argvOrLocation, [...argvOrLocation.args]]; - } else { - return [await this.locate(argvOrLocation), [...argvOrLocation]]; - } - } -} - -export interface BaseExecutorFormatHelpOptions { - format?: 'terminal' | 'json'; -} - -export interface BaseExecutorDeps, N extends INamespace, M extends CommandMetadata, I extends CommandMetadataInput, O extends CommandMetadataOption> { - readonly namespace: N; - readonly colors?: Colors; - readonly stdout?: NodeJS.WriteStream; - readonly stderr?: NodeJS.WriteStream; -} - -export interface BaseExecutor, N extends INamespace, M extends CommandMetadata, I extends CommandMetadataInput, O extends CommandMetadataOption> extends AbstractExecutor { - on(event: 'operation-rpc', callback: (rpc: import('../utils/ipc').RPCProcess) => void): this; - emit(event: 'operation-rpc', rpc: import('../utils/ipc').RPCProcess): boolean; -} - -export class BaseExecutor, N extends INamespace, M extends CommandMetadata, I extends CommandMetadataInput, O extends CommandMetadataOption> extends AbstractExecutor { - readonly colors: Colors; - readonly namespace: N; - readonly stdout: NodeJS.WriteStream; - readonly stderr: NodeJS.WriteStream; - - constructor({ namespace, stdout, stderr, colors }: BaseExecutorDeps) { - super(); - this.namespace = namespace; - this.colors = colors ? colors : DEFAULT_COLORS; - this.stdout = stdout ? stdout : process.stdout; - this.stderr = stderr ? stderr : process.stderr; - } - - /** - * Locate a command or namespace given an array of positional arguments - * (argv). - * - * @param argv Command arguments sliced to the root for the namespace of this - * executor. Usually, this means `process.argv.slice(2)`. - */ - async locate(argv: readonly string[]): Promise> { - const parsedArgs = stripOptions(argv, {}); - const location = await this.namespace.locate(parsedArgs); - const args = lodash.drop(argv, location.path.length - 1); - - return { ...location, args }; - } - - /** - * Locate and execute a command given an array of positional command - * arguments (argv) and a set of environment variables. - * - * If a command is not found, formatted help is automatically output for the - * right-most namespace found. - * - * @param argv Command arguments sliced to the root for the namespace of this - * executor. Usually, this means `process.argv.slice(2)`. - * @param env Environment variables for this execution. - */ - async execute(argvOrLocation: readonly string[] | NamespaceLocateResult, env?: NodeJS.ProcessEnv): Promise { - if (Array.isArray(argvOrLocation) && argvOrLocation[0] === EXECUTOR_OPS.RPC) { - return this.rpc(); - } - - const [ location, argv ] = await this.resolveExecuteInput(argvOrLocation); - - if (lodash.intersection(HELP_FLAGS, argv).length > 0 || isNamespace(location.obj)) { - const cmdoptions = parseArgs(argv); - this.stdout.write(await this.formatHelp(location, { format: cmdoptions['json'] ? 'json' : 'terminal' })); - } else { - const cmd = location.obj; - const cmdargs = location.args; - - await this.run(cmd, cmdargs, { location, env, executor: this }); - } - } - - async run(command: C, cmdargs: readonly string[], runinfo?: Partial>): Promise { - const { input } = this.colors; - const metadata = await command.getMetadata(); - const cmdoptions = parseArgs([...cmdargs], metadataOptionsToParseArgsOptions(metadata.options ? metadata.options : [])); - const cmdinputs = cmdoptions._; - - try { - await command.validate(cmdinputs); - } catch (e: any) { - if (e instanceof InputValidationError) { - for (const err of e.errors) { - this.stderr.write(`${err.message}\n`); - } - - this.stderr.write(`Use the ${input('--help')} flag for more details.\n`); - } - - throw e; - } - - await command.run(cmdinputs, cmdoptions, runinfo); - } - - async formatHelp(location: NamespaceLocateResult, { format = 'terminal' }: BaseExecutorFormatHelpOptions = {}): Promise { - let formatter: HelpFormatter; - - if (isCommand(location.obj)) { - const options = { location, command: location.obj, colors: this.colors }; - formatter = format === 'json' ? new CommandSchemaHelpFormatter(options) : new CommandStringHelpFormatter(options); - } else { - const options = { location, namespace: location.obj, colors: this.colors }; - formatter = format === 'json' ? new NamespaceSchemaHelpFormatter(options) : new NamespaceStringHelpFormatter(options); - } - - return formatter.format(); - } - - /** - * Initiate RPC operation. - * - * This means the CLI has been executed by a parent Node process with an IPC - * channel, allowing request/response communication via RPC. - */ - async rpc(): Promise { - const { RPCProcess } = await import('../utils/ipc'); - const metadata = await this.namespace.getMetadata(); - - const rpc = new RPCProcess({ name: metadata.name }); - - rpc.register>('help', async ([cmdpath]) => { - const location = await this.namespace.locate(cmdpath); - - const formatter = isCommand(location.obj) - ? new CommandSchemaHelpFormatter({ location, command: location.obj, colors: this.colors }) - : new NamespaceSchemaHelpFormatter({ location, namespace: location.obj, colors: this.colors }); - - return formatter.serialize(); - }); - - this.emit('operation-rpc', rpc); - - rpc.start(process); - } -} - -export class Executor extends BaseExecutor {} - -export async function execute, N extends INamespace, M extends CommandMetadata, I extends CommandMetadataInput, O extends CommandMetadataOption>({ namespace, argv, env, ...rest }: { namespace: N; argv: string[]; env: NodeJS.ProcessEnv } & Partial>) { - const executor = new BaseExecutor({ namespace, ...rest }); - - try { - await executor.execute(argv, env); - } catch (e: any) { - if (e instanceof BaseError) { - executor.stderr.write(`Error: ${e.message}`); - process.exitCode = typeof e.exitCode === 'undefined' ? 1 : e.exitCode; - return; - } - - throw e; - } -} diff --git a/packages/@ionic/cli-framework/src/lib/help.ts b/packages/@ionic/cli-framework/src/lib/help.ts deleted file mode 100644 index b6c88559d7..0000000000 --- a/packages/@ionic/cli-framework/src/lib/help.ts +++ /dev/null @@ -1,836 +0,0 @@ -import { filter, map } from '@ionic/utils-array'; -import { generateFillSpaceStringList, stringWidth, wordWrap } from '@ionic/utils-terminal'; -import { debug as Debug } from 'debug'; -import * as lodash from 'lodash'; - -import { CommandMetadata, CommandMetadataInput, CommandMetadataOption, Footnote, HydratedCommandMetadata, HydratedNamespaceMetadata, ICommand, INamespace, LinkFootnote, MetadataGroup, NamespaceLocateResult, NamespaceMetadata } from '../definitions'; -import { isHydratedCommandMetadata, isLinkFootnote } from '../guards'; - -import { ColorFunction, Colors, DEFAULT_COLORS } from './colors'; -import { formatOptionName, hydrateOptionSpec } from './options'; -import { validators } from './validators'; - -const debug = Debug('ionic:cli-framework:lib:help'); - -const DEFAULT_DOTS_WIDTH = 32; - -function formatHelpGroups(groups: string[] = [], colors: Colors = DEFAULT_COLORS): string { - const { help: { group: gcolors } } = colors; - - return groups - .map(g => g in gcolors ? gcolors[g as keyof typeof gcolors](`(${g})`) + ' ' : '') - .join(''); -} - -function formatLinkFootnote(footnote: LinkFootnote, colors: Colors = DEFAULT_COLORS): string { - const { strong } = colors; - - return strong(footnote.shortUrl ? footnote.shortUrl : footnote.url); -} - -function formatFootnote(index: number, footnote: Footnote, colors: Colors = DEFAULT_COLORS): string { - const { ancillary } = colors; - const prefix = ancillary(`[${index}]`); - - const output = isLinkFootnote(footnote) ? formatLinkFootnote(footnote, colors) : footnote.text; - - return `${prefix}: ${output}`; -} - -function formatFootnotes(text: string, footnotes?: readonly Footnote[], colors: Colors = DEFAULT_COLORS): string { - if (!footnotes) { - return text; - } - - const { ancillary } = colors; - const discoveredFootnotes: Footnote[] = []; - const output = text.replace(/\[\^([A-z0-9-]+)\]/g, (match, p1) => { - const m = Number.parseInt(p1, 10); - const id = !Number.isNaN(m) ? m : p1; - const foundFootnote = footnotes.find(footnote => footnote.id === id); - - if (foundFootnote) { - const len = discoveredFootnotes.push(foundFootnote); - return ancillary(`[${len}]`); - } else { - debug('No footnote found by ID: %O', id); - return ''; - } - }); - - return output + ( - discoveredFootnotes.length > 0 ? - `\n\n${discoveredFootnotes.map((footnote, i) => formatFootnote(i + 1, footnote, colors)).join('\n')}` : - '' - ); -} - -export async function isOptionVisible(opt: O): Promise { - return !opt.groups || !opt.groups.includes(MetadataGroup.HIDDEN); -} - -export async function isCommandVisible, N extends INamespace, M extends CommandMetadata, I extends CommandMetadataInput, O extends CommandMetadataOption>(cmd: HydratedCommandMetadata): Promise { - const ns = await cmd.namespace.getMetadata(); - return (!cmd.groups || !cmd.groups.includes(MetadataGroup.HIDDEN)) && (!ns.groups || !ns.groups.includes(MetadataGroup.HIDDEN)); -} - -export abstract class HelpFormatter { - protected readonly colors: Colors; - - constructor({ colors }: { colors?: Colors; }) { - this.colors = colors ? colors : DEFAULT_COLORS; - } - - abstract format(): Promise; -} - -export interface NamespaceHelpFormatterDeps, N extends INamespace, M extends CommandMetadata, I extends CommandMetadataInput, O extends CommandMetadataOption> { - readonly location: NamespaceLocateResult; - readonly namespace: N; - readonly colors?: Colors; -} - -export abstract class NamespaceHelpFormatter, N extends INamespace, M extends CommandMetadata, I extends CommandMetadataInput, O extends CommandMetadataOption> extends HelpFormatter { - protected readonly location: NamespaceLocateResult; - protected readonly namespace: N; - protected readonly dotswidth: number = DEFAULT_DOTS_WIDTH; - - protected _metadata?: NamespaceMetadata; - protected _fullName?: string; - - constructor({ location, namespace, colors }: NamespaceHelpFormatterDeps) { - super({ colors }); - this.location = location; - this.namespace = namespace; - } - - protected normalizeMetadata(metadata: NamespaceMetadata): NamespaceMetadata { - return { ...metadata, groups: lodash.uniq(metadata.groups) }; - } - - protected normalizeCommandMetadata(metadata: HydratedCommandMetadata): HydratedCommandMetadata { - return { ...metadata, groups: lodash.uniq(metadata.groups) }; - } - - /** - * Given command metadata, decide whether to keep or discard the command that - * the metadata represents. - * - * @param meta: The metadata of the command. - * @return `true` to keep, `false` to discard - */ - async filterCommandCallback(meta: HydratedCommandMetadata): Promise { - return isCommandVisible(meta); - } - - async getNamespaceMetadata(): Promise { - if (!this._metadata) { - this._metadata = this.normalizeMetadata(await this.namespace.getMetadata()); - } - - return this._metadata; - } - - async getCommandMetadataList(): Promise[]> { - const commands = await this.namespace.getCommandMetadataList(); - - return commands.map(cmd => this.normalizeCommandMetadata(cmd)); - } - - async getNamespaceFullName(): Promise { - if (!this._fullName) { - const parts = await map( - this.location.path.slice(0, this.location.path.length - 1), - async ([, cmd]) => (await cmd.getMetadata()).name - ); - - const name = (await this.getNamespaceMetadata()).name; - - this._fullName = [...parts, name].join(' '); - } - - return this._fullName; - } -} - -export class NamespaceStringHelpFormatter, N extends INamespace, M extends CommandMetadata, I extends CommandMetadataInput, O extends CommandMetadataOption> extends NamespaceHelpFormatter { - async formatHeader(): Promise { - const { strong, input } = this.colors; - const fullName = await this.getNamespaceFullName(); - - const summary = await this.formatSummary(); - const description = await this.formatDescription(); - - return ( - `\n ${strong(`${input(fullName)}${summary}`)}` + - (description ? `\n\n ${description}` : '') + '\n' - ); - } - - async formatSummary(): Promise { - const fullName = await this.getNamespaceFullName(); - const metadata = await this.getNamespaceMetadata(); - const summary = ( - (await this.formatBeforeSummary(metadata)) + - metadata.summary + - (await this.formatAfterSummary(metadata)) - ); - - const wrappedSummary = wordWrap(summary, { indentation: fullName.length + 5 }); - - return wrappedSummary ? ` - ${wrappedSummary}` : ''; - } - - async formatDescription(): Promise { - const metadata = await this.getNamespaceMetadata(); - - if (!metadata.description) { - return ''; - } - - const text = formatFootnotes(metadata.description.trim(), metadata.footnotes, this.colors); - - return wordWrap(text, { indentation: 4 }); - } - - async getGlobalOptions(): Promise { - return []; - } - - async formatUsage(): Promise { - const { help: { title }, weak, input } = this.colors; - const fullName = await this.getNamespaceFullName(); - - const options = ['--help', ...(await this.getGlobalOptions())]; - const usageLines = [ - ` ${weak('[]')} ${options.map(opt => weak('[' + opt + ']')).join(' ')} ${weak('[options]')}`, - ]; - - return ( - `\n ${title('Usage')}:` + - `\n\n ${usageLines.map(u => `${weak('$')} ${input(fullName + ' ' + u)}`).join('\n ')}\n` - ); - } - - async formatCommands() { - const commands = await this.getCommandMetadataList(); - - return this.formatCommandGroup('Commands', commands); - } - - async formatCommandGroup(titleText: string, commands: readonly HydratedCommandMetadata[]): Promise { - const { help: { title } } = this.colors; - - const filteredCommands = await filter(commands, async cmd => this.filterCommandCallback(cmd)); - - const [cmdDetails, nsDetails] = await Promise.all([ - this.getListOfCommandDetails(filteredCommands.filter(cmd => cmd.namespace === this.namespace)), - this.getListOfNamespaceDetails(filteredCommands.filter(cmd => cmd.namespace !== this.namespace)), - ]); - - const details = [...cmdDetails, ...nsDetails]; - - if (details.length === 0) { - return ''; - } - - details.sort(); - - return ( - `\n ${title(titleText)}:` + - `\n\n ${details.join('\n ')}\n` - ); - } - - async getListOfCommandDetails(commands: readonly HydratedCommandMetadata[]): Promise { - const { weak, input } = this.colors; - const fullCmd = commands.map(cmd => lodash.tail(cmd.path).map(([p]) => p).join(' ')); - const fillStringArray = generateFillSpaceStringList(fullCmd, this.dotswidth, weak('.')); - - const formattedCommands = await Promise.all(commands.map(async (cmd, index) => { - const wrapColor: ColorFunction = cmd.groups && cmd.groups.includes(MetadataGroup.DEPRECATED) ? weak : lodash.identity; - - const summary = ( - (await this.formatBeforeCommandSummary(cmd)) + - cmd.summary + - (await this.formatAfterCommandSummary(cmd)) - ); - - const wrappedSummary = wordWrap(summary, { indentation: this.dotswidth + 6 }); - const line = `${input(lodash.tail(cmd.path).map(([p]) => p).join(' '))}${wrappedSummary ? ' ' + fillStringArray[index] + ' ' + wrappedSummary : ''}`; - - return wrapColor(line); - })); - - return formattedCommands; - } - - async getListOfNamespaceDetails(commands: readonly HydratedCommandMetadata[]): Promise { - const { weak, input } = this.colors; - - const namespaces = await this.namespace.groupCommandsByNamespace(commands); - const fillStringArray = generateFillSpaceStringList(namespaces.map(({ name }) => name + ' '), this.dotswidth, weak('.')); - - const formattedNamespaces = await Promise.all(namespaces.map(async (meta, i) => { - const summary = ( - (await this.formatBeforeNamespaceSummary(meta, meta.commands)) + - meta.summary + - (await this.formatAfterNamespaceSummary(meta, meta.commands)) - ); - - const wrappedSummary = wordWrap(summary, { indentation: this.dotswidth + 6 }); - - return `${input(meta.name + ' ')}${wrappedSummary ? ' ' + fillStringArray[i] + ' ' + wrappedSummary : ''}`; - })); - - return formattedNamespaces; - } - - /** - * Insert text before the namespace's summary. - * - * @param meta: The metadata of the namespace. - */ - async formatBeforeSummary(meta: NamespaceMetadata): Promise { - return formatHelpGroups(meta.groups, this.colors); - } - - /** - * Insert text after the namespace's summary. - * - * @param meta: The metadata of the namespace. - */ - async formatAfterSummary(meta: NamespaceMetadata): Promise { - return ''; - } - - /** - * Insert text that appears before a commands's summary. - * - * @param meta: The metadata of the command. - */ - async formatBeforeCommandSummary(meta: HydratedCommandMetadata): Promise { - return formatHelpGroups(meta.groups, this.colors); - } - - /** - * Insert text that appears after a commands's summary. - * - * @param meta: The metadata of the command. - */ - async formatAfterCommandSummary(meta: HydratedCommandMetadata): Promise { - const { weak, input } = this.colors; - - const aliases = meta.aliases.map(alias => lodash.tail(alias.split(' ')).join(' ')); - const formattedAliases = aliases.length > 0 ? weak('(alias' + (aliases.length === 1 ? '' : 'es') + ': ') + aliases.map(a => input(a)).join(', ') + weak(')') : ''; - - return formattedAliases ? ` ${formattedAliases}` : ''; - } - - /** - * Insert text that appears before a namespace's summary. - * - * @param meta The metadata of the namespace. - * @param commands An array of the metadata of the namespace's commands. - */ - async formatBeforeNamespaceSummary(meta: HydratedNamespaceMetadata, commands: readonly HydratedCommandMetadata[]): Promise { - return formatHelpGroups(meta.groups, this.colors); - } - - /** - * Insert text that appears after a namespace's summary. - * - * @param meta The metadata of the namespace. - * @param commands An array of the metadata of the namespace's commands. - */ - async formatAfterNamespaceSummary(meta: HydratedNamespaceMetadata, commands: readonly HydratedCommandMetadata[]): Promise { - const { weak, input } = this.colors; - - const formattedSubcommands = commands.length > 0 ? `${weak('(subcommands:')} ${commands.map(c => input(c.name)).join(', ')}${weak(')')}` : ''; - const formattedAliases = meta.aliases.length > 0 ? `${weak('(alias' + (meta.aliases.length === 1 ? '' : 'es') + ': ') + meta.aliases.map(a => input(a)).join(', ') + weak(')')}` : ''; - - return `${formattedSubcommands ? ` ${formattedSubcommands}` : ''}${formattedAliases ? ` ${formattedAliases}` : ''}`; - } - - async format(): Promise { - return ( - (await this.formatHeader()) + - (await this.formatUsage()) + - (await this.formatCommands()) + - '\n' - ); - } -} - -export interface CommandHelpFormatterDeps, N extends INamespace, M extends CommandMetadata, I extends CommandMetadataInput, O extends CommandMetadataOption> { - readonly location: NamespaceLocateResult; - readonly command: C; - - /** - * Provide extra context with hydrated command metadata. If not provided, - * `command.getMetadata()` is called. - */ - readonly metadata?: HydratedCommandMetadata; - readonly colors?: Colors; -} - -export abstract class CommandHelpFormatter, N extends INamespace, M extends CommandMetadata, I extends CommandMetadataInput, O extends CommandMetadataOption> extends HelpFormatter { - protected readonly location: NamespaceLocateResult; - protected readonly command: C; - protected readonly dotswidth: number = DEFAULT_DOTS_WIDTH; - - protected _metadata?: M; - protected _fullName?: string; - - constructor({ location, command, metadata, colors }: CommandHelpFormatterDeps) { - super({ colors }); - this.location = location; - this.command = command; - this._metadata = metadata ? this.normalizeMetadata(metadata) : undefined; - } - - protected normalizeMetadata(metadata: M | HydratedCommandMetadata): M { - return { ...metadata, groups: lodash.uniq(metadata.groups) }; - } - - /** - * Given an option definition from command metadata, decide whether to keep - * or discard it. - * - * @return `true` to keep, `false` to discard - */ - async filterOptionCallback(option: O): Promise { - return isOptionVisible(option); - } - - async getCommandMetadata(): Promise> { - if (!this._metadata) { - this._metadata = this.normalizeMetadata(await this.command.getMetadata({ location: this.location })); - } - - return this._metadata; - } - - async getCommandFullName(): Promise { - if (!this._fullName) { - const parts = await map( - this.location.path.slice(0, this.location.path.length - 1), - async ([, cmd]) => (await cmd.getMetadata()).name - ); - - const name = (await this.getCommandMetadata()).name; - - this._fullName = [...parts, name].join(' '); - } - - return this._fullName; - } -} - -export class CommandStringHelpFormatter, N extends INamespace, M extends CommandMetadata, I extends CommandMetadataInput, O extends CommandMetadataOption> extends CommandHelpFormatter { - async formatHeader(): Promise { - const { strong, input } = this.colors; - const fullName = await this.getCommandFullName(); - - const summary = await this.formatSummary(); - const description = await this.formatDescription(); - - return ( - `\n ${strong(`${input(fullName)}${summary}`)}` + - (description ? `\n\n ${description}` : '') + '\n' - ); - } - - async formatSummary(): Promise { - const fullName = await this.getCommandFullName(); - const metadata = await this.getCommandMetadata(); - - const summary = ( - (await this.formatBeforeSummary(metadata)) + - metadata.summary + - (await this.formatAfterSummary(metadata)) - ); - - const wrappedSummary = wordWrap(summary, { indentation: fullName.length + 5 }); - - return wrappedSummary ? ` - ${wrappedSummary}` : ''; - } - - async formatDescription(): Promise { - const metadata = await this.getCommandMetadata(); - - if (!metadata.description) { - return ''; - } - - const text = formatFootnotes(metadata.description.trim(), metadata.footnotes, this.colors); - - return wordWrap(text, { indentation: 4 }); - } - - async formatInlineInput(input: I): Promise { - if (input.validators && input.validators.includes(validators.required)) { - return '<' + input.name + '>'; - } - - return '[<' + input.name + '>]'; - } - - async formatUsage(): Promise { - const { help: { title }, weak, input } = this.colors; - const fullName = await this.getCommandFullName(); - const metadata = await this.getCommandMetadata(); - - const options = metadata.options ? metadata.options : []; - const filteredOptions = await filter(options, async opt => this.filterOptionCallback(opt)); - const formattedInputs = metadata.inputs ? await Promise.all(metadata.inputs.map(async i => this.formatInlineInput(i))) : []; - - return ( - `\n ${title('Usage')}:` + - `\n\n ${weak('$')} ${input(fullName + (formattedInputs.length > 0 ? ' ' + formattedInputs.join(' ') : ''))}${filteredOptions.length > 0 ? ' ' + input('[options]') : ''}\n` - ); - } - - async formatInputs(): Promise { - const { help: { title } } = this.colors; - const metadata = await this.getCommandMetadata(); - const inputs = metadata.inputs ? metadata.inputs : []; - - if (inputs.length === 0) { - return ''; - } - - const formattedInputs = await Promise.all(inputs.map(input => this.formatInput(input))); - - return ( - `\n ${title('Inputs')}:` + - `\n\n ${formattedInputs.join('\n ')}\n` - ); - } - - async formatInput(i: I): Promise { - const { input, weak } = this.colors; - const inputName = input(i.name); - const inputNameLength = stringWidth(inputName); - const fullLength = inputNameLength > this.dotswidth ? inputNameLength + 1 : this.dotswidth; - const fullDescription = ( - (await this.formatBeforeInputSummary(i)) + - i.summary + - (await this.formatAfterInputSummary(i)) - ); - - const wrappedDescription = wordWrap(fullDescription, { indentation: this.dotswidth + 6 }); - - return `${inputName} ${weak('.').repeat(fullLength - inputNameLength)} ${wrappedDescription}`; - } - - async formatOptionLine(opt: O) { - const { weak } = this.colors; - const wrapColor: ColorFunction = opt.groups && opt.groups.includes(MetadataGroup.DEPRECATED) ? weak : lodash.identity; - const optionName = formatOptionName(opt, { colors: this.colors }); - const optionNameLength = stringWidth(optionName); - const fullLength = optionNameLength > this.dotswidth ? optionNameLength + 1 : this.dotswidth; - const fullDescription = ( - (await this.formatBeforeOptionSummary(opt)) + - opt.summary + - (await this.formatAfterOptionSummary(opt)) - ); - - const wrappedDescription = wordWrap(fullDescription, { indentation: this.dotswidth + 6 }); - const line = `${optionName} ${weak('.').repeat(fullLength - optionNameLength)} ${wrappedDescription}`; - - return wrapColor(line); - } - - /** - * Insert text that appears before the command's summary. - * - * @param meta The metadata of the command. - */ - async formatBeforeSummary(meta: M): Promise { - return formatHelpGroups(meta.groups, this.colors); - } - - /** - * Insert text that appears after the command's summary. - * - * @param meta The metadata of the command. - */ - async formatAfterSummary(meta: M): Promise { - return ''; - } - - /** - * Insert text that appears before the input's summary. - * - * @param input The metadata of the input. - */ - async formatBeforeInputSummary(input: I): Promise { - const { weak } = this.colors; - const required = input.validators && input.validators.includes(validators.required) ? true : false; - - return required ? '' : `${weak('(optional)')} `; - } - - /** - * Insert text that appears after the input's summary. - * - * @param input The metadata of the input. - */ - async formatAfterInputSummary(input: I): Promise { - return ''; - } - - /** - * Insert text that appears before an option's summary. - * - * @param opt The metadata of the option. - */ - async formatBeforeOptionSummary(opt: O): Promise { - return formatHelpGroups(opt.groups, this.colors); - } - - /** - * Insert text that appears after the option's summary. - * - * @param opt The metadata of the option. - */ - async formatAfterOptionSummary(opt: O): Promise { - return this.formatOptionDefault(opt); - } - - async formatOptionDefault(opt: O): Promise { - const { weak, input } = this.colors; - - if (typeof opt.default === 'string') { - return weak(' (default: ') + input(opt.default) + weak(')'); - } else { - return ''; - } - } - - async formatOptions(): Promise { - const metadata = await this.getCommandMetadata(); - const options = metadata.options ? metadata.options : []; - - return this.formatOptionsGroup('Options', options); - } - - async formatOptionsGroup(titleText: string, options: O[]): Promise { - const { help: { title } } = this.colors; - - const filteredOptions = await filter(options, async opt => this.filterOptionCallback(opt)); - - if (filteredOptions.length === 0) { - return ''; - } - - const formattedOptions = await Promise.all(filteredOptions.map(async option => this.formatOptionLine(option))); - - return ( - `\n ${title(titleText)}:` + - `\n\n ${formattedOptions.join('\n ')}\n` - ); - } - - async formatExamples(): Promise { - const { help: { title }, weak, input } = this.colors; - const metadata = await this.getCommandMetadata(); - const fullName = await this.getCommandFullName(); - - if (!metadata.exampleCommands || !Array.isArray(metadata.exampleCommands)) { - return ''; - } - - const exampleLines = metadata.exampleCommands.map(cmd => { - const sepIndex = cmd.indexOf(' -- '); - cmd = sepIndex === -1 ? input(cmd) : input(cmd.substring(0, sepIndex)) + cmd.substring(sepIndex); - const wrappedCmd = wordWrap(cmd, { indentation: 12, append: ' \\' }); - - return `${weak('$')} ${input(fullName + ' ')}${wrappedCmd ? wrappedCmd : ''}`; - }); - - return ( - `\n ${title('Examples')}:` + - `\n\n ${exampleLines.join('\n ')}\n` - ); - } - - async format(): Promise { - return ( - (await this.formatHeader()) + - (await this.formatUsage()) + - (await this.formatInputs()) + - (await this.formatOptions()) + - (await this.formatExamples()) + - '\n' - ); - } -} - -export interface NamespaceHelpSchema { - readonly name: string; - readonly summary: string; - readonly description: string; - readonly groups: readonly string[]; - readonly commands: CommandHelpSchema[]; - readonly aliases: readonly string[]; -} - -export class NamespaceSchemaHelpFormatter, N extends INamespace, M extends CommandMetadata, I extends CommandMetadataInput, O extends CommandMetadataOption> extends NamespaceHelpFormatter { - async format(): Promise { - return JSON.stringify(await this.serialize()); - } - - async serialize(): Promise { - const metadata = await this.getNamespaceMetadata(); - const commands = await this.getCommandMetadataList(); - - return { - name: metadata.name, - summary: metadata.summary, - description: metadata.description ? metadata.description : '', - groups: metadata.groups ? metadata.groups : [], - commands: await this.formatCommandGroup(commands), - aliases: [], - }; - } - - async formatCommandGroup(commands: readonly HydratedCommandMetadata[]): Promise { - const filteredCommands = await filter(commands, async cmd => this.filterCommandCallback(cmd)); - - return map(filteredCommands, async cmd => this.formatCommand(cmd)); - } - - async formatCommand(cmd: HydratedCommandMetadata): Promise { - const { command } = cmd; - - const formatter = new CommandSchemaHelpFormatter({ - location: { path: [...cmd.path], obj: command, args: [] }, - command, - metadata: cmd, - }); - - return formatter.serialize(); - } -} - -export interface CommandHelpSchemaInput { - readonly name: string; - readonly summary: string; - readonly required: boolean; -} - -export interface CommandHelpSchemaOption { - readonly name: string; - readonly summary: string; - readonly groups: readonly string[]; - readonly aliases: readonly string[]; - readonly type: string; - readonly default?: string | boolean; - readonly spec: { - readonly value: string; - }; -} - -export interface CommandHelpSchemaFootnoteText { - readonly type: 'text'; - readonly id: string | number; - readonly text: string; -} - -export interface CommandHelpSchemaFootnoteLink { - readonly type: 'link'; - readonly id: string | number; - readonly url: string; - readonly shortUrl?: string; -} - -export type CommandHelpSchemaFootnote = CommandHelpSchemaFootnoteText | CommandHelpSchemaFootnoteLink; - -export interface CommandHelpSchema { - readonly name: string; - readonly namespace: readonly string[]; - readonly summary: string; - readonly description: string; - readonly footnotes: readonly CommandHelpSchemaFootnote[]; - readonly groups: readonly string[]; - readonly exampleCommands: readonly string[]; - readonly aliases: readonly string[]; - readonly inputs: readonly CommandHelpSchemaInput[]; - readonly options: readonly CommandHelpSchemaOption[]; -} - -export class CommandSchemaHelpFormatter, N extends INamespace, M extends CommandMetadata, I extends CommandMetadataInput, O extends CommandMetadataOption> extends CommandHelpFormatter { - async format(): Promise { - return JSON.stringify(await this.serialize()); - } - - async serialize(): Promise { - const metadata = await this.getCommandMetadata(); - - return this.formatCommand(metadata); - } - - async formatInputs(inputs: readonly I[]): Promise { - return Promise.all(inputs.map(async input => this.formatInput(input))); - } - - async formatInput(input: I): Promise { - const name = input.name; - const summary = input.summary; - const required = input.validators && input.validators.includes(validators.required) ? true : false; - - return { name, summary, required }; - } - - async formatOptions(options: readonly O[]): Promise { - const filteredOptions = await filter(options, async opt => this.filterOptionCallback(opt)); - - return Promise.all(filteredOptions.map(async opt => this.formatOption(opt))); - } - - async formatOption(option: O): Promise { - const name = option.name; - const summary = option.summary ? option.summary.trim() : ''; - const groups = option.groups ? option.groups : []; - const aliases = option.aliases ? option.aliases : []; - const type = option.type ? option.type.name.toLowerCase() : 'string'; - const spec = hydrateOptionSpec(option); - - return { name, type, summary, default: option.default, groups, aliases, spec }; - } - - formatFootnote(footnote: Footnote): CommandHelpSchemaFootnote { - return isLinkFootnote(footnote) ? ({ type: 'link', ...footnote }) : ({ type: 'text', ...footnote }); - } - - async formatCommand(cmd: M | HydratedCommandMetadata): Promise { - const commandPath = this.location.path.map(([p]) => p); - const namespacePath = lodash.initial(commandPath); - const name = commandPath.join(' '); - const summary = cmd.summary ? cmd.summary.trim() : ''; - const description = cmd.description ? cmd.description.trim() : ''; - const footnotes = cmd.footnotes ? cmd.footnotes.map(footnote => this.formatFootnote(footnote)) : []; - const groups = cmd.groups ? cmd.groups : []; - const exampleCommands = cmd.exampleCommands ? cmd.exampleCommands.map(c => `${name} ${c}`) : []; - const aliases = isHydratedCommandMetadata(cmd) ? cmd.aliases : []; - const inputs = cmd.inputs ? await this.formatInputs(cmd.inputs) : []; - const options = cmd.options ? await this.formatOptions(cmd.options) : []; - - return { name, namespace: namespacePath, summary, description, footnotes, groups, exampleCommands, aliases, inputs, options }; - } -} - -export function createCommandMetadataFromSchema(schema: CommandHelpSchema): Required { - return { - name: schema.name, - summary: schema.summary, - description: schema.description, - footnotes: [...schema.footnotes], - groups: [...schema.groups], - exampleCommands: [...schema.exampleCommands], - inputs: [...schema.inputs], - options: schema.options.map(opt => ({ ...opt, type: opt.type === 'boolean' ? Boolean : String, groups: [...opt.groups], aliases: [...opt.aliases] })), - }; -} diff --git a/packages/@ionic/cli-framework/src/lib/index.ts b/packages/@ionic/cli-framework/src/lib/index.ts deleted file mode 100644 index 7161fd1781..0000000000 --- a/packages/@ionic/cli-framework/src/lib/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export * from './colors'; -export * from './command'; -export * from './completion'; -export * from './config'; -export * from './executor'; -export * from './help'; -export * from './options'; -export * from './validators'; diff --git a/packages/@ionic/cli-framework/src/lib/options.ts b/packages/@ionic/cli-framework/src/lib/options.ts deleted file mode 100644 index dc5994737a..0000000000 --- a/packages/@ionic/cli-framework/src/lib/options.ts +++ /dev/null @@ -1,296 +0,0 @@ -import * as lodash from 'lodash'; -import minimist from 'minimist'; - -import { CommandLineOptions, CommandMetadataOption, HydratedParseArgsOptions, ParsedArg } from '../definitions'; - -import { Colors, DEFAULT_COLORS } from './colors'; - -export const parseArgs = minimist; -export { ParsedArgs } from 'minimist'; - -/** - * Remove options, which are any arguments that starts with a hyphen (-), from - * a list of process args and return the result. - * - * If a double-hyphen separator (--) is encountered, it and the remaining - * arguments are included in the result, as they are not interpreted. This - * behavior can be disabled by setting the `includeSeparated` option to - * `false`. - */ -export function stripOptions(pargv: readonly string[], { includeSeparated = true }: { includeSeparated?: boolean; }): string[] { - const r = /^\-/; - const [ownArgs, otherArgs] = separateArgv(pargv); - const filteredArgs = ownArgs.filter(arg => !r.test(arg)); - - if (!includeSeparated) { - return filteredArgs; - } - - if (otherArgs.length > 0) { - otherArgs.unshift('--'); - } - - return [...filteredArgs, ...otherArgs]; -} - -/** - * Split a list of process args into own-arguments and other-arguments, which - * are separated by the double-hyphen (--) separator. - * - * For example, `['cmd', 'arg1', '--', 'arg2']` will be split into - * `['cmd', 'arg1']` and `['arg2']`. - */ -export function separateArgv(pargv: readonly string[]): [string[], string[]] { - const ownArgs = [...pargv]; - const otherArgs: string[] = []; - const sepIndex = pargv.indexOf('--'); - - if (sepIndex >= 0) { - otherArgs.push(...ownArgs.splice(sepIndex)); - otherArgs.shift(); // strip separator - } - - return [ownArgs, otherArgs]; -} - -/** - * Takes a Minimist command option and normalizes its values. - */ -export function hydrateCommandMetadataOption(option: O): O { - const type = option.type ? option.type : String; - - return lodash.assign({}, option, { - type, - default: typeof option.default !== 'undefined' ? option.default : null, - aliases: Array.isArray(option.aliases) ? option.aliases : [], - }); -} - -export interface HydratedOptionSpec { - readonly value: string; -} - -export function hydrateOptionSpec(opt: O): HydratedOptionSpec { - return { ...{ value: opt.type === Boolean ? 'true/false' : opt.name }, ...opt.spec || {} }; -} - -export interface FormatOptionNameOptions { - readonly showAliases?: boolean; - readonly showValueSpec?: boolean; - readonly colors?: Colors; -} - -export function formatOptionName(opt: O, { showAliases = true, showValueSpec = true, colors = DEFAULT_COLORS }: FormatOptionNameOptions = {}): string { - const { input, weak } = colors; - const spec = hydrateOptionSpec(opt); - - const showInverse = opt.type === Boolean && opt.default === true && opt.name.length > 1; - const valueSpec = opt.type === Boolean ? '' : `=<${spec.value}>`; - const aliasValueSpec = opt.type === Boolean ? '' : '=?'; - - return ( - (showInverse ? input(`--no-${opt.name}`) : input(`-${opt.name.length > 1 ? '-' : ''}${opt.name}`)) + - (showValueSpec ? weak(valueSpec) : '') + - (showAliases ? - (!showInverse && opt.aliases && opt.aliases.length > 0 ? ', ' + opt.aliases - .map(alias => input(`-${alias}`) + (showValueSpec ? weak(aliasValueSpec) : '')) - .join(', ') : '') : '') - ); -} - -export function metadataOptionsToParseArgsOptions(commandOptions: readonly CommandMetadataOption[]): HydratedParseArgsOptions { - const options: HydratedParseArgsOptions = { - string: ['_'], - boolean: [], - alias: {}, - default: {}, - '--': true, - }; - - for (const o of commandOptions) { - const opt = hydrateCommandMetadataOption(o); - - if (opt.type === String) { - options.string.push(opt.name); - } else if (opt.type === Boolean) { - options.boolean.push(opt.name); - } - - if (typeof opt.default !== 'undefined') { - options.default[opt.name] = opt.default; - } - - if (typeof opt.aliases !== 'undefined') { - options.alias[opt.name] = opt.aliases; - } - } - - return options; -} - -export type OptionPredicate = (option: O, value?: ParsedArg) => boolean; - -export namespace OptionFilters { - export function includesGroups(groups: string | string[]): OptionPredicate { - const g = Array.isArray(groups) ? groups : [groups]; - return (option: O) => typeof option.groups !== 'undefined' && lodash.intersection(option.groups, g).length > 0; - } - - export function excludesGroups(groups: string | string[]): OptionPredicate { - const g = Array.isArray(groups) ? groups : [groups]; - return (option: O) => typeof option.groups === 'undefined' || lodash.difference(option.groups, g).length > 0; - } -} - -/** - * Given an array of command metadata options and an object of parsed options, - * match each supplied option with its command metadata option definition and - * pass it, along with its value, to a predicate function, which is used to - * return a subset of the parsed options. - * - * Options which are unknown to the command metadata are always excluded. - * - * @param predicate If excluded, `() => true` is used. - */ -export function filterCommandLineOptions(options: readonly O[], parsedArgs: CommandLineOptions, predicate: OptionPredicate = () => true): CommandLineOptions { - const initial: CommandLineOptions = { _: parsedArgs._ }; - - if (parsedArgs['--']) { - initial['--'] = parsedArgs['--']; - } - - const mapped = new Map([ - ...options.map((o): [string, O] => [o.name, o]), - ...lodash.flatten(options.map(opt => opt.aliases ? opt.aliases.map((a): [string, O] => [a, opt]) : [])), - ]); - - const pairs = Object.keys(parsedArgs) - .map((k): [string, O | undefined, ParsedArg | undefined] => [k, mapped.get(k), parsedArgs[k]]) - .filter(([k, opt, value]) => opt && predicate(opt, value)) - .map(([k, opt, value]) => [opt ? opt.name : k, value]); - - return { ...initial, ...lodash.fromPairs(pairs) }; -} - -/** - * Given an array of command metadata options and an object of parsed options, - * return a subset of the parsed options whose command metadata option - * definition contains the supplied group(s). - * - * Options which are unknown to the command metadata are always excluded. - * - * @param groups One or more option groups. - */ -export function filterCommandLineOptionsByGroup(options: readonly O[], parsedArgs: CommandLineOptions, groups: string | string[]): CommandLineOptions { - return filterCommandLineOptions(options, parsedArgs, OptionFilters.includesGroups(groups)); -} - -export interface UnparseArgsOptions { - useDoubleQuotes?: boolean; - useEquals?: boolean; - ignoreFalse?: boolean; - allowCamelCase?: boolean; -} - -/** - * The opposite of `parseArgs()`. This function takes parsed args and converts - * them back into an argv array of arguments and options. - * - * Based on dargs, by sindresorhus - * @see https://github.com/sindresorhus/dargs/blob/master/license - * - * @param parsedArgs Inputs and options parsed by minimist. - * @param options.useDoubleQuotes For options with values, wrap the value in - * double quotes if it contains a space. - * @param options.useEquals Instead of separating an option and its value with - * a space, use an equals sign. - * @param options.ignoreFalse Optionally ignore flags that equate to false. - * @param options.allowCamelCase Optionally allow camel cased options instead - * of converting to kebab case. - * @param parseArgsOptions To provide more accuracy, specify the options that - * were used to parse the args in the first place. - */ -export function unparseArgs(parsedArgs: minimist.ParsedArgs, { useDoubleQuotes, useEquals = true, ignoreFalse = true, allowCamelCase }: UnparseArgsOptions = {}, parseArgsOptions?: minimist.Opts): string[] { - const args = [...parsedArgs['_'] || []]; - const separatedArgs = parsedArgs['--']; - - if (useDoubleQuotes) { - useEquals = true; - } - - const dashKey = (k: string) => (k.length === 1 ? '-' : '--') + k; - - const pushPairs = (...pairs: [string, string | undefined][]) => { - for (const [k, val] of pairs) { - const key = dashKey(allowCamelCase ? k : k.replace(/[A-Z]/g, '-$&').toLowerCase()); - - if (useEquals) { - args.push(key + (val ? `=${useDoubleQuotes && val.includes(' ') ? `"${val}"` : val}` : '')); - } else { - args.push(key); - - if (val) { - args.push(val); - } - } - } - }; - - // Normalize the alias definitions from the options for `parseArgs`. - const aliasDef: { [key: string]: string[]; } = parseArgsOptions && parseArgsOptions.alias - ? lodash.mapValues(parseArgsOptions.alias, v => Array.isArray(v) ? v : [v]) - : {}; - - // Construct a mapping of alias to original key name. - const aliases = new Map(lodash.flatten(Object.keys(aliasDef).map(k => aliasDef[k].map((a): [string, string] => [a, k])))); - - const isKnown = (key: string) => { - if (!parseArgsOptions || !parseArgsOptions.unknown) { - return true; - } - - if ( - (typeof parseArgsOptions.string !== 'undefined' && (Array.isArray(parseArgsOptions.string) && parseArgsOptions.string.includes(key))) || - (typeof parseArgsOptions.boolean !== 'undefined' && (Array.isArray(parseArgsOptions.boolean) && parseArgsOptions.boolean.includes(key))) || - aliases.has(key) - ) { - return true; - } - - return parseArgsOptions.unknown(key); - }; - - // Convert the parsed args to an array of 2-tuples of shape [key, value]. - // Then, filter out pairs which match any of the following criteria: - // - `_` (positional argument list) - // - `--` (separated args) - // - Aliases whose original key is defined - // - Options not known to the schema, according to - // `parseArgsOptions.unknown` option. - const pairedOptions = lodash.toPairs(parsedArgs).filter(([k]) => - k !== '_' && - k !== '--' && - !(aliases.get(k) && typeof parsedArgs[k] !== 'undefined') && - isKnown(k) - ); - - for (const [key, val] of pairedOptions) { - if (val === true) { - pushPairs([key, undefined]); - } else if (val === false && !ignoreFalse) { - pushPairs([`no-${key}`, undefined]); - } else if (typeof val === 'string') { - pushPairs([key, val]); - } else if (typeof val === 'number' && !Number.isNaN(val)) { - pushPairs([key, val.toString()]); - } else if (Array.isArray(val)) { - pushPairs(...val.map((v): [string, string] => [key, v])); - } - } - - if (separatedArgs && separatedArgs.length > 0) { - args.push('--', ...separatedArgs); - } - - return args; -} diff --git a/packages/@ionic/cli-framework/src/lib/validators.ts b/packages/@ionic/cli-framework/src/lib/validators.ts deleted file mode 100644 index a22ee49d81..0000000000 --- a/packages/@ionic/cli-framework/src/lib/validators.ts +++ /dev/null @@ -1,119 +0,0 @@ -import chalk from 'chalk'; - -import { ValidationError, Validator, Validators } from '../definitions'; -import { InputValidationError } from '../errors'; -import { isValidEmail, isValidURL, slugify } from '../utils/string'; - -export function validate(input: string, key: string, validatorsToUse: Validator[]): void { - const errors: ValidationError[] = []; - - for (const validator of validatorsToUse) { - const message = validator(input, key); - - if (message !== true) { - errors.push({ key, message, validator }); - } - } - - if (errors.length > 0) { - throw new InputValidationError('Invalid inputs.', errors); - } -} - -export const validators: Validators = { - required(input?: string, key?: string) { - if (!input) { - if (key) { - return `${chalk.green(key)} must not be empty.`; - } else { - return 'Must not be empty.'; - } - } - - return true; - }, - email(input?: string, key?: string) { - if (!isValidEmail(input)) { - if (key) { - return `${chalk.green(key)} is an invalid email address.`; - } else { - return 'Invalid email address.'; - } - } - - return true; - }, - url(input?: string, key?: string) { - if (!isValidURL(input)) { - if (key) { - return `${chalk.green(key)} is an invalid URL.`; - } else { - return 'Invalid URL.'; - } - } - - return true; - }, - numeric(input?: string, key?: string) { - if (isNaN(Number(input))) { - if (key) { - return `${chalk.green(key)} must be numeric.`; - } else { - return 'Must be numeric.'; - } - } - - return true; - }, - slug(input?: string, key?: string) { - if (!input || slugify(input) !== input) { - if (key) { - return `${chalk.green(key)} is an invalid slug (machine name).`; - } else { - return 'Invalid slug (machine name).'; - } - } - - return true; - }, -}; - -export function combine(...validators: Validator[]): Validator { - return (input?: string, key?: string) => { - for (const validator of validators) { - const message = validator(input, key); - - if (message !== true) { - return message; - } - } - - return true; - }; -} - -export function contains(values: (string | undefined)[], { caseSensitive = true }: { caseSensitive?: boolean }): Validator { - if (!caseSensitive) { - values = values.map(v => typeof v === 'string' ? v.toLowerCase() : v); - } - - return (input?: string, key?: string): true | string => { - if (!caseSensitive && typeof input === 'string') { - input = input.toLowerCase(); - } - - if (values.indexOf(input) === -1) { - const strValues = values.filter(v => typeof v === 'string') as string[]; // TODO: typescript bug? - const mustBe = (strValues.length !== values.length ? 'unset or one of' : 'one of') + ': ' + strValues.map(v => chalk.green(v)).join(', '); - const fmtPretty = (v?: string) => typeof v === 'undefined' ? 'unset' : (v === '' ? 'empty' : chalk.green(v)); - - if (key) { - return `${chalk.green(key)} must be ${mustBe} (not ${fmtPretty(input)})`; - } else { - return `Must be ${mustBe} (not ${fmtPretty(input)})`; - } - } - - return true; - }; -} diff --git a/packages/@ionic/cli-framework/src/utils/__tests__/fn.ts b/packages/@ionic/cli-framework/src/utils/__tests__/fn.ts deleted file mode 100644 index 5f16fa6c2e..0000000000 --- a/packages/@ionic/cli-framework/src/utils/__tests__/fn.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { resolveValue, resolveValueSync } from '../fn'; - -describe('@ionic/cli-framework', () => { - - describe('fn', () => { - - describe('resolveValue', () => { - - it('should resolve with undefined with 0 functions', async () => { - const result = await resolveValue(); - expect(result).not.toBeDefined(); - }); - - it('should resolve with undefined with functions that never resolve values', async () => { - const result = await resolveValue(async () => {}, async () => {}, async () => {}); - expect(result).not.toBeDefined(); - }); - - it('should resolve with value of first function that resolve value', async () => { - const result = await resolveValue(async () => 1, async () => 2, async () => 3); - expect(result).toBe(1); - }); - - it('should skip functions that do not resolve values', async () => { - const result = await resolveValue(async () => undefined, async () => 2, async () => 3); - expect(result).toBe(2); - }); - - describe('falsy values', () => { - - it('should resolve with null', async () => { - const result = await resolveValue(async () => null, async () => 'foo'); - expect(result).toBe(null); - }); - - it('should resolve with 0', async () => { - const result = await resolveValue(async () => 0, async () => 5); - expect(result).toBe(0); - }); - - it('should resolve with empty string', async () => { - const result = await resolveValue(async () => '', async () => 'foo'); - expect(result).toBe(''); - }); - - }); - - }); - - describe('resolveValueSync', () => { - - it('should return undefined with 0 functions', () => { - const result = resolveValueSync(); - expect(result).not.toBeDefined(); - }); - - it('should return undefined with functions that never return values', () => { - const result = resolveValueSync(() => {}, () => {}, () => {}); - expect(result).not.toBeDefined(); - }); - - it('should return value of first function that return value', () => { - const result = resolveValueSync(() => 1, () => 2, () => 3); - expect(result).toBe(1); - }); - - it('should skip functions that do not return values', () => { - const result = resolveValueSync(() => undefined, () => 2, () => 3); - expect(result).toBe(2); - }); - - describe('falsy values', () => { - - it('should return null', () => { - const result = resolveValueSync(() => null, () => 'foo'); - expect(result).toBe(null); - }); - - it('should return 0', () => { - const result = resolveValueSync(() => 0, () => 5); - expect(result).toBe(0); - }); - - it('should return empty string', () => { - const result = resolveValueSync(() => '', () => 'foo'); - expect(result).toBe(''); - }); - - }); - - }); - - }); - -}); diff --git a/packages/@ionic/cli-framework/src/utils/__tests__/node.ts b/packages/@ionic/cli-framework/src/utils/__tests__/node.ts deleted file mode 100644 index 279c4aa964..0000000000 --- a/packages/@ionic/cli-framework/src/utils/__tests__/node.ts +++ /dev/null @@ -1,59 +0,0 @@ -import * as path from 'path' - -describe('@ionic/cli-framework', () => { - - describe('utils/node', () => { - - describe('compileNodeModulesPaths', () => { - - describe('posix', () => { - const mock_path_posix = path.posix; - jest.resetModules(); - jest.mock('path', () => mock_path_posix); - - const nodeLib = require('../node'); - - it('should not accept a malformed path', () => { - expect(() => nodeLib.compileNodeModulesPaths('.')).toThrowError('. is not an absolute path'); - }); - - it('should compile an array of node_modules directories working backwards from a base directory', () => { - const result = nodeLib.compileNodeModulesPaths('/some/dir'); - expect(result).toEqual(['/some/dir/node_modules', '/some/node_modules', '/node_modules']); - }); - - it('should work for the root directory', () => { - const result = nodeLib.compileNodeModulesPaths('/'); - expect(result).toEqual(['/node_modules']); - }); - - }); - - describe('windows', () => { - const mock_path_win32 = path.win32; - jest.resetModules(); - jest.mock('path', () => mock_path_win32); - - const nodeLib = require('../node'); - - it('should not accept a malformed path', () => { - expect(() => nodeLib.compileNodeModulesPaths('.')).toThrowError('. is not an absolute path'); - }); - - it('should compile an array of node_modules directories working backwards from a base directory', () => { - const result = nodeLib.compileNodeModulesPaths('\\some\\dir'); - expect(result).toEqual(['\\some\\dir\\node_modules', '\\some\\node_modules', '\\node_modules']); - }); - - it('should work for the root directory', () => { - const result = nodeLib.compileNodeModulesPaths('\\'); - expect(result).toEqual(['\\node_modules']); - }); - - }); - - }); - - }); - -}); diff --git a/packages/@ionic/cli-framework/src/utils/__tests__/promise.ts b/packages/@ionic/cli-framework/src/utils/__tests__/promise.ts deleted file mode 100644 index e29f1786c4..0000000000 --- a/packages/@ionic/cli-framework/src/utils/__tests__/promise.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { EventEmitter } from 'events'; - -import { PromiseUtil, promisifyEvent } from '../promise'; - -describe('@ionic/cli-framework', () => { - - describe('utils/promise', () => { - - describe('promisifyEvent', () => { - - it('should reject on error event', async () => { - const emitter = new EventEmitter(); - const p = promisifyEvent(emitter, 'asdf'); - const err = new Error('some error'); - emitter.emit('error', err); - emitter.emit('asdf'); - expect(p).rejects.toThrow(err); - }); - - it('should resolve when event is emitted', async () => { - const emitter = new EventEmitter(); - const p = promisifyEvent(emitter, 'asdf'); - emitter.emit('asdf'); - await p; - }); - - it('should resolve with emitted value', async () => { - const emitted = {}; - const emitter = new EventEmitter(); - const p = promisifyEvent(emitter, 'asdf'); - emitter.emit('asdf', emitted); - const result = await p; - expect(result).toBe(emitted); - }); - - }); - - describe('PromiseUtil', () => { - - it('should resolve for 1 promise using some by default', async () => { - const result = await PromiseUtil.some([Promise.resolve('a'), Promise.resolve('b'), Promise.resolve('c')]); - expect(result.length).toEqual(1); - for (const val of result) { - expect(val).toEqual(expect.stringMatching(/^[abc]{1}$/)); - } - }); - - it('should resolve for specified number of promises using some', async () => { - const result = await PromiseUtil.some([Promise.resolve('a'), Promise.resolve('b'), Promise.resolve('c')], 2); - expect(result.length).toEqual(2); - for (const val of result) { - expect(val).toEqual(expect.stringMatching(/^[abc]{1}$/)); - } - }); - - it('should resolve for all promises using some', async () => { - const promises = [Promise.resolve('a'), Promise.resolve('b'), Promise.resolve('c')]; - const result = await PromiseUtil.some(promises, promises.length); - expect(result.length).toEqual(promises.length); - for (const val of result) { - expect(val).toEqual(expect.stringMatching(/^[abc]{1}$/)); - } - }); - - it('should resolve for 1 promise value using any', async () => { - const result = await PromiseUtil.any([Promise.resolve('a'), Promise.resolve('b'), Promise.resolve('c')]); - expect(result).toEqual(expect.stringMatching(/^[abc]{1}$/)); - }); - - }); - - }); - -}); diff --git a/packages/@ionic/cli-framework/src/utils/__tests__/string.ts b/packages/@ionic/cli-framework/src/utils/__tests__/string.ts deleted file mode 100644 index 4ba47ce739..0000000000 --- a/packages/@ionic/cli-framework/src/utils/__tests__/string.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { isValidURL, slugify } from '../string'; - -describe('@ionic/cli-framework', () => { - - describe('utils/string', () => { - - describe('isValidURL', () => { - - it('should work without parameter', () => { - expect(isValidURL()).toBe(false); - }); - - it('should work with non-string', () => { - expect(isValidURL(5)).toBe(false); - }); - - it('should test a non-url', () => { - expect(isValidURL('this is some text')).toBe(false); - }); - - it('should test text with a colon', () => { - expect(isValidURL('let me tell you something: hello')).toBe(false); - }); - - it('should test a localhost url', () => { - expect(isValidURL('http://localhost')).toBe(true); - }); - - it('should test an https url', () => { - expect(isValidURL('https://ionicframework.com')).toBe(true); - }); - - it('should test a git url', () => { - expect(isValidURL('git@github.com:ionic-team/ionic-cli.git')).toBe(true); - }); - }); - - describe('slugify', () => { - - it('should pass back the empty string', () => { - const result = slugify(''); - expect(result).toEqual(''); - }); - - it('should not change slugified input', () => { - const result = slugify('foo'); - expect(result).toEqual('foo'); - }); - - it('should trim whitespace', () => { - const result = slugify(' foo '); - expect(result).toEqual('foo'); - }); - - it('should lowercase input', () => { - const result = slugify('FOO'); - expect(result).toEqual('foo'); - }); - - it('should convert input to kebab case by default', () => { - const result = slugify('foo bar baz'); - expect(result).toEqual('foo-bar-baz'); - }); - - it('should convert input to snake case with underscore separator', () => { - const result = slugify('foo bar baz', { separator: '_' }); - expect(result).toEqual('foo_bar_baz'); - }); - - it('should strip out invalid characters', () => { - const result = slugify('~ ` ! @ # $ % ^ & * ( ) - = + [ { } ] \' \\ | / > < . , ; : "'); - expect(result).toEqual(''); - }); - - it('should convert inferred words to kebab case', () => { - const result = slugify('MyCoolApp'); - expect(result).toEqual('my-cool-app'); - }); - - it('should trim whitespace', () => { - const result = slugify(' foo '); - expect(result).toEqual('foo'); - }); - - it('should strip out apostrophes', () => { - const result = slugify(' foo\'s bar '); - expect(result).toEqual('foos-bar'); - }); - - it('should convert ÀÁÂÃÄÅ and àáâãäå characters', () => { - const result = slugify('ÀÁÂÃÄÅ àáâãäå'); - expect(result).toEqual('a'.repeat(6) + '-' + 'a'.repeat(6)); - }); - - it('should convert Ææ characters', () => { - const result = slugify('Ææ'); - expect(result).toEqual('ae'.repeat(2)); - }); - - it('should convert Çç characters', () => { - const result = slugify('Çç'); - expect(result).toEqual('cc'); - }); - - it('should convert ÈÉÊË and èéêë characters', () => { - const result = slugify('ÈÉÊË èéêë'); - expect(result).toEqual('e'.repeat(4) + '-' + 'e'.repeat(4)); - }); - - it('should convert ÌÍÎÏ and ìíîï characters', () => { - const result = slugify('ÌÍÎÏ ìíîï'); - expect(result).toEqual('i'.repeat(4) + '-' + 'i'.repeat(4)); - }); - - it('should convert Ññ characters', () => { - const result = slugify('Ññ'); - expect(result).toEqual('n'.repeat(2)); - }); - - it('should convert ÒÓÔÕÖØ and òóôõöø characters', () => { - const result = slugify('ÒÓÔÕÖØ òóôõöø'); - expect(result).toEqual('o'.repeat(6) + '-' + 'o'.repeat(6)); - }); - - it('should convert ÙÚÛÜ, ùúûü characters', () => { - const result = slugify('ÙÚÛÜ ùúûü'); - expect(result).toEqual('u'.repeat(4) + '-' + 'u'.repeat(4)); - }); - - it('should convert Þþ characters', () => { - const result = slugify('Þþ'); - expect(result).toEqual('th'.repeat(2)); - }); - - it('should convert Ýýÿ characters', () => { - const result = slugify('Ýýÿ'); - expect(result).toEqual('y'.repeat(3)); - }); - - it('should convert ß characters', () => { - const result = slugify('ß'); - expect(result).toEqual('ss'); - }); - - }); - - }); - -}); diff --git a/packages/@ionic/cli-framework/src/utils/fn.ts b/packages/@ionic/cli-framework/src/utils/fn.ts deleted file mode 100644 index e3fb654a6d..0000000000 --- a/packages/@ionic/cli-framework/src/utils/fn.ts +++ /dev/null @@ -1,19 +0,0 @@ -export async function resolveValue(...fns: (() => Promise)[]): Promise { - for (const fn of fns) { - const result = await fn(); - - if (typeof result !== 'undefined') { - return result; - } - } -} - -export function resolveValueSync(...fns: (() => T | undefined)[]): T | undefined { - for (const fn of fns) { - const result = fn(); - - if (typeof result !== 'undefined') { - return result; - } - } -} diff --git a/packages/@ionic/cli-framework/src/utils/ipc.ts b/packages/@ionic/cli-framework/src/utils/ipc.ts deleted file mode 100644 index 97102babc6..0000000000 --- a/packages/@ionic/cli-framework/src/utils/ipc.ts +++ /dev/null @@ -1,195 +0,0 @@ -import { fork } from '@ionic/utils-subprocess'; -import { ChildProcess } from 'child_process'; -import { debug as Debug } from 'debug'; -import * as fs from 'fs'; - -import { ERROR_IPC_UNKNOWN_PROCEDURE, IPCError } from '../errors'; - -const debug = Debug('ionic:cli-framework:utils:ipc'); - -export interface RPCRequest

{ - type: 'rpc-request'; - id: string; - procedure: P; - args: A; -} - -export interface RPCResponse, D extends object> { - type: 'rpc-response'; - id: string; - procedure: R['procedure']; - request: R; - err?: any; - data: D; -} - -export type RPC

= RPCResponse, D>; - -export interface RPCProcessOptions { - readonly name?: string; - readonly timeout?: number; -} - -export class RPCProcess { - readonly name: string; - readonly timeout: number; - protected responseProcedures: Map Promise> = new Map(); - protected proc?: ChildProcess; - - constructor({ name = 'unnamed', timeout = 5000 }: RPCProcessOptions = {}) { - this.name = name; - this.timeout = timeout; - } - - start(proc: ChildProcess | NodeJS.Process): void { - if (this.proc) { - throw new IPCError('RPC process already started.'); - } - - const p = proc as ChildProcess; - - if (!p.send) { - throw new IPCError('Cannot use proc: `send()` undefined.'); - } - - this.proc = p; - - p.on('message', async (msg: any) => { - if (isRPCRequest(msg)) { - debug('%s: Received RPC request: %O', this.name, msg); - const fn = this.responseProcedures.get(msg.procedure); - let err: any; - let data: any; - - if (fn) { - try { - data = await fn(msg.args); - } catch (e: any) { - err = e; - } - } else { - err = new IPCError(`Unknown procedure: ${msg.procedure}`); - err.code = ERROR_IPC_UNKNOWN_PROCEDURE; - } - - const response: RPCResponse = { type: 'rpc-response', id: msg.id, procedure: msg.procedure, request: msg, err, data }; - - if (p.send) { - p.send(response); - debug('%s: Sent RPC response: %O', this.name, response); - } else { - throw new IPCError('Cannot use proc: `send()` undefined.'); - } - } - }); - - p.on('error', err => { - debug('%s: Encountered error with proc: %O', this.name, err); - }); - - debug('%s: RPC process initiated (pid: %d)', this.name, p.pid); - } - - register>(procedure: R['procedure'], fn: (args: R['request']['args']) => Promise): void { - this.responseProcedures.set(procedure, fn); - } - - async call>(procedure: R['procedure'], args: R['request']['args']): Promise { - const p = this.proc; - - if (!p) { - throw new IPCError('Cannot call procedure: no proc started.'); - } - - const id = Math.random().toString(16).substring(2, 8); - const request: RPCRequest = { type: 'rpc-request', id, procedure, args }; - - return new Promise((resolve, reject) => { - const timer = setTimeout(() => { - reject(new IPCError(`Timeout of ${this.timeout}ms reached.`)); - }, this.timeout); - - const messageHandler = (msg: any) => { - if (isRPCResponse(msg) && msg.id === id) { - debug('%s: Received RPC response: %O', this.name, msg); - if (msg.err) { - reject(msg.err); - } else { - resolve(msg.data); - } - - p.removeListener('message', messageHandler); - p.removeListener('disconnect', disconnectHandler); - clearTimeout(timer); - } - }; - - const disconnectHandler = () => { - reject(new IPCError('Unexpected disconnect. Rejecting call!')); - clearTimeout(timer); - }; - - p.on('message', messageHandler); - p.on('disconnect', disconnectHandler); - - if (p.send) { - p.send(request); - debug('%s: Sent RPC request: %O', this.name, request); - } else { - reject(new IPCError('Cannot use proc: `send()` undefined.')); - clearTimeout(timer); - } - }); - } - - end(): void { - if (!this.proc) { - throw new IPCError(`RPC process not started.`); - } - - this.proc.disconnect(); - debug('%s: Disconnected', this.name); - } -} - -export class RPCHost { - protected rpc: RPCProcess; - - constructor(readonly modulePath: string, readonly args: readonly string[]) { - this.rpc = new RPCProcess({ name: 'host' }); - } - - start(): void { - try { - fs.accessSync(this.modulePath, fs.constants.R_OK); - } catch (e: any) { - debug('Error during access check: %O', e); - throw new IPCError(`Module not accessible: ${this.modulePath}`); - } - - const p = fork(this.modulePath, this.args, { stdio: ['ignore', 'ignore', 'ignore', 'ipc'] }); - debug('RPC subprocess forked %o', [this.modulePath, ...this.args]); - - this.rpc.start(p); - } - - register>(procedure: R['procedure'], fn: (args: R['request']['args']) => Promise): void { - this.rpc.register(procedure, fn); - } - - async call>(procedure: R['procedure'], args: R['request']['args']): Promise { - return this.rpc.call(procedure, args); - } - - end(): void { - this.rpc.end(); - } -} - -function isRPCRequest(msg: any): msg is RPCRequest { - return msg && msg.type === 'rpc-request' && typeof msg.procedure === 'string'; -} - -function isRPCResponse(msg: any): msg is RPCResponse { - return msg && msg.type === 'rpc-response' && typeof msg.procedure === 'string'; -} diff --git a/packages/@ionic/cli-framework/src/utils/node.ts b/packages/@ionic/cli-framework/src/utils/node.ts deleted file mode 100644 index f0882a79bb..0000000000 --- a/packages/@ionic/cli-framework/src/utils/node.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { compilePaths, readJson } from '@ionic/utils-fs'; -import * as fs from 'fs'; -import * as path from 'path'; - -import { PackageJson } from '../definitions'; -import { isPackageJson } from '../guards'; - -export const ERROR_INVALID_PACKAGE_JSON = 'INVALID_PACKAGE_JSON'; -export const ERROR_BIN_NOT_FOUND = 'BIN_NOT_FOUND'; - -/** - * Lightweight version of https://github.com/npm/validate-npm-package-name - */ -export function isValidPackageName(name: string): boolean { - return encodeURIComponent(name) === name; -} - -export async function readPackageJsonFile(p: string): Promise { - const packageJson = await readJson(p); - - if (!isPackageJson(packageJson)) { - throw ERROR_INVALID_PACKAGE_JSON; - } - - return packageJson; -} - -export function compileNodeModulesPaths(filePath: string): string[] { - return compilePaths(filePath).map(f => path.join(f, 'node_modules')); -} - -export interface ResolveOptions { - paths?: string[]; -} - -export function resolveBin(m: string, bin: string, options?: ResolveOptions): string { - const packageJsonPath = require.resolve(`${m}/package`, options); - const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, { encoding: 'utf8' })); - - if (!isPackageJson(packageJson) || !packageJson.bin) { - throw ERROR_INVALID_PACKAGE_JSON; - } - - const desiredBin = packageJson.bin[bin]; - - if (!desiredBin) { - throw ERROR_BIN_NOT_FOUND; - } - - return path.resolve(path.dirname(packageJsonPath), desiredBin); -} diff --git a/packages/@ionic/cli-framework/src/utils/promise.ts b/packages/@ionic/cli-framework/src/utils/promise.ts deleted file mode 100644 index fffbaea1c7..0000000000 --- a/packages/@ionic/cli-framework/src/utils/promise.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { EventEmitter } from 'events'; - -export interface Promisify { - (func: (callback: (err: any, result?: T) => void) => void): () => Promise; - (func: (arg1: A1, callback: (err: any, result?: T) => void) => void): (arg1: A1) => Promise; - (func: (arg1: A1, arg2: A2, callback: (err: any, result?: T) => void) => void): (arg1: A1, arg2: A2) => Promise; - (func: (arg1: A1, arg2: A2, arg3: A3, callback: (err: any, result?: T) => void) => void): (arg1: A1, arg2: A2, arg3: A3) => Promise; - (func: (arg1: A1, arg2: A2, arg3: A3, arg4: A4, callback: (err: any, result?: T) => void) => void): (arg1: A1, arg2: A2, arg3: A3, arg4: A4) => Promise; - (func: (arg1: A1, arg2: A2, arg3: A3, arg4: A4, arg5: A5, callback: (err: any, result?: T) => void) => void): (arg1: A1, arg2: A2, arg3: A3, arg4: A4, arg5: A5) => Promise; -} - -export const promisify: Promisify = (func: any) => { - return (...args: any[]) => { - return new Promise((resolve, reject) => { - func(...args, (err: any, response: any) => { - if (err) { - return reject(err); - } - - resolve(response); - }); - }); - }; -}; - -export const promisifyEvent = (emitter: EventEmitter, event: string | symbol): Promise => { - return new Promise((resolve, reject) => { - emitter.once(event, (value: any) => { - resolve(value); - }); - - emitter.once('error', (err: Error) => { - reject(err); - }); - }); -}; - -export namespace PromiseUtil { - export function some(promises: Promise[], expected = 1): Promise { - if (promises.length === expected) { - return Promise.all(promises); - } - - return new Promise((resolve, reject) => { - const values: any[] = []; - - const resolveOne = (value: any) => { - if (expected-- > 0) { - values.push(value); - } else { - resolve(values); - } - }; - - const rejectOne = (err: any) => { - reject(err); - }; - - for (const promise of promises) { - promise.then(resolveOne, rejectOne); - } - }); - } - - export async function any(promises: Promise[]): Promise { - const [ first ] = await some(promises, 1); - return first; - } -} diff --git a/packages/@ionic/cli-framework/src/utils/string.ts b/packages/@ionic/cli-framework/src/utils/string.ts deleted file mode 100644 index d6915f4f9f..0000000000 --- a/packages/@ionic/cli-framework/src/utils/string.ts +++ /dev/null @@ -1,64 +0,0 @@ -import * as lodash from 'lodash'; - -export const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; -export const URL_REGEX = /^[\S]+:[\S]+$/; - -export function isValidEmail(email?: any): boolean { - if (typeof email !== 'string') { - return false; - } - - return EMAIL_REGEX.test(email); -} - -export function isValidURL(url?: any): boolean { - if (typeof url !== 'string') { - return false; - } - - return URL_REGEX.test(url); -} - -export function strcmp(a: string | undefined, b: string | undefined): number { - if (!a) { - a = ''; - } - - if (!b) { - b = ''; - } - - return +(a > b) || +(a === b) - 1; -} - -export function str2num(value: any, defaultValue = -1): number { - if (typeof value === 'number') { - return value; - } - - if (typeof value !== 'string') { - return defaultValue; - } - - const result = parseInt(value, 10); - - if (Number.isNaN(result)) { - return defaultValue; - } - - return result; -} - -export interface SlugifyOptions { - separator?: string; -} - -/** - * Create a slug out of an input string. - * - * Whitespace is trimmed, everything is lowercased, some international - * characters are converted, then dasherized. - */ -export function slugify(input: string, { separator = '-' }: SlugifyOptions = {}): string { - return lodash.words(lodash.deburr(input.trim())).map(w => w.toLowerCase().replace(/[^A-z0-9]/g, '')).join(separator); -} diff --git a/packages/@ionic/cli-framework/tsconfig.json b/packages/@ionic/cli-framework/tsconfig.json deleted file mode 100644 index 169cd716a1..0000000000 --- a/packages/@ionic/cli-framework/tsconfig.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./", - "types": [ - "node" - ] - }, - "include": [ - "../../../types/stream-combiner2.d.ts", - "src/**/*.ts" - ], - "exclude": [ - "node_modules", - "src/**/__tests__/*.ts" - ] -} diff --git a/packages/@ionic/cli/.gitignore b/packages/@ionic/cli/.gitignore deleted file mode 100644 index d2e88252f6..0000000000 --- a/packages/@ionic/cli/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -*.js -!tslint.js -!jest.config.js -!lint-staged.config.js -*.d.ts -*.tgz diff --git a/packages/@ionic/cli/.npmignore b/packages/@ionic/cli/.npmignore deleted file mode 100644 index 6b64f662ef..0000000000 --- a/packages/@ionic/cli/.npmignore +++ /dev/null @@ -1,5 +0,0 @@ -src -jest.config.js -lint-staged.config.js -tsconfig.json -tslint.json diff --git a/packages/@ionic/cli/.npmrc b/packages/@ionic/cli/.npmrc deleted file mode 100644 index 43c97e719a..0000000000 --- a/packages/@ionic/cli/.npmrc +++ /dev/null @@ -1 +0,0 @@ -package-lock=false diff --git a/packages/@ionic/cli/CHANGELOG.md b/packages/@ionic/cli/CHANGELOG.md deleted file mode 100644 index 26f61eb5f2..0000000000 --- a/packages/@ionic/cli/CHANGELOG.md +++ /dev/null @@ -1,1905 +0,0 @@ -# Change Log - -All notable changes to this project will be documented in this file. -See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -## [7.2.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@7.2.0...@ionic/cli@7.2.1) (2025-03-18) - - -### Bug Fixes - -* **angular:** change default project type to Standalone ([#5104](https://github.com/ionic-team/ionic-cli/issues/5104)) ([cea5728](https://github.com/ionic-team/ionic-cli/commit/cea5728c8b228ab336a98bee5655364e720336a4)) - - - - - -# [7.2.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@7.1.6...@ionic/cli@7.2.0) (2024-01-02) - - -### Bug Fixes - -* live reload deploys app when warnings are present [#4807](https://github.com/ionic-team/ionic-cli/issues/4807) ([#5043](https://github.com/ionic-team/ionic-cli/issues/5043)) ([ef5706e](https://github.com/ionic-team/ionic-cli/commit/ef5706ed5e7ef347280320a88b5604a279667184)) - - -### Features - -* **angular:** support angulars vite dev server ([#5064](https://github.com/ionic-team/ionic-cli/issues/5064)) ([8a94434](https://github.com/ionic-team/ionic-cli/commit/8a94434a413c1b60b0e88904064edfe7a98206ab)) - - - - - -## [7.1.6](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@7.1.5...@ionic/cli@7.1.6) (2023-12-19) - - -### Bug Fixes - -* **cli:** resolve vm2 security vulnerability ([#5070](https://github.com/ionic-team/ionic-cli/issues/5070)) ([4050419](https://github.com/ionic-team/ionic-cli/commit/4050419bef70fb92e58b0a83cd4b68b48090e596)) - - - - - -## [7.1.5](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@7.1.4...@ionic/cli@7.1.5) (2023-11-08) - - -### Bug Fixes - -* use native ES2022 error cause ([#5010](https://github.com/ionic-team/ionic-cli/issues/5010)) ([d19f28e](https://github.com/ionic-team/ionic-cli/commit/d19f28e3f41b8f0248c11c49b7006754bb315218)) - - - - - -## [7.1.4](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@7.1.3...@ionic/cli@7.1.4) (2023-11-08) - - -### Reverts - -* use native ES2022 error cause ([#5060](https://github.com/ionic-team/ionic-cli/issues/5060)) ([1e64a1a](https://github.com/ionic-team/ionic-cli/commit/1e64a1ada60545adf8e7c99fbd1f8766cf2416f9)) - - - - - -## [7.1.3](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@7.1.2...@ionic/cli@7.1.3) (2023-11-07) - -**Note:** Version bump only for package @ionic/cli - - - - - -## [7.1.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@7.1.1...@ionic/cli@7.1.2) (2023-11-07) - - -### Bug Fixes - -* use native ES2022 error cause ([#5010](https://github.com/ionic-team/ionic-cli/issues/5010)) ([0c4cd0f](https://github.com/ionic-team/ionic-cli/commit/0c4cd0f47e00b43e8c0ce4eef072351a846b566c)) - - - - - -## [7.1.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@7.1.0...@ionic/cli@7.1.1) (2023-05-02) - -**Note:** Version bump only for package @ionic/cli - - - - - -# [7.1.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@7.0.1...@ionic/cli@7.1.0) (2023-05-01) - - -### Features - -* add standalone as an option ([#5005](https://github.com/ionic-team/ionic-cli/issues/5005)) ([707a42d](https://github.com/ionic-team/ionic-cli/commit/707a42d29579a355d9a57b65c07ec6a0b681028e)) - - - - - -## [7.0.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@7.0.0...@ionic/cli@7.0.1) (2023-04-05) - - -### Bug Fixes - -* state should not be required on OAuth Response ([#4995](https://github.com/ionic-team/ionic-cli/issues/4995)) ([c053cb3](https://github.com/ionic-team/ionic-cli/commit/c053cb3394acec5402d2ea436796775d67c98d39)) -* **cli:** ionic info returns package information ([#4991](https://github.com/ionic-team/ionic-cli/issues/4991)) ([2dd9136](https://github.com/ionic-team/ionic-cli/commit/2dd9136bb85375b03883105d04d1c1bb090d884d)), closes [#4992](https://github.com/ionic-team/ionic-cli/issues/4992) - - - - - -# [7.0.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.20.9...@ionic/cli@7.0.0) (2023-03-29) - - -### Bug Fixes - -* update test to reflect new output ([#4967](https://github.com/ionic-team/ionic-cli/issues/4967)) ([5c23518](https://github.com/ionic-team/ionic-cli/commit/5c23518b1a7bda4f020a82ced5557e157ccdbbc7)) - - -### chore - -* **appflow:** remove appflow from @ionic/cli ([#4777](https://github.com/ionic-team/ionic-cli/issues/4777)) ([34b07ad](https://github.com/ionic-team/ionic-cli/commit/34b07ad4b53130057ecc5a736036a87d582744c2)) - - -### Code Refactoring - -* **docs:** remove ionic docs command ([#4969](https://github.com/ionic-team/ionic-cli/issues/4969)) ([e624258](https://github.com/ionic-team/ionic-cli/commit/e62425889d07b496c0e578b61413ab00a8dae8d3)) -* **doctor:** remove ionic doctor command ([#4959](https://github.com/ionic-team/ionic-cli/issues/4959)) ([13568b1](https://github.com/ionic-team/ionic-cli/commit/13568b138bb8007e91901820656e97ac121a4913)) -* **lab:** remove ionic serve --lab option ([#4960](https://github.com/ionic-team/ionic-cli/issues/4960)) ([d6512a5](https://github.com/ionic-team/ionic-cli/commit/d6512a5139943dbe2084f99e63075db6e6b15b4d)) - - -### Features - -* add vite support for react and vue ([#4966](https://github.com/ionic-team/ionic-cli/issues/4966)) ([94e9fa2](https://github.com/ionic-team/ionic-cli/commit/94e9fa2a2146883ad857d2acad71b7605e3687d0)) -* remove ionic-angular project type ([ff1c936](https://github.com/ionic-team/ionic-cli/commit/ff1c93635a74fc26e57319123118eaf3ce6d9958)) -* remove ionicv1 project type ([6ea2d48](https://github.com/ionic-team/ionic-cli/commit/6ea2d48da10d2b964ab95961cc9600874279c59d)) -* remove v3 and v1 integrations ([d40da3d](https://github.com/ionic-team/ionic-cli/commit/d40da3d07da909f82807fe7182c035c69d20a65c)) -* use vite specific projects for starters ([#4970](https://github.com/ionic-team/ionic-cli/issues/4970)) ([f35ea72](https://github.com/ionic-team/ionic-cli/commit/f35ea7239d5b23f819084691480f1c791377d55f)) - - -### BREAKING CHANGES - -* **appflow:** The `package` commands and the `deploy build` command have been removed. Developers should migrate to the Ionic Cloud CLI instead. -- Using `deploy build`? Migrate to ionic-cloud build web and if you used the -channel option, also use ionic-cloud deploy web. -- Using `package` commands? Migrate to ionic-cloud build and ionic-cloud deploy. - -The remaining `deploy` commands have been renamed to `live-update`. - -| deploy command | live-update command | -| - | - | -| `ionic deploy add` | `ionic live-update add` | -| `ionic deploy configure` | `ionic live-update configure` | -| `ionic deploy manifest` | `ionic live-update manifest` | - -See https://ionic.io/docs/appflow/cli/overview for more information on the Ionic Cloud CLI. -* **docs:** The `ionic docs` command has been removed. See https://github.com/ionic-team/ionic-cli/pull/4905 for an alternative. -* **doctor:** The `ionic doctor` command has been removed. See https://github.com/ionic-team/ionic-cli/pull/4907 for alternatives. -* **lab:** The `--lab` option has been removed from `ionic serve`. See https://ionicframework.com/docs/developing/previewing for alternatives. -* This commit removes integrations with V1 and V3 Ionic -project types -* This commit removes the Ionic V1 project type from the -available project integrations. Please migrate to a modern Ionic -project. -* Integration with V3 (aka ionic-angular) project has -been removed. Please update to modern Ionic projects. - - - - - -## [6.20.9](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.20.8...@ionic/cli@6.20.9) (2023-03-17) - - -### Bug Fixes - -* **cli:** native solution preview should no longer be hidden ([#4962](https://github.com/ionic-team/ionic-cli/issues/4962)) ([a2eb4b2](https://github.com/ionic-team/ionic-cli/commit/a2eb4b2cf46e0f2004c1850acfdeada46fb211b5)) - - - - - -## [6.20.9](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.20.8...@ionic/cli@6.20.9) (2023-03-17) - - -### Bug Fixes - -* **cli:** native solution preview should no longer be hidden ([#4962](https://github.com/ionic-team/ionic-cli/issues/4962)) ([a2eb4b2](https://github.com/ionic-team/ionic-cli/commit/a2eb4b2cf46e0f2004c1850acfdeada46fb211b5)) - - - - - -## [6.20.8](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.20.7...@ionic/cli@6.20.8) (2023-01-13) - -**Note:** Version bump only for package @ionic/cli - - - - - -## [6.20.7](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.20.7...@ionic/cli@6.20.7) (2023-01-13) - -**Note:** Version bump only for package @ionic/cli - - - - - -## [6.20.7](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.20.6...@ionic/cli@6.20.7) (2023-01-13) - - -### Bug Fixes - -* **capacitor:** forward --inline to Capacitor CLI on copy and sync ([#4928](https://github.com/ionic-team/ionic-cli/issues/4928)) ([f5f75fa](https://github.com/ionic-team/ionic-cli/commit/f5f75fa27cffee37571813a2827cfc02baf0cb9a)) -* **cli:** Deprecate doctor and lab commands ([#4945](https://github.com/ionic-team/ionic-cli/issues/4945)) ([6fcf882](https://github.com/ionic-team/ionic-cli/commit/6fcf882b9ee9c31fcea38a6390b3c181c4df5ca6)) -* deploy manifest error if no capacitor CLI ([#4940](https://github.com/ionic-team/ionic-cli/issues/4940)) ([f586a18](https://github.com/ionic-team/ionic-cli/commit/f586a18805ead288850f18d8981db4f0ad8848ed)) - - - - - -## [6.20.6](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.20.5...@ionic/cli@6.20.6) (2022-12-20) - - -### Bug Fixes - -* **cli:** capacitor recognize multi-project ([#4937](https://github.com/ionic-team/ionic-cli/issues/4937)) ([553ea79](https://github.com/ionic-team/ionic-cli/commit/553ea794f916f17db131d359caba9b6c1b31b919)) - - - - - -## [6.20.5](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.20.4...@ionic/cli@6.20.5) (2022-12-13) - -**Note:** Version bump only for package @ionic/cli - - - - - -## [6.20.4](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.20.3...@ionic/cli@6.20.4) (2022-11-15) - - -### Bug Fixes - -* **capacitor:** Error if no emulators or devices are found on run ([#4931](https://github.com/ionic-team/ionic-cli/issues/4931)) ([7ec24c0](https://github.com/ionic-team/ionic-cli/commit/7ec24c04043b760ff9940d24c9ac281dce3aa21f)) - - - - - -## [6.20.3](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.20.2...@ionic/cli@6.20.3) (2022-10-06) - - -### Bug Fixes - -* **cli:** capacitor commands not working ([#4918](https://github.com/ionic-team/ionic-cli/issues/4918)) ([27b958b](https://github.com/ionic-team/ionic-cli/commit/27b958bdf22c37f962d705a1b8ba1fee78b59c42)) - - - - - -## [6.20.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.20.1...@ionic/cli@6.20.2) (2022-09-29) - - -### Bug Fixes - -* **cli:** capacitor config not being read from multi-project directories ([#4909](https://github.com/ionic-team/ionic-cli/issues/4909)) ([0ab4881](https://github.com/ionic-team/ionic-cli/commit/0ab4881736f0d3b967d12802cb9cd01ba24d2088)) - - - - - -## [6.20.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.20.0...@ionic/cli@6.20.1) (2022-06-21) - -**Note:** Version bump only for package @ionic/cli - - - - - -# [6.20.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.19.1...@ionic/cli@6.20.0) (2022-06-16) - - -### Bug Fixes - -* **integrations:** Move ng add @ionic/cordova-builders after package installs ([#4853](https://github.com/ionic-team/ionic-cli/issues/4853)) ([8a69ae0](https://github.com/ionic-team/ionic-cli/commit/8a69ae0fd7d7fc15a0d1d5107199299117d34af4)) - - -### Features - -* **capacitor:** enable NSAppTransportSecurity settings for live reload if needed ([#4851](https://github.com/ionic-team/ionic-cli/issues/4851)) ([24e22a6](https://github.com/ionic-team/ionic-cli/commit/24e22a60d17709ff78563a796db01e69b6d641cb)) -* **cli:** allow cap electron commands ([#4854](https://github.com/ionic-team/ionic-cli/issues/4854)) ([46da22f](https://github.com/ionic-team/ionic-cli/commit/46da22ff5b89c66155829a307acd765c5ff957d0)) - - - - - -## [6.19.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.19.0...@ionic/cli@6.19.1) (2022-05-09) - - -### Bug Fixes - -* **capacitor:** Match platform version with CLI version ([#4829](https://github.com/ionic-team/ionic-cli/issues/4829)) ([9846d46](https://github.com/ionic-team/ionic-cli/commit/9846d467e926e76232826175d3d0d50c00b1ef1f)) -* **vue:** pass extra args to vue-cli serve ([#4827](https://github.com/ionic-team/ionic-cli/issues/4827)) ([87a0dcd](https://github.com/ionic-team/ionic-cli/commit/87a0dcdfd9f75b91e83c0eb13761d6135b226212)) - - - - - -# [6.19.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.18.2...@ionic/cli@6.19.0) (2022-03-15) - - -### Features - -* **integrations:** add ng add cordova-builder ([#4823](https://github.com/ionic-team/ionic-cli/issues/4823)) ([de8d028](https://github.com/ionic-team/ionic-cli/commit/de8d028f54199ddd59aed6636378e4829875a0cd)) - - - - - -## [6.18.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.18.1...@ionic/cli@6.18.2) (2022-03-04) - - -### Bug Fixes - -* Pin @types/superagent version to avoid build problems ([#4802](https://github.com/ionic-team/ionic-cli/issues/4802)) ([bdf5b05](https://github.com/ionic-team/ionic-cli/commit/bdf5b05833b92471f4ac5ba24437a8131adbf2e1)) - - - - - -## [6.18.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.18.0...@ionic/cli@6.18.1) (2021-11-10) - - -### Bug Fixes - -* Update superagent-proxy dependency to v3 ([#4779](https://github.com/ionic-team/ionic-cli/issues/4779)) ([f153355](https://github.com/ionic-team/ionic-cli/commit/f153355324a958389f8414d72caa41035ec8bd98)) - - - - - -# [6.18.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.17.1...@ionic/cli@6.18.0) (2021-10-25) - - -### Bug Fixes - -* **vue:** pass extra args to vue-cli ([56dd8a7](https://github.com/ionic-team/ionic-cli/commit/56dd8a7fe94d764bac63a41d35e0b709a2dde893)), closes [#4669](https://github.com/ionic-team/ionic-cli/issues/4669) [#4642](https://github.com/ionic-team/ionic-cli/issues/4642) [#4642](https://github.com/ionic-team/ionic-cli/issues/4642) - - -### Features - -* **appflow:** deprecate appflow functionality in @ionic/cli ([#4776](https://github.com/ionic-team/ionic-cli/issues/4776)) ([6a39641](https://github.com/ionic-team/ionic-cli/commit/6a39641555c9247cc53091db5afde73f9b1d3e34)) - - - - - -## [6.17.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.17.0...@ionic/cli@6.17.1) (2021-09-03) - - -### Bug Fixes - -* support Capacitor 1 apps ([#4758](https://github.com/ionic-team/ionic-cli/issues/4758)) ([0ca282b](https://github.com/ionic-team/ionic-cli/commit/0ca282b615e52867b1cbf263d08472878083f34f)) - - - - - -# [6.17.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.16.3...@ionic/cli@6.17.0) (2021-08-11) - - -### Bug Fixes - -* numeric app names dont crash adding capacitor ([#4709](https://github.com/ionic-team/ionic-cli/issues/4709)) ([e13a4c5](https://github.com/ionic-team/ionic-cli/commit/e13a4c5ebccc79a85334968854688df9ada4b150)) -* Remove serve options from cap run command ([#4736](https://github.com/ionic-team/ionic-cli/issues/4736)) ([75e635b](https://github.com/ionic-team/ionic-cli/commit/75e635b6431d186a79ab5ad685ca10798120e38f)) - - -### Features - -* Add web based GUI start wizard to ionic start command ([#4746](https://github.com/ionic-team/ionic-cli/issues/4746)) ([c0669a9](https://github.com/ionic-team/ionic-cli/commit/c0669a9d48b3728c485aa19a2d55fedbe582d121)) -* Cap is no longer experimental and is default for Angular ([#4745](https://github.com/ionic-team/ionic-cli/issues/4745)) ([d81e08b](https://github.com/ionic-team/ionic-cli/commit/d81e08bee0e2495c97b984dcc69f95c00ade31ab)) - - - - - -## [6.16.3](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.16.2...@ionic/cli@6.16.3) (2021-06-08) - - -### Bug Fixes - -* revert the change to update the lock file when the --no-deps flag is used and issue a warning instead ([#4713](https://github.com/ionic-team/ionic-cli/issues/4713)) ([96ba8b4](https://github.com/ionic-team/ionic-cli/commit/96ba8b40a24a8ba4e4bcfb84aa972e77220fb3d4)) - - - - - -## [6.16.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.16.1...@ionic/cli@6.16.2) (2021-06-03) - - -### Bug Fixes - -* Add status-bar to plugins installed on capacitor integration ([#4701](https://github.com/ionic-team/ionic-cli/issues/4701)) ([7c49d5c](https://github.com/ionic-team/ionic-cli/commit/7c49d5cdd717a05eff2e2f2d1fc5f209fd450f43)) -* updated `ionic start` to update lock files when the --no-deps arg is passed ([#4706](https://github.com/ionic-team/ionic-cli/issues/4706)) ([868e61d](https://github.com/ionic-team/ionic-cli/commit/868e61d70c54023e39896b677ceac331b0606167)) - - - - - -## [6.16.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.16.0...@ionic/cli@6.16.1) (2021-05-19) - - -### Bug Fixes - -* **capacitor:** Make add use latest instead of next ([#4700](https://github.com/ionic-team/ionic-cli/issues/4700)) ([04be661](https://github.com/ionic-team/ionic-cli/commit/04be661d9ec48960428aa65987c21a5f2bd6951b)) - - - - - -# [6.16.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.15.0...@ionic/cli@6.16.0) (2021-05-19) - - -### Bug Fixes - -* **capacitor:** remove extra logging ([6cebf63](https://github.com/ionic-team/ionic-cli/commit/6cebf631dc3ab2b683da2a9a8fd24787504b88ae)) - - -### Features - -* **capacitor:** capacitor 3 support ([#4610](https://github.com/ionic-team/ionic-cli/issues/4610)) ([359cdec](https://github.com/ionic-team/ionic-cli/commit/359cdec031f3c3484093e9fee65e06ef866cfd8b)), closes [#4690](https://github.com/ionic-team/ionic-cli/issues/4690) - - - - - -# [6.15.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.14.1...@ionic/cli@6.15.0) (2021-05-11) - - -### Features - -* **link:** Link bug fix and git selection changes ([#4689](https://github.com/ionic-team/ionic-cli/issues/4689)) ([942e965](https://github.com/ionic-team/ionic-cli/commit/942e96576f17b9958d4fe0be680235bc22ecbd32)) - - - - - -## [6.14.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.14.0...@ionic/cli@6.14.1) (2021-04-30) - - -### Bug Fixes - -* **appflow:** allow personal accounts to install enterprise packages ([#4681](https://github.com/ionic-team/ionic-cli/issues/4681)) ([97b0ee5](https://github.com/ionic-team/ionic-cli/commit/97b0ee5fa4432f965eabd38089681045dd3a4d5c)) - - - - - -# [6.14.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.13.1...@ionic/cli@6.14.0) (2021-04-28) - - -### Features - -* **appflow:** add flag for skipping artifact download after builds succeed ([#4678](https://github.com/ionic-team/ionic-cli/issues/4678)) ([d87c6f2](https://github.com/ionic-team/ionic-cli/commit/d87c6f28a34c668349868f8214346ff07398c820)) - - - - - -## [6.13.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.13.0...@ionic/cli@6.13.1) (2021-02-22) - - -### Bug Fixes - -* **appflow:** fix deploy manifest for Capacitor 1.x apps ([#4648](https://github.com/ionic-team/ionic-cli/issues/4648)) ([77b02a3](https://github.com/ionic-team/ionic-cli/commit/77b02a39d08fb369c6b95b5f3b0c255370e9fc23)) - - - - - -# [6.13.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.12.4...@ionic/cli@6.13.0) (2021-02-18) - - -### Bug Fixes - -* **appflow:** Default should be all available not hard-coded by platform ([#4645](https://github.com/ionic-team/ionic-cli/issues/4645)) ([6e66109](https://github.com/ionic-team/ionic-cli/commit/6e66109d4993ddb24882bd5c5875a31b2fe65cff)) - - -### Features - -* **appflow:** support Capacitor only apps with `ionic deploy manifest` ([#4641](https://github.com/ionic-team/ionic-cli/issues/4641)) ([cee620b](https://github.com/ionic-team/ionic-cli/commit/cee620baff2515115393d882b88f6a99e3fefd9b)) -* **appflow:** Support downloading all artifact types from appflow builds ([cae11db](https://github.com/ionic-team/ionic-cli/commit/cae11dba3f1efbec753ae6e91c21ad79be169e38)) -* **appflow:** Support downloading WWW_ZIP after deploy build ([bce15d1](https://github.com/ionic-team/ionic-cli/commit/bce15d1b57a102938d4e6922773fe27af24dcca7)) - - - - - -## [6.12.4](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.12.3...@ionic/cli@6.12.4) (2021-01-27) - - -### Bug Fixes - -* **Appflow:** Fix issue with deploy add and deploy configure commands where sometimes necessary variables weren't correctly set. ([#4634](https://github.com/ionic-team/ionic-cli/issues/4634)) ([be6cf2e](https://github.com/ionic-team/ionic-cli/commit/be6cf2e05957b38c35dd692a3917c22c262e5e71)) - - - - - -## [6.12.3](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.12.2...@ionic/cli@6.12.3) (2020-12-10) - - -### Bug Fixes - -* **appflow:** allow download of packages built from branches with in the name. CT-434 ([#4622](https://github.com/ionic-team/ionic-cli/issues/4622)) ([0054fd5](https://github.com/ionic-team/ionic-cli/commit/0054fd51706c071ffad98bf04ee053266bc780d1)) - - - - - -## [6.12.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.12.1...@ionic/cli@6.12.2) (2020-11-17) - - -### Bug Fixes - -* **react:** fix stdin to unblock react-scripts>3.4.0 ([#4609](https://github.com/ionic-team/ionic-cli/issues/4609)) ([47631f5](https://github.com/ionic-team/ionic-cli/commit/47631f5a9331f3bc428933da68c82176221746c6)) - - - - - -## [6.12.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.12.0...@ionic/cli@6.12.1) (2020-10-29) - - -### Bug Fixes - -* **Appflow:** fixes bug when a build that gets cancelled on the dashboard is not recognized as a final state in the CLI ([#4598](https://github.com/ionic-team/ionic-cli/issues/4598)) ([51eab00](https://github.com/ionic-team/ionic-cli/commit/51eab00105a7616df869cd82eea54f1a55b7ab44)) - - - - - -# [6.12.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.11.12...@ionic/cli@6.12.0) (2020-10-15) - - -### Features - -* support vue projects ([#4515](https://github.com/ionic-team/ionic-cli/issues/4515)) ([ad6c483](https://github.com/ionic-team/ionic-cli/commit/ad6c483bd59b4f5e70acac22f456b0b062f4d39c)) - - - - - -## [6.11.12](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.11.11...@ionic/cli@6.11.12) (2020-10-12) - - -### Bug Fixes - -* **capacitor:** use correct root when checking platforms ([e243357](https://github.com/ionic-team/ionic-cli/commit/e2433573bae66210d7ef29d6a2c170faa8abe408)) -* **cordova:** load config.xml after serve/build hooks ([9867ada](https://github.com/ionic-team/ionic-cli/commit/9867adafbdd200712a35b38d39c54af6f809c987)) - - - - - -## [6.11.11](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.11.10...@ionic/cli@6.11.11) (2020-10-05) - - -### Bug Fixes - -* **Appflow:** fix bug where appflow build would fail to copy IPA/APK if tmp dir was mounted on a different filesystem ([#4578](https://github.com/ionic-team/ionic-cli/issues/4578)) ([65c740d](https://github.com/ionic-team/ionic-cli/commit/65c740d8ef3195e63d026ac229b7fe8bf1187da6)) - - - - - -## [6.11.10](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.11.9...@ionic/cli@6.11.10) (2020-09-29) - -**Note:** Version bump only for package @ionic/cli - - - - - -## [6.11.9](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.11.8...@ionic/cli@6.11.9) (2020-09-24) - - -### Bug Fixes - -* **cordova:** handle output-metadata.json files during android run ([5905cc1](https://github.com/ionic-team/ionic-cli/commit/5905cc1a9cd75b1a2bb8343da2fbc7146322ad28)) - - - - - -## [6.11.8](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.11.7...@ionic/cli@6.11.8) (2020-09-02) - -**Note:** Version bump only for package @ionic/cli - - - - - -## [6.11.7](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.11.6...@ionic/cli@6.11.7) (2020-08-29) - -**Note:** Version bump only for package @ionic/cli - - - - - -## [6.11.6](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.11.5...@ionic/cli@6.11.6) (2020-08-28) - -**Note:** Version bump only for package @ionic/cli - - - - - -## [6.11.5](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.11.4...@ionic/cli@6.11.5) (2020-08-27) - -**Note:** Version bump only for package @ionic/cli - - - - - -## [6.11.4](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.11.3...@ionic/cli@6.11.4) (2020-08-27) - -**Note:** Version bump only for package @ionic/cli - - - - - -## [6.11.3](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.11.2...@ionic/cli@6.11.3) (2020-08-26) - -**Note:** Version bump only for package @ionic/cli - - - - - -## [6.11.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.11.1...@ionic/cli@6.11.2) (2020-08-25) - -**Note:** Version bump only for package @ionic/cli - - - - - -## [6.11.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.11.0...@ionic/cli@6.11.1) (2020-08-20) - - -### Bug Fixes - -* **appflow:** fix an issue where a network connectivity issue could c… ([#4520](https://github.com/ionic-team/ionic-cli/issues/4520)) ([8634fde](https://github.com/ionic-team/ionic-cli/commit/8634fde68bdfa8f4af724e4a86bb285c44924f37)) - - - - - -# [6.11.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.10.2...@ionic/cli@6.11.0) (2020-07-29) - - -### Features - -* add `capacitor:sync:after` hook ([#4496](https://github.com/ionic-team/ionic-cli/issues/4496)) ([c63d4e1](https://github.com/ionic-team/ionic-cli/commit/c63d4e1957f36549142275c11f7198c68874cef1)) - - - - - -## [6.10.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.10.1...@ionic/cli@6.10.2) (2020-07-27) - - -### Bug Fixes - -* use inline uuid v4 function ([#4501](https://github.com/ionic-team/ionic-cli/issues/4501)) ([3743d7b](https://github.com/ionic-team/ionic-cli/commit/3743d7b3e01166dd70716558d55a500419a8bb13)) - - - - - -## [6.10.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.10.0...@ionic/cli@6.10.1) (2020-06-15) - - -### Bug Fixes - -* **cordova:** handle new format of output.json files for Android builds ([#4470](https://github.com/ionic-team/ionic-cli/issues/4470)) ([689b886](https://github.com/ionic-team/ionic-cli/commit/689b886a4a1395aa8bf3c6b0da21bde4a41e0de1)) -* use pnpm if configured in build/serve commands ([#4459](https://github.com/ionic-team/ionic-cli/issues/4459)) ([174985f](https://github.com/ionic-team/ionic-cli/commit/174985fca87322d4b52929ae44e93f639665bdf8)) - - - - - -# [6.10.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.9.3...@ionic/cli@6.10.0) (2020-06-02) - - -### Bug Fixes - -* **cordova:** platform is a required input ([1bd3087](https://github.com/ionic-team/ionic-cli/commit/1bd30874f46d89248193c959dd8d769dd99503cf)) - - -### Features - -* **login:** default to browser login ([#4451](https://github.com/ionic-team/ionic-cli/issues/4451)) ([0738abd](https://github.com/ionic-team/ionic-cli/commit/0738abd4123b6b5e078cd7ff2f76ed1a26474cf0)) - - - - - -## [6.9.3](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.9.2...@ionic/cli@6.9.3) (2020-05-27) - - -### Bug Fixes - -* **start:** fix issue with starting the react conf app ([daf1f72](https://github.com/ionic-team/ionic-cli/commit/daf1f72372ca3966c57c7e32adc38001cf3a5fdb)) - - - - - -## [6.9.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.9.1...@ionic/cli@6.9.2) (2020-05-17) - - -### Bug Fixes - -* **link:** fix GitHub OAuth URL on WSL ([#4442](https://github.com/ionic-team/ionic-cli/issues/4442)) ([93c5bf5](https://github.com/ionic-team/ionic-cli/commit/93c5bf5ba0cc0549a4bf61f534ee30d465bfe35e)) -* **package:** fix name of the package deploy name ([#4435](https://github.com/ionic-team/ionic-cli/issues/4435)) ([6f6f9d7](https://github.com/ionic-team/ionic-cli/commit/6f6f9d7540fc064b3ffcbc05b56441f9b5c2efeb)) -* **start:** do not prompt for project type when cloning ([#4427](https://github.com/ionic-team/ionic-cli/issues/4427)) ([4ff1622](https://github.com/ionic-team/ionic-cli/commit/4ff162281e327b4740e92513925b6d37b9dfb71e)) - - - - - -## [6.9.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.9.0...@ionic/cli@6.9.1) (2020-05-12) - - -### Bug Fixes - -* pin tslib to avoid "Cannot set property pathExists" error ([689e1f0](https://github.com/ionic-team/ionic-cli/commit/689e1f038b907356ef855a067a76d4822e7072a8)) - - - - - -# [6.9.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.8.0...@ionic/cli@6.9.0) (2020-05-11) - - -### Bug Fixes - -* **capacitor:** do not open browser with `ionic cap run` ([8e3908d](https://github.com/ionic-team/ionic-cli/commit/8e3908d35ba99289e64a268c8a747054afceed92)) - - -### Features - -* add `capacitor:run:before` and `capacitor:build:before` hooks ([#4417](https://github.com/ionic-team/ionic-cli/issues/4417)) ([28d8540](https://github.com/ionic-team/ionic-cli/commit/28d854041d418fdd4c36233eb47af177506fdbaf)) - - - - - -# [6.8.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.7.0...@ionic/cli@6.8.0) (2020-05-06) - - -### Features - -* **package:** deployments to destinations (e.g. app stores) ([#4418](https://github.com/ionic-team/ionic-cli/issues/4418)) ([f0314f6](https://github.com/ionic-team/ionic-cli/commit/f0314f64a0a992c6bba9bb46343c1679b8b6521e)) - - - - - -# [6.7.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.6.0...@ionic/cli@6.7.0) (2020-04-29) - - -### Features - -* **capacitor:** add `--no-open` option for run and build ([#4403](https://github.com/ionic-team/ionic-cli/issues/4403)) ([977811e](https://github.com/ionic-team/ionic-cli/commit/977811e652f96aa4f5b4de92a80d9c25ae693d15)) - - - - - -# [6.6.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.5.0...@ionic/cli@6.6.0) (2020-04-14) - - -### Bug Fixes - -* **capacitor:** restore capacitor config after livereload ([cdc0190](https://github.com/ionic-team/ionic-cli/commit/cdc0190eaef38f5a5f1dad79c0953bfd8fa08c57)) - - -### Features - -* **capacitor:** warn about server.url during capacitor builds ([5f6b5dd](https://github.com/ionic-team/ionic-cli/commit/5f6b5dd46e0206c7de51a518a1e463a921d85217)) -* `ionic capacitor build` command ([#4254](https://github.com/ionic-team/ionic-cli/issues/4254)) ([906fa96](https://github.com/ionic-team/ionic-cli/commit/906fa96ddca7e0156f22d94af3029b29306a6d3a)) - - - - - -# [6.5.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.4.3...@ionic/cli@6.5.0) (2020-04-07) - - -### Features - -* **start:** Default to Capacitor for React ([#4383](https://github.com/ionic-team/ionic-cli/issues/4383)) ([3fd72ec](https://github.com/ionic-team/ionic-cli/commit/3fd72ec828a1a30ddb8439bb9c7123bdcc88e46a)) -* **start:** icon/splash from start wizard ([#4376](https://github.com/ionic-team/ionic-cli/issues/4376)) ([bb4c12b](https://github.com/ionic-team/ionic-cli/commit/bb4c12b569685dad228ff7a5588a19ba22c06278)) - - - - - -## [6.4.3](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.4.2...@ionic/cli@6.4.3) (2020-04-05) - - -### Bug Fixes - -* **cordova:** make sure build is complete before reading output.json ([f1abe83](https://github.com/ionic-team/ionic-cli/commit/f1abe8392a6f2eaf910d23558b26228affd467bb)) -* **cordova:** use relative path for ipa files ([4831758](https://github.com/ionic-team/ionic-cli/commit/4831758ed12982a1dd4a4cd30f447a304abc4140)) - - - - - -## [6.4.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.4.1...@ionic/cli@6.4.2) (2020-04-05) - - -### Bug Fixes - -* **cordova:** use integration root path for file read ([79dc0b1](https://github.com/ionic-team/ionic-cli/commit/79dc0b10a9ff68e0b4a29e826ef95891b0129b8e)) - - - - - -## [6.4.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.4.0...@ionic/cli@6.4.1) (2020-04-02) - - -### Bug Fixes - -* **cordova:** use output.json to deploy correct apk ([#4377](https://github.com/ionic-team/ionic-cli/issues/4377)) ([3f09331](https://github.com/ionic-team/ionic-cli/commit/3f09331daa38f1ebe9eae79831e9d1fe7f28c1a2)) - - - - - -# [6.4.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.3.0...@ionic/cli@6.4.0) (2020-03-30) - - -### Bug Fixes - -* **angular:** convert `--no-` to `--=false` for ng ([176ff4b](https://github.com/ionic-team/ionic-cli/commit/176ff4b960c883950f7247120f776ee110816f88)) -* **capacitor:** check for capacitor.config.json before integrating ([2d7992c](https://github.com/ionic-team/ionic-cli/commit/2d7992cf2093e671a4d10243bf959a2e0f917b60)) - - -### Features - -* **package:** add functionality to deploy to app store ([#4366](https://github.com/ionic-team/ionic-cli/issues/4366)) ([cdd87ac](https://github.com/ionic-team/ionic-cli/commit/cdd87ac795f22d82401f79abb97bef65d8dd1e33)) - - - - - -# [6.3.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.2.2...@ionic/cli@6.3.0) (2020-03-18) - - -### Features - -* **start:** online start experience ([#4356](https://github.com/ionic-team/ionic-cli/issues/4356)) ([fcc5a75](https://github.com/ionic-team/ionic-cli/commit/fcc5a75a11291d069dae123b9d331ecae2d2ac2a)) - - - - - -## [6.2.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.2.1...@ionic/cli@6.2.2) (2020-03-16) - - -### Bug Fixes - -* **angular:** always provide the project name fallback ([#4349](https://github.com/ionic-team/ionic-cli/issues/4349)) ([c498f59](https://github.com/ionic-team/ionic-cli/commit/c498f598dac54e8050f041401648efba0597ec3a)) -* **multi-app:** use proper paths for integration root ([d53cee3](https://github.com/ionic-team/ionic-cli/commit/d53cee3def8dab8dd3e76c643d73c7c134b327fa)) - - - - - -## [6.2.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.2.0...@ionic/cli@6.2.1) (2020-03-09) - - -### Bug Fixes - -* **react:** accept environment variables into build process ([0932ff0](https://github.com/ionic-team/ionic-cli/commit/0932ff04f45633e51b1d4b78857dbecedaee0c45)) - - - - - -# [6.2.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.1.0...@ionic/cli@6.2.0) (2020-03-03) - - -### Features - -* add support for pnpm ([#4330](https://github.com/ionic-team/ionic-cli/issues/4330)) ([47bf076](https://github.com/ionic-team/ionic-cli/commit/47bf076427f3c5d36e241b85cacf30b23f2efd24)) -* **init:** add --multi-app option to init monorepos ([42c5199](https://github.com/ionic-team/ionic-cli/commit/42c51996632cdbc6ad6e514f36a584e3a450e380)) -* **serve:** -c as alias for --configuration for Angular projects ([c010f37](https://github.com/ionic-team/ionic-cli/commit/c010f375dc8f1d7bc93cb313729f92d4d905a3cc)) -* **serve:** add `--public-host` option ([#4331](https://github.com/ionic-team/ionic-cli/issues/4331)) ([8ae752d](https://github.com/ionic-team/ionic-cli/commit/8ae752d60de69063c752870f96a617a270316ca7)) -* **start:** add signup prompt ([#4338](https://github.com/ionic-team/ionic-cli/issues/4338)) ([1f23b66](https://github.com/ionic-team/ionic-cli/commit/1f23b662c05114d1645b6f3ab2678e54d517b324)) - - - - - -# 6.1.0 (2020-02-11) - - -### Features - -* **start:** add new list starter option ([#4315](https://github.com/ionic-team/ionic-cli/issues/4315)) ([1df44c1](https://github.com/ionic-team/ionic-cli/commit/1df44c1591f37b89f2b672857740edd6cb2aea67)) - - - - - -## [6.0.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.0.1...@ionic/cli@6.0.2) (2020-02-10) - -### Bug Fixes - -* **capacitor:** allow cleartext for Android live-reload ([#4308](https://github.com/ionic-team/ionic-cli/issues/4308)) ([4d13261](https://github.com/ionic-team/ionic-cli/commit/4d132615fdac01af93404ff939a3364d6f72fb6e)) - -### Features - -* **capacitor:** perform Ionic build for copy and sync ([be42aee](https://github.com/ionic-team/ionic-cli/commit/be42aee601e7abb0fd2f0b3957d561e26b0c688f)) -* **config:** make project file configurable ([43ba45c](https://github.com/ionic-team/ionic-cli/commit/43ba45c134abae3279dd8776bd667b7dd9c002a7)) - -## [6.0.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.0.0...@ionic/cli@6.0.1) (2020-02-03) - - -### Bug Fixes - -* **update:** show update message even with command error ([6f32ebc](https://github.com/ionic-team/ionic-cli/commit/6f32ebc7f07f8bdd39ee538c9d605945c4a8e03e)) - - -### Features - -* **start**: update "my-first-app" to use Capacitor for Angular and React ([#4307](https://github.com/ionic-team/ionic-cli/issues/4307)) ([de350a7](https://github.com/ionic-team/ionic-cli/commit/de350a757078515a94ebbac3cf45b915970345f7)) -* **config:** add `delete` and `del` aliases for `ionic config unset` ([56295c2](https://github.com/ionic-team/ionic-cli/commit/56295c2641cadd350c780db0e559a4bac49a0112)) -* **cordova:** add `cdv` alias for `ionic cordova` ([ea503b7](https://github.com/ionic-team/ionic-cli/commit/ea503b7c5580fe449b10a1e11a40179375dfa1e9)) -* **cordova:** add `res` alias for `ionic cordova resources` ([2d6ec39](https://github.com/ionic-team/ionic-cli/commit/2d6ec39243752abed92eaa6f3be56460d2b40bf4)) -* **integrations:** add `integration` alias for `ionic integrations` ([a9bf5a1](https://github.com/ionic-team/ionic-cli/commit/a9bf5a16c95f0a5760179d5b9db569242d6a12a5)) -* **integrations:** add `en` and `add` aliases for `ionic integrations enable` ([b85b5f9](https://github.com/ionic-team/ionic-cli/commit/b85b5f9d4c76e877d126e7185459589d73deb529)) -* **integrations:** add `dis`, `delete`, `del`, `remove`, and `rm` aliases for `ionic integrations disable`([fd33cb3](https://github.com/ionic-team/ionic-cli/commit/fd33cb3d0b32520d609850cab83cbba3c767c7e4)) -* **integrations:** add `dis`, `delete`, `del`, `remove`, and `rm` aliases for `ionic integrations disable`([fd33cb3](https://github.com/ionic-team/ionic-cli/commit/fd33cb3d0b32520d609850cab83cbba3c767c7e4)) - - -# [6.0.0](https://github.com/ionic-team/ionic-cli/compare/ionic@5.4.15...@ionic/cli@6.0.0) (2020-01-25) - -The following changes were made in this major release of the Ionic CLI: - -* The package name for the Ionic CLI is now `@ionic/cli`! :tada: The old `ionic` package will not be updated to CLI 6. -* The DevApp [has been retired](https://ionicframework.com/docs/appflow/devapp), so `--devapp` has been removed from `ionic serve`. We encourage you to use native tooling (Cordova/Capacitor, Android Studio, and Xcode) to develop apps on hardware and virtual devices. -* The Ionic Resources server is being retired, so `--no-cordova-res` has been removed from `ionic cordova resources`. We encourage you to switch to [`cordova-res`](https://github.com/ionic-team/cordova-res). -* We are now recommending :zap: [Capacitor](https://capacitor.ionicframework.com/) :zap: as the official native runtime for Ionic apps. Cordova will still be supported for this major release. - -#### :lollipop: Upgrading from CLI 5 - -Make sure you have NodeJS v10.3.0+ installed. We recommend [the latest LTS version](https://nodejs.org/). - -Uninstall the old Ionic CLI from your computer. - -``` -npm uninstall -g ionic -``` - -Install Ionic CLI 6 to your computer. - -``` -npm install -g @ionic/cli -``` - -### BREAKING CHANGES - -* The npm package for the Ionic CLI is now `@ionic/cli`. -* A minimum of Node.js 10.3.0 is required. -* `--devapp` is no longer an option of `ionic serve`. -* `--no-cordova-res` is no longer an option of `ionic cordova resources`. - - - -## [5.4.15](https://github.com/ionic-team/ionic-cli/compare/ionic@5.4.14...ionic@5.4.15) (2020-01-15) - - -### Bug Fixes - -* **react:** indicate serve ready on warn/error as well ([#4280](https://github.com/ionic-team/ionic-cli/issues/4280)) ([1e9b754](https://github.com/ionic-team/ionic-cli/commit/1e9b7546fa561a99c33072908672fcea25c778d9)) - - - - - -## [5.4.14](https://github.com/ionic-team/ionic-cli/compare/ionic@5.4.13...ionic@5.4.14) (2020-01-13) - - -### Bug Fixes - -* **deploy:** exclude source maps from manifest ([#4256](https://github.com/ionic-team/ionic-cli/issues/4256)) ([52a33ba](https://github.com/ionic-team/ionic-cli/commit/52a33ba116e28c61287b03e938b5883df45ffb7b)) - - - - - -## [5.4.13](https://github.com/ionic-team/ionic-cli/compare/ionic@5.4.12...ionic@5.4.13) (2019-12-13) - - -### Bug Fixes - -* **react:** fix build options, update tonality/style ([8656c60](https://github.com/ionic-team/ionic-cli/commit/8656c60ebb8206d2935669b7654995b82910e048)) - - - - - -## [5.4.12](https://github.com/ionic-team/ionic-cli/compare/ionic@5.4.11...ionic@5.4.12) (2019-12-10) - -**Note:** Version bump only for package ionic - - - - - -## [5.4.11](https://github.com/ionic-team/ionic-cli/compare/ionic@5.4.10...ionic@5.4.11) (2019-12-09) - - -### Bug Fixes - -* **cordova:** do not detect hardware devices when using emulate ([446a149](https://github.com/ionic-team/ionic-cli/commit/446a1495eefc63995450d8b7e899b44910d70372)) - - - - - -## [5.4.10](https://github.com/ionic-team/ionic-cli/compare/ionic@5.4.9...ionic@5.4.10) (2019-12-05) - - -### Bug Fixes - -* **devapp:** enable cordova upon first serve w/ devapp ([fd27ee3](https://github.com/ionic-team/ionic-cli/commit/fd27ee3425ed4c20e8b117e1bb3c8175d709d94b)) -* **telemetry:** disable for non-TTY ([81dfff3](https://github.com/ionic-team/ionic-cli/commit/81dfff3d7936519763c11a0f128360434435d179)) - - - - - -## [5.4.9](https://github.com/ionic-team/ionic-cli/compare/ionic@5.4.8...ionic@5.4.9) (2019-11-25) - - -### Bug Fixes - -* **link:** don't double encode GitHub URL ([#4230](https://github.com/ionic-team/ionic-cli/issues/4230)) ([cfb593b](https://github.com/ionic-team/ionic-cli/commit/cfb593b177cd98ad41358d1640accd62b78a4c6b)) - - - - - -## [5.4.8](https://github.com/ionic-team/ionic-cli/compare/ionic@5.4.7...ionic@5.4.8) (2019-11-24) - - -### Bug Fixes - -* fix parsing of command line options ([060f67c](https://github.com/ionic-team/ionic-cli/commit/060f67cf63d37662ae44c4ae952161464a5d553c)) - - - - - -## [5.4.7](https://github.com/ionic-team/ionic-cli/compare/ionic@5.4.6...ionic@5.4.7) (2019-11-21) - - -### Bug Fixes - -* do not try to auth for select commands ([162e733](https://github.com/ionic-team/ionic-cli/commit/162e733eaa5cf2efcaa0e5e087df53c376f62933)) - - - - - -## [5.4.6](https://github.com/ionic-team/ionic-cli/compare/ionic@5.4.5...ionic@5.4.6) (2019-11-12) - - -### Bug Fixes - -* **login:** don't doubly encode SSO login URL params ([#4209](https://github.com/ionic-team/ionic-cli/issues/4209)) ([e5fddfb](https://github.com/ionic-team/ionic-cli/commit/e5fddfb175f3eaf53a9dc5dd134d173a56098298)) - - - - - -## [5.4.5](https://github.com/ionic-team/ionic-cli/compare/ionic@5.4.4...ionic@5.4.5) (2019-10-30) - - -### Bug Fixes - -* **repair:** ignore missing package-lock.json when repairing ([#4197](https://github.com/ionic-team/ionic-cli/issues/4197)) ([94bddf3](https://github.com/ionic-team/ionic-cli/commit/94bddf38bd895396ab6d0aca87bf5016b45dc42b)) - - - - - -## [5.4.4](https://github.com/ionic-team/ionic-cli/compare/ionic@5.4.3...ionic@5.4.4) (2019-10-14) - -**Note:** Version bump only for package ionic - - - - - -## [5.4.3](https://github.com/ionic-team/ionic-cli/compare/ionic@5.4.2...ionic@5.4.3) (2019-10-14) - -**Note:** Version bump only for package ionic - - - - - -## [5.4.2](https://github.com/ionic-team/ionic-cli/compare/ionic@5.4.1...ionic@5.4.2) (2019-10-01) - - -### Bug Fixes - -* **cordova:** fix hanging issue for run w/ livereload ([#4162](https://github.com/ionic-team/ionic-cli/issues/4162)) ([1001108](https://github.com/ionic-team/ionic-cli/commit/1001108)) - - - - - -## [5.4.1](https://github.com/ionic-team/ionic-cli/compare/ionic@5.4.0...ionic@5.4.1) (2019-09-20) - - -### Bug Fixes - -* **cordova:** fix hanging issue for prepare/compile ([44697cf](https://github.com/ionic-team/ionic-cli/commit/44697cf)) - - - - - -# [5.4.0](https://github.com/ionic-team/ionic-cli/compare/ionic@5.3.0...ionic@5.4.0) (2019-09-20) - - -### Bug Fixes - -* **angular:** fix `--prod` option ([609b80d](https://github.com/ionic-team/ionic-cli/commit/609b80d)) - - -### Features - -* **start:** add prompt for framework ([#4161](https://github.com/ionic-team/ionic-cli/issues/4161)) ([8f6ce37](https://github.com/ionic-team/ionic-cli/commit/8f6ce37)) - - - - - -# [5.3.0](https://github.com/ionic-team/ionic-cli/compare/ionic@5.2.8...ionic@5.3.0) (2019-09-18) - - -### Bug Fixes - -* **angular:** show Angular CLI prompts in `ionic generate` ([#4123](https://github.com/ionic-team/ionic-cli/issues/4123)) ([e97f868](https://github.com/ionic-team/ionic-cli/commit/e97f868)) -* **cordova:** fix hanging issue with modern iOS build system ([b7758ed](https://github.com/ionic-team/ionic-cli/commit/b7758ed)) -* **cordova:** offer confirmation on adding unknown platform ([453f08b](https://github.com/ionic-team/ionic-cli/commit/453f08b)) - - -### Features - -* **angular:** document and pass along `--watch` option to ng ([1444212](https://github.com/ionic-team/ionic-cli/commit/1444212)) -* **cordova:** --external option for hosting dev server on 0.0.0.0 ([fc6020f](https://github.com/ionic-team/ionic-cli/commit/fc6020f)) - - - - - -## [5.2.8](https://github.com/ionic-team/ionic-cli/compare/ionic@5.2.7...ionic@5.2.8) (2019-09-10) - - -### Bug Fixes - -* **cordova:** use allowlist for supported project types ([167a68c](https://github.com/ionic-team/ionic-cli/commit/167a68c)) -* **multi-app:** use proper config path for integrations ([4e8a226](https://github.com/ionic-team/ionic-cli/commit/4e8a226)) - - - - - -## [5.2.7](https://github.com/ionic-team/ionic-cli/compare/ionic@5.2.6...ionic@5.2.7) (2019-08-28) - - -### Bug Fixes - -* **cordova:** use correct package path for release builds ([df34272](https://github.com/ionic-team/ionic-cli/commit/df34272)) - - - - - -## [5.2.6](https://github.com/ionic-team/ionic-cli/compare/ionic@5.2.5...ionic@5.2.6) (2019-08-23) - - -### Bug Fixes - -* **cordova:** handle whitespace in app name when running ([0e662e7](https://github.com/ionic-team/ionic-cli/commit/0e662e7)) -* **react:** Disable CRA from opening window so two windows don't open ([#4119](https://github.com/ionic-team/ionic-cli/issues/4119)) ([f10c92c](https://github.com/ionic-team/ionic-cli/commit/f10c92c)) - - - - - -## [5.2.5](https://github.com/ionic-team/ionic-cli/compare/ionic@5.2.4...ionic@5.2.5) (2019-08-14) - -**Note:** Version bump only for package ionic - - - - - -## [5.2.4](https://github.com/ionic-team/ionic-cli/compare/ionic@5.2.3...ionic@5.2.4) (2019-08-07) - -**Note:** Version bump only for package ionic - - - - - -## [5.2.3](https://github.com/ionic-team/ionic-cli/compare/ionic@5.2.2...ionic@5.2.3) (2019-07-15) - -**Note:** Version bump only for package ionic - - - - - -## [5.2.2](https://github.com/ionic-team/ionic-cli/compare/ionic@5.2.1...ionic@5.2.2) (2019-07-09) - - -### Bug Fixes - -* **cordova:** revert to `--no-connect` as default ([e4368a8](https://github.com/ionic-team/ionic-cli/commit/e4368a8)) -* **deploy:** support custom Capacitor build directory ([#4065](https://github.com/ionic-team/ionic-cli/issues/4065)) ([6146296](https://github.com/ionic-team/ionic-cli/commit/6146296)) - - - - - -## [5.2.1](https://github.com/ionic-team/ionic-cli/compare/ionic@5.2.0...ionic@5.2.1) (2019-06-28) - -* **deploy:** fix bug with `ionic deploy configure` in CI ([7774dfe](https://github.com/ionic-team/ionic-cli/commit/7774dfe)) - - - - - -# [5.2.0](https://github.com/ionic-team/ionic-cli/compare/ionic@5.1.1...ionic@5.2.0) (2019-06-26) - - -### Bug Fixes - -* **capacitor:** save Capacitor CLI in dev deps ([dd2fd86](https://github.com/ionic-team/ionic-cli/commit/dd2fd86)) -* **cordova:** rely on package.json for plugins/platforms ([286917f](https://github.com/ionic-team/ionic-cli/commit/286917f)) - - -### Features - -* **angular:** pass `--verbose` to Angular CLI ([cf611ed](https://github.com/ionic-team/ionic-cli/commit/cf611ed)) -* **angular:** pass `--verbose` to Angular CLI for serve ([3f9e859](https://github.com/ionic-team/ionic-cli/commit/3f9e859)) -* **repair:** add `--cordova` flag for only repairing Cordova ([1055b49](https://github.com/ionic-team/ionic-cli/commit/1055b49)) - - - - - -## [5.1.1](https://github.com/ionic-team/ionic-cli/compare/ionic@5.1.0...ionic@5.1.1) (2019-06-26) - -**Note:** Version bump only for package ionic - - - - - -# [5.1.0](https://github.com/ionic-team/ionic-cli/compare/ionic@5.0.3...ionic@5.1.0) (2019-06-21) - - -### Bug Fixes - -* **start:** fix glitch with writing package.json without spaces ([389c398](https://github.com/ionic-team/ionic-cli/commit/389c398)) -* subsequent validation for various prompts ([1b92f8a](https://github.com/ionic-team/ionic-cli/commit/1b92f8a)) - - -### Features - -* **info:** show available updates for utilities ([a9f4b54](https://github.com/ionic-team/ionic-cli/commit/a9f4b54)) -* replace update-notifier ([a232c07](https://github.com/ionic-team/ionic-cli/commit/a232c07)) -* **deploy:** command to interactively add deploy plugin ([#4039](https://github.com/ionic-team/ionic-cli/issues/4039)) ([c75e66f](https://github.com/ionic-team/ionic-cli/commit/c75e66f)) - - - - - -## [5.0.3](https://github.com/ionic-team/ionic-cli/compare/ionic@5.0.2...ionic@5.0.3) (2019-06-18) - - -### Bug Fixes - -* **capacitor:** use correct web-dir path for react and capacitor ([#4029](https://github.com/ionic-team/ionic-cli/issues/4029)) ([cdf2579](https://github.com/ionic-team/ionic-cli/commit/cdf2579)) -* **cordova:** always generate resources when adding platforms ([3220b2f](https://github.com/ionic-team/ionic-cli/commit/3220b2f)) -* **cordova:** don't append --save to platform/plugin management commands ([#4028](https://github.com/ionic-team/ionic-cli/issues/4028)) ([0356d00](https://github.com/ionic-team/ionic-cli/commit/0356d00)) -* **cordova:** warn about missing cordova-res when adding platforms ([77fa904](https://github.com/ionic-team/ionic-cli/commit/77fa904)) - - - - - -## [5.0.2](https://github.com/ionic-team/ionic-cli/compare/ionic@5.0.1...ionic@5.0.2) (2019-06-10) - - -### Bug Fixes - -* **capacitor:** use dist for --web-dir for vue/capacitor ([#4012](https://github.com/ionic-team/ionic-cli/issues/4012)) ([0979715](https://github.com/ionic-team/ionic-cli/commit/0979715)) -* **cordova:** fix --project flag for cordova commands ([fb9ff0f](https://github.com/ionic-team/ionic-cli/commit/fb9ff0f)) -* **cordova:** invoke native-run in integration root ([526eda3](https://github.com/ionic-team/ionic-cli/commit/526eda3)) - - - - - -## [5.0.1](https://github.com/ionic-team/ionic-cli/compare/ionic@5.0.0...ionic@5.0.1) (2019-06-05) - - -### Bug Fixes - -* **cordova:** add port forwarding for --consolelogs ([d2e0c81](https://github.com/ionic-team/ionic-cli/commit/d2e0c81)) -* **cordova:** pass --device to native-run if specified ([5db46d5](https://github.com/ionic-team/ionic-cli/commit/5db46d5)) - - - - - -# [5.0.0](https://github.com/ionic-team/ionic-cli/compare/ionic@4.12.0...ionic@5.0.0) (2019-05-29) - -At a glance, this is what was changed or added in this major release of the Ionic CLI: - -* Support for **Ionic React** (beta) :rocket: (`ionic start --type=react`) -* [`native-run`](https://github.com/ionic-team/native-run) is used to deploy app binaries to devices for `ionic cordova run` -* [`cordova-res`](https://github.com/ionic-team/cordova-res) is used to generate Cordova resources locally for `ionic cordova resources` -* `localhost` is now the default host for `ionic serve`, `ionic cordova run -l`, and `ionic capacitor run -l` - * Port forwarding via `native-run` is used for connected Android devices - * For iOS hardware devices, you may need to serve your app externally with `--address=0.0.0.0` -* `--devapp` is now a required flag if you want to use DevApp with `ionic serve` -* `--consolelogs` for Angular projects is now available for `ionic cordova run -l` -* Command completion is available for ZSH users (`ionic completion --help`) (see [#2482](https://github.com/ionic-team/ionic-cli/issues/2482) for bash) -* CLI source code targets modern JS, resulting in noticable speed improvements - -#### :lollipop: Upgrading from CLI 4 - -Make sure you have NodeJS v8.9.4+ installed. We recommend [the latest LTS version](https://nodejs.org/). - -Install the [`cordova-res`](https://github.com/ionic-team/cordova-res) and [`native-run`](https://github.com/ionic-team/native-run) utilities. - -``` -npm i -g cordova-res native-run -``` - -For Angular 8 projects, make sure you have the latest `@ionic/angular-toolkit` installed. - -``` -npm i @ionic/angular-toolkit@latest -``` - -For Angular 7 projects, make sure you have the [latest 1.x](https://www.npmjs.com/package/@ionic/angular-toolkit?activeTab=versions) `@ionic/angular-toolkit` installed. - -``` -npm i @ionic/angular-toolkit@1 -``` - - -### Bug Fixes - -* **capacitor:** send --npm-client param to capacitor init ([#3963](https://github.com/ionic-team/ionic-cli/issues/3963)) ([44b0918](https://github.com/ionic-team/ionic-cli/commit/44b0918)) -* **cordova:** prefer plugged-in devices, be explicit with Cordova ([ef8296b](https://github.com/ionic-team/ionic-cli/commit/ef8296b)) -* **doctor:** relax the viewport regex ([d0bc021](https://github.com/ionic-team/ionic-cli/commit/d0bc021)) -* **open:** fix unbound listener ([e63b74a](https://github.com/ionic-team/ionic-cli/commit/e63b74a)) -* **prepare:** check config and platforms dir for platform state ([d9e1ced](https://github.com/ionic-team/ionic-cli/commit/d9e1ced)) -* **react:** support react git repo ([#3982](https://github.com/ionic-team/ionic-cli/issues/3982)) ([c06857f](https://github.com/ionic-team/ionic-cli/commit/c06857f)) - - -### chore - -* **serve:** `--devapp` required for DevApp ([5ad11ef](https://github.com/ionic-team/ionic-cli/commit/5ad11ef)) -* **serve:** remove `--local` option ([806eaaa](https://github.com/ionic-team/ionic-cli/commit/806eaaa)) -* **serve:** switch default host to `localhost` ([d2a32de](https://github.com/ionic-team/ionic-cli/commit/d2a32de)) -* require Node 8 ([5670e68](https://github.com/ionic-team/ionic-cli/commit/5670e68)) - - -### Features - -* **cordova:** handle lack of port forwarding in ios ([d68faf1](https://github.com/ionic-team/ionic-cli/commit/d68faf1)) -* add -v as shortcut for --version ([d917e8e](https://github.com/ionic-team/ionic-cli/commit/d917e8e)) -* **start:** add conference and my-first-app starter templates for angular ([#3978](https://github.com/ionic-team/ionic-cli/issues/3978)) ([ea26181](https://github.com/ionic-team/ionic-cli/commit/ea26181)) -* Add React project type ([#3936](https://github.com/ionic-team/ionic-cli/issues/3936)) ([ef852fa](https://github.com/ionic-team/ionic-cli/commit/ef852fa)) -* **angular:** Ionic 4 commands no longer beta ([16be793](https://github.com/ionic-team/ionic-cli/commit/16be793)) -* **cordova:** check for `native-run` before running ([89ffd21](https://github.com/ionic-team/ionic-cli/commit/89ffd21)) -* **cordova:** instructions on how to fix faulty Android SDKs ([ce87944](https://github.com/ionic-team/ionic-cli/commit/ce87944)) -* **cordova:** use `cordova-res` by default ([83e48cf](https://github.com/ionic-team/ionic-cli/commit/83e48cf)) -* **cordova:** use `native-run` by default ([eadcba0](https://github.com/ionic-team/ionic-cli/commit/eadcba0)) -* **help:** color refactor ([5938429](https://github.com/ionic-team/ionic-cli/commit/5938429)) -* **info:** native-run and cordova-res versions ([d6af864](https://github.com/ionic-team/ionic-cli/commit/d6af864)) -* **login:** authenticate using stdin ([d88529f](https://github.com/ionic-team/ionic-cli/commit/d88529f)) -* **serve:** support `--consolelogs` for cordova serve in angular projects ([#3900](https://github.com/ionic-team/ionic-cli/issues/3900)) ([17f1438](https://github.com/ionic-team/ionic-cli/commit/17f1438)) -* Command-Line Completions ([9f66512](https://github.com/ionic-team/ionic-cli/commit/9f66512)) - - -### BREAKING CHANGES - -* **cordova:** `ionic cordova resources` no longer generates resources -using Ionic servers by default. Instead, `cordova-res` is used to -generate resources locally. PSD and AI file formats are no longer -supported in this new flow. The old behavior is available by specifying -`--no-cordova-res`. -* **cordova:** `ionic cordova run/emulate` no longer use Cordova to -deploy apps by default. `cordova run`, which builds and deploys the app, -is no longer used. Instead, `cordova build` is used to build the app and -then it is deployed to devices using `native-run`. The old behavior is -available by specifying `--no-native-run`. When using Cordova to run, -unless you manually forward ports, you will need to also specify -`--address=0.0.0.0` (or any host accessible externally). -* **serve:** For DevApp usage, the `--devapp` flag is now required -for all project types, instead of being automatic. This change was made -in conjunction with changing the default host from `0.0.0.0` to -`localhost`. By explicitly specifying `--devapp`, however, `0.0.0.0` is -used if `--address` is not specified. -* **serve:** `ionic serve` and other commands using the dev server -will no longer be hosted on the BIND ALL address (0.0.0.0) by default. -`localhost` is the new default. The old behavior is available by passing -`--address=0.0.0.0`. -* **serve:** The `ionic serve --local` option is now irrelevant. The default address is -changing to `localhost`. -* A minimum of Node.js 8.9.4 is required. -* **help:** option/command/namespace groups are now `MetadataGroup` - - - - - - -# [4.12.0](https://github.com/ionic-team/ionic-cli/compare/ionic@4.11.0...ionic@4.12.0) (2019-03-12) - - -### Features - -* **enterprise:** add shortcut for registering ([a1890b4](https://github.com/ionic-team/ionic-cli/commit/a1890b4)) -* add `i` as alias for `integrations` ([14a7ddb](https://github.com/ionic-team/ionic-cli/commit/14a7ddb)) -* **integrations:** add `--web-dir` option for capacitor integration ([#3895](https://github.com/ionic-team/ionic-cli/issues/3895)) ([8a1c4b2](https://github.com/ionic-team/ionic-cli/commit/8a1c4b2)) -* **integrations:** ionic enterprise integration ([#3905](https://github.com/ionic-team/ionic-cli/issues/3905)) ([b071fcb](https://github.com/ionic-team/ionic-cli/commit/b071fcb)) - - - - - -# [4.11.0](https://github.com/ionic-team/ionic-cli/compare/ionic@4.10.4...ionic@4.11.0) (2019-03-06) - - -### Features - -* **cordova:** add experimental `--cordova-res` flag ([16cdef8](https://github.com/ionic-team/ionic-cli/commit/16cdef8)) -* **cordova:** add experimental `--native-run` flag ([a80d465](https://github.com/ionic-team/ionic-cli/commit/a80d465)) - - - - - -## [4.10.4](https://github.com/ionic-team/ionic-cli/compare/ionic@4.10.3...ionic@4.10.4) (2019-02-27) - - - - -**Note:** Version bump only for package ionic - - -## [4.10.3](https://github.com/ionic-team/ionic-cli/compare/ionic@4.10.2...ionic@4.10.3) (2019-02-15) - - - - -**Note:** Version bump only for package ionic - - -## [4.10.2](https://github.com/ionic-team/ionic-cli/compare/ionic@4.10.1...ionic@4.10.2) (2019-02-04) - - -### Bug Fixes - -* **angular:** pass `--project` and `--configuration` for custom scripts ([2cf724f](https://github.com/ionic-team/ionic-cli/commit/2cf724f)) - - - - - -## [4.10.1](https://github.com/ionic-team/ionic-cli/compare/ionic@4.10.0...ionic@4.10.1) (2019-01-30) - - -### Bug Fixes - -* **cordova:** only forward correct options ([817879b](https://github.com/ionic-team/ionic-cli/commit/817879b)) - - - - - -# [4.10.0](https://github.com/ionic-team/ionic-cli/compare/ionic@4.9.0...ionic@4.10.0) (2019-01-29) - - -### Bug Fixes - -* **ionic-angular:** link to proper v3 docs ([a89c097](https://github.com/ionic-team/ionic-cli/commit/a89c097)) - - -### Features - -* **serve:** add DevApp support for Ionic 4 projects ([#3830](https://github.com/ionic-team/ionic-cli/issues/3830)) ([6edf373](https://github.com/ionic-team/ionic-cli/commit/6edf373)) - - - - - -# [4.9.0](https://github.com/ionic-team/ionic-cli/compare/ionic@4.8.0...ionic@4.9.0) (2019-01-23) - - -### Features - -* **custom:** run ionic:build/ionic:serve scripts ([9898fa8](https://github.com/ionic-team/ionic-cli/commit/9898fa8)) -* **resources:** --cordova-res option for local resource generation ([3c27e05](https://github.com/ionic-team/ionic-cli/commit/3c27e05)) -* **start:** make Ionic 4 the default for new projects ([#3820](https://github.com/ionic-team/ionic-cli/issues/3820)) ([0195f96](https://github.com/ionic-team/ionic-cli/commit/0195f96)) - - - - - -# [4.8.0](https://github.com/ionic-team/ionic-cli/compare/ionic@4.7.1...ionic@4.8.0) (2019-01-14) - - -### Features - -* **appflow:** command to create deploy builds ([#3815](https://github.com/ionic-team/ionic-cli/issues/3815)) ([d346e03](https://github.com/ionic-team/ionic-cli/commit/d346e03)) - - - - - -## [4.7.1](https://github.com/ionic-team/ionic-cli/compare/ionic@4.7.0...ionic@4.7.1) (2019-01-08) - - - - -**Note:** Version bump only for package ionic - - -# [4.7.0](https://github.com/ionic-team/ionic-cli/compare/ionic@4.6.0...ionic@4.7.0) (2019-01-07) - - -### Bug Fixes - -* **doctor:** handle multilines for viewport-fit-not-set ([#3809](https://github.com/ionic-team/ionic-cli/issues/3809)) ([6aa7ae6](https://github.com/ionic-team/ionic-cli/commit/6aa7ae6)) -* **executor:** Exclude options for command argument parsing ([#3798](https://github.com/ionic-team/ionic-cli/issues/3798)) ([514015f](https://github.com/ionic-team/ionic-cli/commit/514015f)) -* **serve:** handle error in opn with debug statement ([89b6d33](https://github.com/ionic-team/ionic-cli/commit/89b6d33)) - - -### Features - -* **appflow:** command to create package builds ([#3808](https://github.com/ionic-team/ionic-cli/issues/3808)) ([149f06e](https://github.com/ionic-team/ionic-cli/commit/149f06e)) -* **cordova:** auto-forward port when using --native-run ([0da50ac](https://github.com/ionic-team/ionic-cli/commit/0da50ac)) - - - - - -# [4.6.0](https://github.com/ionic-team/ionic-cli/compare/ionic@4.5.0...ionic@4.6.0) (2018-12-19) - - -### Bug Fixes - -* **cordova:** warn for prepare w/o platforms ([385bdf2](https://github.com/ionic-team/ionic-cli/commit/385bdf2)) - - -### Features - -* **serve:** collapse numbered chunk output and summarize ([5ac6834](https://github.com/ionic-team/ionic-cli/commit/5ac6834)) -* **start:** update for Ionic Framework 4.0 RC ([7e943cc](https://github.com/ionic-team/ionic-cli/commit/7e943cc)) - - - - - -# [4.5.0](https://github.com/ionic-team/ionic-cli/compare/ionic@4.4.0...ionic@4.5.0) (2018-11-27) - - -### Bug Fixes - -* **cordova:** do not error for ctrl+c during livereload ([facc96b](https://github.com/ionic-team/ionic-cli/commit/facc96b)) -* **serve:** ignore link-local addresses ([#3761](https://github.com/ionic-team/ionic-cli/issues/3761)) ([1b7fd90](https://github.com/ionic-team/ionic-cli/commit/1b7fd90)) -* **telemetry:** disable automatically for CI ([217ca12](https://github.com/ionic-team/ionic-cli/commit/217ca12)) - - -### Features - -* Ionic Appflow rebranding ([16360af](https://github.com/ionic-team/ionic-cli/commit/16360af)) -* **cordova:** add `--native-run` option to Cordova run ([#3757](https://github.com/ionic-team/ionic-cli/issues/3757)) ([9ef53ad](https://github.com/ionic-team/ionic-cli/commit/9ef53ad)) - - - - - -# [4.4.0](https://github.com/ionic-team/ionic-cli/compare/ionic@4.3.1...ionic@4.4.0) (2018-11-20) - - -### Bug Fixes - -* **multi-app:** properly handle bare projects ([3f84a6f](https://github.com/ionic-team/ionic-cli/commit/3f84a6f)), closes [/github.com/ionic-team/ionic-docs/issues/83#issuecomment-439655706](https://github.com//github.com/ionic-team/ionic-docs/issues/83/issues/issuecomment-439655706) -* **project:** write determined project type to config ([307d81f](https://github.com/ionic-team/ionic-cli/commit/307d81f)) - - -### Features - -* **build:** show build progress ([3090615](https://github.com/ionic-team/ionic-cli/commit/3090615)) -* **help:** show value hint for options ([aa13ba8](https://github.com/ionic-team/ionic-cli/commit/aa13ba8)) -* **init:** add `ionic init` command ([4a12b17](https://github.com/ionic-team/ionic-cli/commit/4a12b17)) -* **login:** SSO authentication flow ([#3741](https://github.com/ionic-team/ionic-cli/issues/3741)) ([71b319a](https://github.com/ionic-team/ionic-cli/commit/71b319a)) -* **serve:** forward `--ssl` to Angular CLI ([815b49a](https://github.com/ionic-team/ionic-cli/commit/815b49a)) - - - - - -## [4.3.1](https://github.com/ionic-team/ionic-cli/compare/ionic@4.3.0...ionic@4.3.1) (2018-11-04) - - -### Bug Fixes - -* **bin:** stringify unresolved promise event ([788a5ec](https://github.com/ionic-team/ionic-cli/commit/788a5ec)) -* **bootstrap:** supply env for CLI <4.3.0 ([78dbda8](https://github.com/ionic-team/ionic-cli/commit/78dbda8)) -* **multi-app:** hide project warnings during start ([b1ecd77](https://github.com/ionic-team/ionic-cli/commit/b1ecd77)) -* **start:** fix stdio freezing issue on Windows ([#3725](https://github.com/ionic-team/ionic-cli/issues/3725)) ([a570770](https://github.com/ionic-team/ionic-cli/commit/a570770)) - - - - - -# [4.3.0](https://github.com/ionic-team/ionic-cli/compare/ionic@4.2.1...ionic@4.3.0) (2018-10-31) - - -### Bug Fixes - -* **capacitor:** switch off livereload for --no-build ([9960047](https://github.com/ionic-team/ionic-cli/commit/9960047)) -* **help:** filter out unnecessary global options ([7809c99](https://github.com/ionic-team/ionic-cli/commit/7809c99)) -* **ssh:** adjust validator to work for OpenSSH 7.8 ([dcc598a](https://github.com/ionic-team/ionic-cli/commit/dcc598a)) -* **terminal:** adjust some feature detection on windows ([8a2ed99](https://github.com/ionic-team/ionic-cli/commit/8a2ed99)) - - -### Features - -* **capacitor:** add --livereload-url option for custom dev server ([b7738f5](https://github.com/ionic-team/ionic-cli/commit/b7738f5)) -* **cordova:** add --livereload-url option for custom dev server ([ad57e36](https://github.com/ionic-team/ionic-cli/commit/ad57e36)) -* **integrations:** --root option for choosing an alternative location ([7e8f11e](https://github.com/ionic-team/ionic-cli/commit/7e8f11e)) -* **multi-app:** determine active project via cwd path match ([f83dc5b](https://github.com/ionic-team/ionic-cli/commit/f83dc5b)) -* **resources:** generate resources without needing platform installation ([4f20554](https://github.com/ionic-team/ionic-cli/commit/4f20554)) -* **start:** better multi-app support ([3c70e87](https://github.com/ionic-team/ionic-cli/commit/3c70e87)) - - - - - -## [4.2.1](https://github.com/ionic-team/ionic-cli/compare/ionic@4.2.0...ionic@4.2.1) (2018-10-05) - - -### Bug Fixes - -* **doctor:** fix viewport-fit-not-set for ionic1 ([826b9ae](https://github.com/ionic-team/ionic-cli/commit/826b9ae)) -* **info:** show versions for [@ionic](https://github.com/ionic)/angular-toolkit ([9d1824a](https://github.com/ionic-team/ionic-cli/commit/9d1824a)) - - - - - -# [4.2.0](https://github.com/ionic-team/ionic-cli/compare/ionic@4.1.2...ionic@4.2.0) (2018-10-03) - - -### Bug Fixes - -* **cordova:** ng should ignore separated args for Cordova build ([16a0111](https://github.com/ionic-team/ionic-cli/commit/16a0111)) -* **lab:** use correct query params for project type ([4801680](https://github.com/ionic-team/ionic-cli/commit/4801680)) -* **serve:** fix incorrect message about app-scripts not being installed ([abd665b](https://github.com/ionic-team/ionic-cli/commit/abd665b)) -* **serve:** unnecessary message about utility CLI exiting during Ctrl+C ([8e78bf3](https://github.com/ionic-team/ionic-cli/commit/8e78bf3)) - - -### Features - -* new `ionic repair` command ([7588233](https://github.com/ionic-team/ionic-cli/commit/7588233)) - - - - - -## [4.1.2](https://github.com/ionic-team/ionic-cli/compare/ionic@4.1.1...ionic@4.1.2) (2018-09-05) - - -### Bug Fixes - -* **capacitor:** use integration root for Capacitor CLI ([81a45d5](https://github.com/ionic-team/ionic-cli/commit/81a45d5)) - - - - - -## [4.1.1](https://github.com/ionic-team/ionic-cli/compare/ionic@4.1.0...ionic@4.1.1) (2018-08-20) - - -### Bug Fixes - -* **cordova:** respect --nosave for platform/plugin add ([eb4934b](https://github.com/ionic-team/ionic-cli/commit/eb4934b)) -* **deploy:** exclude any existing pro-manifest.json files ([#3527](https://github.com/ionic-team/ionic-cli/issues/3527)) ([d03057d](https://github.com/ionic-team/ionic-cli/commit/d03057d)) -* **shell:** allow output() to fail with original error ([e6a5bff](https://github.com/ionic-team/ionic-cli/commit/e6a5bff)) -* **shell:** check if process is still alive before sending signal ([0ff1e48](https://github.com/ionic-team/ionic-cli/commit/0ff1e48)) - - - - - -# [4.1.0](https://github.com/ionic-team/ionic-cli/compare/ionic@4.0.6...ionic@4.1.0) (2018-08-15) - - -### Bug Fixes - -* **capacitor:** pass in project/package id during start ([1357c5c](https://github.com/ionic-team/ionic-cli/commit/1357c5c)) -* **cordova:** properly error for multiple IPs with non-interactive mode ([0346adc](https://github.com/ionic-team/ionic-cli/commit/0346adc)) -* **cordova:** remove unwanted allow-navigation entries ([77984e1](https://github.com/ionic-team/ionic-cli/commit/77984e1)) -* **info:** disable update check for cordova cli ([8310ff6](https://github.com/ionic-team/ionic-cli/commit/8310ff6)) -* **info:** remove version.json warning for v1 ([78c3582](https://github.com/ionic-team/ionic-cli/commit/78c3582)) -* **link:** support new repo association types ([1c1e1f1](https://github.com/ionic-team/ionic-cli/commit/1c1e1f1)) - - -### Features - -* **capacitor:** add run command ([62a2918](https://github.com/ionic-team/ionic-cli/commit/62a2918)) -* **capacitor:** document platform argument, prompt when required ([2a312ab](https://github.com/ionic-team/ionic-cli/commit/2a312ab)) -* **capacitor:** install platform if missing ([9e29235](https://github.com/ionic-team/ionic-cli/commit/9e29235)) -* **capacitor:** prompt for supported platforms when adding ([54c7d55](https://github.com/ionic-team/ionic-cli/commit/54c7d55)) -* **capacitor:** unlock capacitor commands as beta ([2480a01](https://github.com/ionic-team/ionic-cli/commit/2480a01)) -* **info:** print whitelisted cordova plugins ([c266b7b](https://github.com/ionic-team/ionic-cli/commit/c266b7b)) - - - - - -## [4.0.6](https://github.com/ionic-team/ionic-cli/compare/ionic@4.0.5...ionic@4.0.6) (2018-08-09) - - -### Bug Fixes - -* **serve:** fix unclosed connection issue again ([#3500](https://github.com/ionic-team/ionic-cli/issues/3500)) ([1f0ef3b](https://github.com/ionic-team/ionic-cli/commit/1f0ef3b)) - - - - - -## [4.0.5](https://github.com/ionic-team/ionic-cli/compare/ionic@4.0.4...ionic@4.0.5) (2018-08-07) - - - - -**Note:** Version bump only for package ionic - - -## [4.0.4](https://github.com/ionic-team/ionic-cli/compare/ionic@4.0.3...ionic@4.0.4) (2018-08-06) - - -### Bug Fixes - -* **serve:** properly cleanup child processes ([#3481](https://github.com/ionic-team/ionic-cli/issues/3481)) ([38217bf](https://github.com/ionic-team/ionic-cli/commit/38217bf)) - - - - - -## [4.0.3](https://github.com/ionic-team/ionic-cli/compare/ionic@4.0.2...ionic@4.0.3) (2018-08-02) - - -### Bug Fixes - -* **build:** prompt to install "build cli" for all projects ([2862762](https://github.com/ionic-team/ionic-cli/commit/2862762)) -* **serve:** await connectivity on specified host, not localhost ([#3444](https://github.com/ionic-team/ionic-cli/issues/3444)) ([bf10674](https://github.com/ionic-team/ionic-cli/commit/bf10674)) -* **serve:** check all network interfaces for an available port ([30fd6ef](https://github.com/ionic-team/ionic-cli/commit/30fd6ef)) -* **serve:** fix --livereload for device/emulator ([f31e79d](https://github.com/ionic-team/ionic-cli/commit/f31e79d)) -* **serve:** use correct livereload port option for v1 ([bf3e775](https://github.com/ionic-team/ionic-cli/commit/bf3e775)) - - - - - -## [4.0.2](https://github.com/ionic-team/ionic-cli/compare/ionic@4.0.1...ionic@4.0.2) (2018-07-30) - - -### Bug Fixes - -* **cordova:** default to `cordova prepare` without platforms ([d40d961](https://github.com/ionic-team/ionic-cli/commit/d40d961)) -* **resources:** fix hanging issue ([#3429](https://github.com/ionic-team/ionic-cli/issues/3429)) ([6b7c732](https://github.com/ionic-team/ionic-cli/commit/6b7c732)) -* **build:** properly pass --target to remove fonts for cordova ([530d87a](https://github.com/ionic-team/ionic-cli/commit/530d87a)) -* **generate:** run in current directory ([54c632b](https://github.com/ionic-team/ionic-cli/commit/54c632b)) -* **info:** filter out hidden files/folders ([2e56dd7](https://github.com/ionic-team/ionic-cli/commit/2e56dd7)) -* **info:** show 'not available' when cordova is missing ([db60879](https://github.com/ionic-team/ionic-cli/commit/db60879)) - - - - - -## [4.0.1](https://github.com/ionic-team/ionic-cli/compare/ionic@4.0.0...ionic@4.0.1) (2018-07-26) - - -### Bug Fixes - -* **generate:** remove pages/ prefix recommendation ([#3392](https://github.com/ionic-team/ionic-cli/issues/3392)) ([23d0db6](https://github.com/ionic-team/ionic-cli/commit/23d0db6)) -* **help:** properly show option decorations ([b2509de](https://github.com/ionic-team/ionic-cli/commit/b2509de)) - - - - - -# [4.0.0](https://github.com/ionic-team/ionic-cli/compare/ionic@4.0.0-rc.13...ionic@4.0.0) (2018-07-25) - - - - -**Note:** Version bump only for package ionic - -This release offers support for :sparkles: **Ionic 4** :sparkles: (beta). - -:memo: Use the new [CLI Documentation](https://beta.ionicframework.com/docs/cli) on the beta framework documentation website for CLI 4. - -#### :lollipop: Upgrading from CLI 3 - -Aside from a few edge cases listed below, upgrading to CLI 4 should be seamless. - -**Ionic 2/3**: The CLI will continue working with apps using `@ionic/app-scripts` for tooling (please [update to the latest version](https://github.com/ionic-team/ionic-app-scripts) to avoid any issues). The CLI will continue to support projects that have yet to migrate to Ionic 4 w/ Angular CLI for tooling. For those who wish to migrate v3 apps to v4, see the [Migration Guide](https://beta.ionicframework.com/docs/building/migration). - -**Ionic 1**: For Ionic 1 projects, a new toolkit has been introduced to slim down the main CLI package. All functionality is still supported, but the `@ionic/v1-toolkit` package needs to be installed. - -#### :boom: Breaking Changes - -* Support for legacy Ionic Cloud ended on January 31st, 2018. The `ionic upload` and `ionic package` commands have been removed from the CLI. Support for [Ionic Appflow](https://ionicframework.com/appflow/) will be a major focus for the CLI going forward. :ok_hand: -* The `app_id` property in `ionic.config.json` has been renamed to `pro_id` and is now optional (see [#3038](https://github.com/ionic-team/ionic-cli/issues/3038)). The CLI automatically detects this and changes it, but this notice is here if your build scripts rely on the setting. -* `ionic build` will no longer run `cordova prepare`. Instead, run `ionic cordova prepare `, which performs an Ionic build beforehand. -* `ionic cordova prepare` will no longer run an Ionic build without a platform, e.g. `ionic cordova prepare ios` (see [#3653](https://github.com/ionic-team/ionic-cli/issues/3653)) -* Ionic Lab has been moved into the [`@ionic/lab`](https://github.com/ionic-team/ionic-cli/tree/develop/packages/@ionic/lab) package, which will need to be installed for Lab to work. -* The `conference` starter template has been removed from `ionic start`. To clone existing apps (as opposed to starting new apps from starter templates), please use the app's repository URL. For example, to create the conference app, use `ionic start "Conference App" https://github.com/ionic-team/ionic-conference-app`. -* The `--display-name` option for `ionic start` has been removed. The `name` argument is now used as the display name and slugified for directory name, package name, etc. To provide a custom slug, use `--project-id` (see [#3038](https://github.com/ionic-team/ionic-cli/issues/3038)). -* The `ionic:watch:before` npm script hook has been renamed to `ionic:serve:before`, but behaves the same. -* `ionic doctor check` will now _only_ print issues and exit with exit code 1 if issues are found. Use `ionic doctor treat` to attempt automatic fixes. -* `ionic doctor ignore` has been removed in favor of `ionic config set -g doctor.issues..ignored true`). -* Ionic 1 build/serve functionality has been moved into the [`@ionic/v1-toolkit`](https://github.com/ionic-team/ionic-cli/tree/develop/packages/@ionic/v1-toolkit) package, which will need to be installed in your Ionic 1 project(s). The file watcher, [chokidar](https://github.com/paulmillr/chokidar), has been updated to 2.0.0, which will require those using `watchPatterns` to [always use POSIX-style slashes](https://github.com/paulmillr/chokidar/blob/master/CHANGELOG.md#chokidar-200-dec-29-2017) (not an issue if you've never used a backslash in globs for Windows). -* The gulp integration has been removed from the CLI and put into the [`@ionic/v1-toolkit`](https://github.com/ionic-team/ionic-cli/tree/develop/packages/@ionic/v1-toolkit) package, which is only for Ionic v1 apps. - -#### :rocket: Enhancements - -* `ionic s` is now an alias for `ionic serve`. -* No need for `@ionic/cli-plugin-proxy`. Proxy support is now built-in. Use existing environment variables or use `ionic config set -g proxy `. -* New `ionic config unset` command for deleting config values. -* New `ionic doctor treat` command that attempts automatic fixes of detected issues. -* If `ionic:build` or `ionic:serve` npm scripts are defined in your `package.json`, the Ionic CLI will use them for the Ionic build/serve instead of the default for your project type. -* Automatic login via `IONIC_TOKEN` environment variable [#2410](https://github.com/ionic-team/ionic-cli/issues/2410) -* `--no-color` flag for turning off CLI colors -* `--no-build` option for `ionic cordova run` and `ionic cordova emulate` [#2930](https://github.com/ionic-team/ionic-cli/pull/2930) -* Better monorepo support. See the discussion in [#2232](https://github.com/ionic-team/ionic-cli/issues/2232). -* Multi-app support for new Angular projects [#3281](https://github.com/ionic-team/ionic-cli/issues/3281) -* Added experimental `ionic ssl generate` command for generating `localhost` SSL certificates for use with `ionic serve`. :memo: HTTPS support in `ionic serve` isn't quite finished yet (see [#3305](https://github.com/ionic-team/ionic-cli/issues/3305)). - -#### :bug: Bug Fixes - -* Interactivity is now disabled when not in a TTY. See [#3047](https://github.com/ionic-team/ionic-cli/issues/3047). -* Respect `--nosave` flag for `ionic cordova platform` and `ionic cordova plugin` [#2946](https://github.com/ionic-team/ionic-cli/issues/2946) -* Chain `--verbose` flag to Cordova for `ionic cordova` commands [#2919](https://github.com/ionic-team/ionic-cli/issues/2919) -* Fixed newlines in piped output from underlying CLIs. - -#### :house: Internal - -* A huge amount of code refactoring has been accomplished to prepare the CLI for a scalable, flexible future. Changes in behavior and help output based upon environment and config are now trivially accomplished. -* The [CLI Framework](https://github.com/ionic-team/ionic-cli/tree/develop/packages/%40ionic/cli-framework), a (currently) internal framework for building general-purpose command-line programs, has had many features added to support the utility CLIs such as `ionic-lab` and `ionic-v1`. -* Ionic Lab has been rebuilt using [StencilJS](https://stenciljs.com) and now works for any Ionic Framework version. - -## Older Changes - -* [3.x `CHANGELOG.md`](https://github.com/ionic-team/ionic-cli/blob/3.x/CHANGELOG.md) -* [1.x-2.x `CHANGELOG.md`](https://github.com/ionic-team/ionic-cli/blob/2.x/CHANGELOG.md) diff --git a/packages/@ionic/cli/README.md b/packages/@ionic/cli/README.md deleted file mode 100644 index 0893189e98..0000000000 --- a/packages/@ionic/cli/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Ionic CLI - -The Ionic command line interface (CLI) is your go-to tool for developing -[Ionic][ionic-homepage] apps. - -**Documentation**: [https://ionicframework.com/docs/cli/][ionic-cli-docs] - -**Github Project**: [https://github.com/ionic-team/ionic-cli](https://github.com/ionic-team/ionic-cli) - -[ionic-homepage]: https://ionicframework.com -[ionic-cli-docs]: https://ionicframework.com/docs/cli/ diff --git a/packages/@ionic/cli/assets/oauth/success/index.html b/packages/@ionic/cli/assets/oauth/success/index.html deleted file mode 100644 index a945060dd4..0000000000 --- a/packages/@ionic/cli/assets/oauth/success/index.html +++ /dev/null @@ -1,171 +0,0 @@ - - - - - Success - - - - - - -

- - - - - -
-
- -
-

You are authenticated!

- -

Your app is now building in your terminal!

- -

There was an error logging in. Please return to the terminal.

- - -
- - - -

Identity Vault provides secure biometric authentication for mobile apps.

-
-
- - - -

Auth Connect provides secure authentication for any hybrid mobile app.

-
-
- - - -

Secure Storage provides secure, data-driven, offline-first mobile experiences.

-
-
- -
- Get Access - Go to Dashboard -
- -
-
- -
- - - - diff --git a/packages/@ionic/cli/bin/ionic b/packages/@ionic/cli/bin/ionic deleted file mode 100755 index 281732e50c..0000000000 --- a/packages/@ionic/cli/bin/ionic +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env node - -'use strict'; - -// IMPORTANT: This file uses ES5 syntax to avoid syntax errors in older Node -// versions (mostly 0.12). See https://node.green for syntax support. - -process.title = 'ionic'; -process.on('unhandledRejection', function(r) { process.stderr.write(String(r)); process.exit(1); }); - -var events = require('events'); -var path = require('path'); -var evt = new events.EventEmitter(); - -var cli; - -process.on('message', function(msg) { - evt.once('ready', function() { - cli.receive(msg); - }); -}); - -var semver = require('semver'); -var version = semver.parse(process.version); -var minversion = 'v16.0.0'; - -if (semver.lt(version, minversion)) { - var details = version.major === 10 - ? ' Node.js 10 reached end-of-life on 2021-04-30 and is no longer supported.' - : version.major === 12 - ? ' Node.js 12 reached end-of-life on 2022-04-30 and is no longer supported.' - : version.major === 14 - ? ' Node.js 14 reached end-of-life on 2023-04-30 and is no longer supported.' - : ''; - - process.stderr.write('ERR: Your Node.js version is ' + version.raw + '.' + details + ' Please update to the latest Node LTS version.\n'); - process.exit(1); -} - -if (process.argv.includes('--verbose')) { - process.env.DEBUG = '*'; -} - -if (process.argv.includes('--no-color')) { - process.env.DEBUG_COLORS = '0'; -} - -var pargv = process.argv.slice(2); -var lib = path.resolve(__dirname, '..'); - -process.env.IONIC_CLI_BIN = __filename; -process.env.IONIC_CLI_LIB = lib; - -var bootstrap = require(path.resolve(lib, 'bootstrap')); - -bootstrap.detectLocalCLI() - .then(function(localPath) { - cli = require(localPath); - return cli.run(pargv, process.env); - }, function(err) { - cli = require(lib); - - if (typeof err !== 'string') { - throw err; - } - - process.env.IONIC_CLI_LOCAL_ERROR = err; - - return cli.run(pargv, process.env); - }) - .then(function() { - evt.emit('ready'); - }); diff --git a/packages/@ionic/cli/jest.config.js b/packages/@ionic/cli/jest.config.js deleted file mode 100644 index eace8d6d9d..0000000000 --- a/packages/@ionic/cli/jest.config.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('../../../jest.config.base'); diff --git a/packages/@ionic/cli/lint-staged.config.js b/packages/@ionic/cli/lint-staged.config.js deleted file mode 100644 index 5815a72f0d..0000000000 --- a/packages/@ionic/cli/lint-staged.config.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('../../../lint-staged.config.base'); diff --git a/packages/@ionic/cli/package.json b/packages/@ionic/cli/package.json deleted file mode 100644 index d6fa27f6e8..0000000000 --- a/packages/@ionic/cli/package.json +++ /dev/null @@ -1,89 +0,0 @@ -{ - "name": "@ionic/cli", - "version": "7.2.1", - "description": "A tool for creating and developing Ionic Framework mobile apps.", - "homepage": "https://ionicframework.com", - "author": "Ionic Team (https://ionicframework.com) ", - "bin": { - "ionic": "./bin/ionic" - }, - "engines": { - "node": ">=16.0.0" - }, - "main": "./index.js", - "types": "./index.d.ts", - "scripts": { - "clean": "rimraf index.* bootstrap.* constants.* definitions.* guards.* lib commands", - "lint": "true", - "build": "npm run clean && tsc", - "watch": "tsc -w --preserveWatchOutput", - "test": "jest --maxWorkers=4", - "prepublishOnly": "npm run build" - }, - "keywords": [ - "ionic", - "ionic framework", - "ionicframework", - "mobile", - "app", - "hybrid", - "cordova", - "native", - "phonegap" - ], - "repository": { - "type": "git", - "url": "https://github.com/ionic-team/ionic-cli.git" - }, - "bugs": { - "url": "https://github.com/ionic-team/ionic-cli/issues" - }, - "license": "MIT", - "dependencies": { - "@ionic/cli-framework": "6.0.1", - "@ionic/cli-framework-output": "2.2.8", - "@ionic/cli-framework-prompts": "2.1.13", - "@ionic/utils-array": "2.1.6", - "@ionic/utils-fs": "3.1.7", - "@ionic/utils-network": "2.1.7", - "@ionic/utils-process": "2.1.12", - "@ionic/utils-stream": "3.1.7", - "@ionic/utils-subprocess": "3.0.1", - "@ionic/utils-terminal": "2.3.5", - "chalk": "^4.0.0", - "debug": "^4.0.0", - "diff": "^4.0.1", - "elementtree": "^0.1.7", - "leek": "0.0.24", - "lodash": "^4.17.5", - "open": "^7.0.4", - "os-name": "^4.0.0", - "proxy-agent": "^6.3.0", - "semver": "^7.1.1", - "split2": "^3.0.0", - "ssh-config": "^1.1.1", - "stream-combiner2": "^1.1.1", - "superagent": "^9.0.2", - "tar": "^6.0.1", - "tslib": "^2.0.1" - }, - "devDependencies": { - "@types/debug": "^4.1.1", - "@types/diff": "^4.0.0", - "@types/elementtree": "^0.1.0", - "@types/jest": "^26.0.10", - "@types/lodash": "^4.14.104", - "@types/node": "~16.0.0", - "@types/semver": "^7.1.0", - "@types/split2": "^2.1.6", - "@types/superagent": "4.1.3", - "@types/tar": "^6.1.2", - "jest": "^26.4.2", - "jest-cli": "^26.0.1", - "lint-staged": "^10.0.2", - "rimraf": "^3.0.0", - "source-map": "^0.7.0", - "ts-jest": "~26.3.0", - "typescript": "~4.8.0" - } -} diff --git a/packages/@ionic/cli/src/__tests__/guards.ts b/packages/@ionic/cli/src/__tests__/guards.ts deleted file mode 100644 index fef080a3ad..0000000000 --- a/packages/@ionic/cli/src/__tests__/guards.ts +++ /dev/null @@ -1,110 +0,0 @@ -import * as guards from '../guards'; - -type Guard = (o: any) => boolean; - -const OBJECT_GUARDS: Guard[] = [ - guards.isCommand, - guards.isCommandPreRun, - guards.isStarterManifest, - guards.isCordovaPackageJson, - guards.isExitCodeException, - guards.isSuperAgentError, - guards.isAPIResponseSuccess, - guards.isAPIResponseError, - guards.isOrg, - guards.isGithubRepo, - guards.isGithubBranch, - guards.isGithubRepoListResponse, - guards.isGithubBranchListResponse, - guards.isAppAssociation, - guards.isAppAssociationResponse, - guards.isGithubRepoAssociation, - guards.isBitbucketCloudRepoAssociation, - guards.isBitbucketServerRepoAssociation, - guards.isApp, - guards.isAppResponse, - guards.isAppsResponse, - guards.isOAuthLogin, - guards.isOAuthLoginResponse, - guards.isSnapshot, - guards.isSnapshotResponse, - guards.isSnapshotListResponse, - guards.isLogin, - guards.isLoginResponse, - guards.isUser, - guards.isUserResponse, - guards.isSSHKey, - guards.isSSHKeyListResponse, - guards.isSSHKeyResponse, - guards.isSecurityProfile, - guards.isSecurityProfileResponse, - guards.isIntegrationName, - guards.isProjectConfig, - guards.isMultiProjectConfig, -]; - -const API_RESPONSE_SUCCESS_GUARDS: Guard[] = [ - guards.isGithubRepoListResponse, - guards.isGithubBranchListResponse, - guards.isAppAssociationResponse, - guards.isAppResponse, - guards.isAppsResponse, - guards.isOAuthLoginResponse, - guards.isSnapshotResponse, - guards.isSnapshotListResponse, - guards.isLoginResponse, - guards.isUserResponse, - guards.isSSHKeyResponse, - guards.isSSHKeyListResponse, - guards.isSecurityProfileResponse, -]; - -describe('@ionic/cli', () => { - - describe('guards', () => { - - describe('object guards', () => { - - it('should return false for non-object types', () => { - const tests = [undefined, null, 0, 5, NaN, '', 'foobar', [], ['a', 'b', 'c']]; - - for (const guard of OBJECT_GUARDS) { - for (const test of tests) { - const result = guard(test); - - // if (result) { - // debugger; - // } - - expect(result).toBeFalsy(); - } - } - }); - - }); - - describe('api response success guards', () => { - - it('should return false for misshapen objects', () => { - const tests = [{}, { data: {} }, { data: 'foobar' }]; - - for (const guard of API_RESPONSE_SUCCESS_GUARDS) { - for (const test of tests) { - const result = guard(test); - - // if (result) { - // debugger; - // } - - expect(result).toBeFalsy(); - } - } - }); - - }); - - // TODO: add more tests for guards - - }); - -}); diff --git a/packages/@ionic/cli/src/bootstrap.ts b/packages/@ionic/cli/src/bootstrap.ts deleted file mode 100644 index 63ce9277d8..0000000000 --- a/packages/@ionic/cli/src/bootstrap.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { compileNodeModulesPaths, readPackageJsonFile } from '@ionic/cli-framework/utils/node'; -import { debug as Debug } from 'debug'; -import * as path from 'path'; -import * as semver from 'semver'; - -import { strong } from './lib/color'; - -const debug = Debug('ionic:bootstrap'); - -export const ERROR_BASE_DIRECTORY_NOT_FOUND = 'BASE_DIRECTORY_NOT_FOUND'; -export const ERROR_LOCAL_CLI_NOT_FOUND = 'LOCAL_CLI_NOT_FOUND'; -export const ERROR_VERSION_TOO_OLD = 'VERSION_TOO_OLD'; - -export async function detectLocalCLI(): Promise { - let pkgPath: string | undefined; - - try { - pkgPath = require.resolve('ionic/package', { paths: compileNodeModulesPaths(process.cwd()) }); - } catch (e: any) { - // ignore - } - - if (pkgPath && process.env.IONIC_CLI_LIB !== path.dirname(pkgPath)) { - const pkg = await readPackageJsonFile(pkgPath); - - debug(`local CLI ${strong(pkg.version)} found at ${strong(pkgPath)}`); - - if (semver.lt(pkg.version, '4.0.0')) { - throw ERROR_VERSION_TOO_OLD; - } - - return path.dirname(pkgPath); - } - - throw ERROR_LOCAL_CLI_NOT_FOUND; -} diff --git a/packages/@ionic/cli/src/commands/build.ts b/packages/@ionic/cli/src/commands/build.ts deleted file mode 100644 index 58ae675261..0000000000 --- a/packages/@ionic/cli/src/commands/build.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { Footnote } from '@ionic/cli-framework'; - -import { CommandInstanceInfo, CommandLineInputs, CommandLineOptions, CommandMetadata, CommandMetadataOption, CommandPreRun } from '../definitions'; -import { COMMON_BUILD_COMMAND_OPTIONS } from '../lib/build'; -import { input } from '../lib/color'; -import { Command } from '../lib/command'; -import { FatalException, RunnerException } from '../lib/errors'; - -export class BuildCommand extends Command implements CommandPreRun { - async getMetadata(): Promise { - const groups: string[] = []; - const options: CommandMetadataOption[] = []; - const footnotes: Footnote[] = []; - const exampleCommands = ['']; - let description = `${input('ionic build')} will perform an Ionic build, which compiles web assets and prepares them for deployment.`; - - const runner = this.project && await this.project.getBuildRunner(); - - if (runner) { - const libmetadata = await runner.getCommandMetadata(); - groups.push(...libmetadata.groups || []); - options.push(...libmetadata.options || []); - description += libmetadata.description ? `\n\n${libmetadata.description.trim()}` : ''; - footnotes.push(...libmetadata.footnotes || []); - exampleCommands.push(...libmetadata.exampleCommands || []); - } - - options.push(...COMMON_BUILD_COMMAND_OPTIONS); - - return { - name: 'build', - type: 'project', - summary: 'Build web assets and prepare your app for any platform targets', - description, - footnotes, - groups, - exampleCommands, - options, - }; - } - - async preRun(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - if (inputs.length > 0 && ['android', 'ios', 'wp8', 'windows', 'browser'].includes(inputs[0])) { - this.env.log.warn( - `${input('ionic build')} is for building web assets and takes no arguments. See ${input('ionic build --help')}.\n` + - `Ignoring argument ${input(inputs[0])}. Perhaps you meant ${input('ionic cordova build ' + inputs[0])}?\n` - ); - - inputs.splice(0); - } - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions, runinfo: CommandInstanceInfo): Promise { - if (!this.project) { - throw new FatalException(`Cannot run ${input('ionic build')} outside a project directory.`); - } - - try { - const runner = await this.project.requireBuildRunner(); - const runnerOpts = runner.createOptionsFromCommandLine(inputs, options); - - await runner.run(runnerOpts); - } catch (e: any) { - if (e instanceof RunnerException) { - throw new FatalException(e.message); - } - - throw e; - } - } -} diff --git a/packages/@ionic/cli/src/commands/capacitor/add.ts b/packages/@ionic/cli/src/commands/capacitor/add.ts deleted file mode 100644 index 8aac8f47b5..0000000000 --- a/packages/@ionic/cli/src/commands/capacitor/add.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { validators } from '@ionic/cli-framework'; - -import { CommandInstanceInfo, CommandLineInputs, CommandLineOptions, CommandMetadata, CommandPreRun } from '../../definitions'; -import { input } from '../../lib/color'; - -import { CapacitorCommand } from './base'; - -export class AddCommand extends CapacitorCommand implements CommandPreRun { - async getMetadata(): Promise { - return { - name: 'add', - type: 'project', - summary: 'Add a native platform to your Ionic project', - description: ` -${input('ionic capacitor add')} will do the following: -- Install the Capacitor platform package -- Copy the native platform template into your project - `, - inputs: [ - { - name: 'platform', - summary: `The platform to add (e.g. ${['android', 'ios'].map(v => input(v)).join(', ')})`, - validators: [validators.required], - }, - ], - }; - } - - async preRun(inputs: CommandLineInputs, options: CommandLineOptions, runinfo: CommandInstanceInfo): Promise { - await this.preRunChecks(runinfo); - - if (!inputs[0]) { - const platform = await this.env.prompt({ - type: 'list', - name: 'platform', - message: 'What platform would you like to add?', - choices: ['android', 'ios'], - }); - - inputs[0] = platform.trim(); - } - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - const [ platform ] = inputs; - - await this.installPlatform(platform); - } -} diff --git a/packages/@ionic/cli/src/commands/capacitor/base.ts b/packages/@ionic/cli/src/commands/capacitor/base.ts deleted file mode 100644 index 1698cc1e4d..0000000000 --- a/packages/@ionic/cli/src/commands/capacitor/base.ts +++ /dev/null @@ -1,329 +0,0 @@ -import { pathExists } from '@ionic/utils-fs'; -import { onBeforeExit } from '@ionic/utils-process'; -import { ERROR_COMMAND_NOT_FOUND, SubprocessError } from '@ionic/utils-subprocess'; -import * as lodash from 'lodash'; -import * as path from 'path'; -import * as semver from 'semver'; - -import { CommandInstanceInfo, CommandLineInputs, CommandLineOptions, IonicCapacitorOptions, ProjectIntegration } from '../../definitions'; -import { input, weak } from '../../lib/color'; -import { Command } from '../../lib/command'; -import { FatalException, RunnerException } from '../../lib/errors'; -import { runCommand } from '../../lib/executor'; -import type { CapacitorCLIConfig, Integration as CapacitorIntegration } from '../../lib/integrations/capacitor' -import { ANDROID_MANIFEST_FILE, CapacitorAndroidManifest } from '../../lib/integrations/capacitor/android'; -import { CAPACITOR_CONFIG_JSON_FILE, CapacitorJSONConfig, CapacitorConfig } from '../../lib/integrations/capacitor/config'; -import { CapacitorIosInfo, IOS_INFO_FILE } from '../../lib/integrations/capacitor/ios'; -import { generateOptionsForCapacitorBuild } from '../../lib/integrations/capacitor/utils'; -import { createPrefixedWriteStream } from '../../lib/utils/logger'; -import { pkgManagerArgs } from '../../lib/utils/npm'; - -export abstract class CapacitorCommand extends Command { - private _integration?: Required; - - get integration(): Required { - if (!this.project) { - throw new FatalException(`Cannot use Capacitor outside a project directory.`); - } - - if (!this._integration) { - this._integration = this.project.requireIntegration('capacitor'); - } - - return this._integration; - } - - async getGeneratedConfig(platform: string): Promise { - if (!this.project) { - throw new FatalException(`Cannot use Capacitor outside a project directory.`); - } - - const p = await this.getGeneratedConfigPath(platform); - - return new CapacitorJSONConfig(p); - } - - async getGeneratedConfigPath(platform: string): Promise { - if (!this.project) { - throw new FatalException(`Cannot use Capacitor outside a project directory.`); - } - - const p = await this.getGeneratedConfigDir(platform); - - return path.resolve(this.integration.root, p, CAPACITOR_CONFIG_JSON_FILE); - } - - async getAndroidManifest(): Promise { - const p = await this.getAndroidManifestPath(); - - return CapacitorAndroidManifest.load(p); - } - - async getAndroidManifestPath(): Promise { - const cli = await this.getCapacitorCLIConfig(); - const srcDir = cli?.android.srcMainDirAbs ?? 'android/app/src/main'; - - return path.resolve(this.integration.root, srcDir, ANDROID_MANIFEST_FILE); - } - - async getiOSAppInfo(): Promise { - const p = await this.getiOSAppInfoPath(); - return CapacitorIosInfo.load(p); - } - - async getiOSAppInfoPath(): Promise { - const cli = await this.getCapacitorCLIConfig(); - const srcDir = cli?.ios.nativeTargetDirAbs ?? 'ios/App/App'; - - return path.resolve(this.integration.root, srcDir, IOS_INFO_FILE); - } - - async getGeneratedConfigDir(platform: string): Promise { - const cli = await this.getCapacitorCLIConfig(); - - switch (platform) { - case 'android': - return cli?.android.assetsDirAbs ?? 'android/app/src/main/assets'; - case 'ios': - return cli?.ios.nativeTargetDirAbs ?? 'ios/App/App'; - } - - throw new FatalException(`Could not determine generated Capacitor config path for ${input(platform)} platform.`); - } - - async getCapacitorCLIConfig(): Promise { - const capacitor = await this.getCapacitorIntegration(); - - return capacitor.getCapacitorCLIConfig(); - } - - async getCapacitorConfig(): Promise { - const capacitor = await this.getCapacitorIntegration(); - - return capacitor.getCapacitorConfig(); - } - - getCapacitorIntegration = lodash.memoize(async (): Promise => { - if (!this.project) { - throw new FatalException(`Cannot use Capacitor outside a project directory.`); - } - - return this.project.createIntegration('capacitor'); - }); - - getCapacitorVersion = lodash.memoize(async (): Promise => { - try { - const proc = await this.env.shell.createSubprocess('capacitor', ['--version'], { cwd: this.integration.root }); - const version = semver.parse((await proc.output()).trim()); - - if (!version) { - throw new FatalException('Error while parsing Capacitor CLI version.'); - } - - return version; - } catch (e: any) { - if (e instanceof SubprocessError) { - if (e.code === ERROR_COMMAND_NOT_FOUND) { - throw new FatalException('Error while getting Capacitor CLI version. Is Capacitor installed?'); - } - - throw new FatalException('Error while getting Capacitor CLI version.\n' + (e.output ? e.output : e.code)); - } - - throw e; - } - }); - - isCorePlatform(platform: string): boolean { - const platforms = ['android', 'ios']; - return platforms.includes(platform); - } - - async getInstalledPlatforms(): Promise { - const cli = await this.getCapacitorCLIConfig(); - const androidPlatformDirAbs = cli?.android.platformDirAbs ?? path.resolve(this.integration.root, 'android'); - const iosPlatformDirAbs = cli?.ios.platformDirAbs ?? path.resolve(this.integration.root, 'ios'); - const platforms: string[] = []; - - if (await pathExists(androidPlatformDirAbs)) { - platforms.push('android'); - } - - if (await pathExists(iosPlatformDirAbs)) { - platforms.push('ios'); - } - - if (await pathExists(path.resolve(this.integration.root, 'electron'))) { - platforms.push('electron'); - } - - return platforms; - } - - async isPlatformInstalled(platform: string): Promise { - const platforms = await this.getInstalledPlatforms(); - - return platforms.includes(platform); - } - - async checkCapacitor(runinfo: CommandInstanceInfo) { - if (!this.project) { - throw new FatalException(`Cannot use Capacitor outside a project directory.`); - } - - const capacitor = this.project.getIntegration('capacitor'); - - if (!capacitor) { - await runCommand(runinfo, ['integrations', 'enable', 'capacitor']); - } - } - - async preRunChecks(runinfo: CommandInstanceInfo) { - await this.checkCapacitor(runinfo); - } - - async runCapacitor(argList: string[]) { - if (!this.project) { - throw new FatalException(`Cannot use Capacitor outside a project directory.`); - } - - const stream = createPrefixedWriteStream(this.env.log, weak(`[capacitor]`)); - - await this.env.shell.run('capacitor', argList, { stream, fatalOnNotFound: false, truncateErrorOutput: 5000, cwd: this.integration.root }); - } - - async runBuild(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - if (!this.project) { - throw new FatalException(`Cannot use Capacitor outside a project directory.`); - } - - const conf = await this.getCapacitorConfig(); - - if (conf?.server?.url) { - this.env.log.warn( - `Capacitor server URL is in use.\n` + - `This may result in unexpected behavior for this build, where an external server is used in the Web View instead of your app. This likely occurred because of ${input('--livereload')} usage in the past and the CLI improperly exiting without cleaning up.\n\n` + - `Delete the ${input('server')} key in the Capacitor config file if you did not intend to use an external server.` - ); - this.env.log.nl(); - } - - if (options['build']) { - try { - const runner = await this.project.requireBuildRunner(); - const runnerOpts = runner.createOptionsFromCommandLine(inputs, generateOptionsForCapacitorBuild(inputs, options)); - await runner.run(runnerOpts); - } catch (e: any) { - if (e instanceof RunnerException) { - throw new FatalException(e.message); - } - - throw e; - } - } - } - - async runServe(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - if (!this.project) { - throw new FatalException(`Cannot run ${input('ionic capacitor run')} outside a project directory.`); - } - - const [ platform ] = inputs; - - try { - const runner = await this.project.requireServeRunner(); - const runnerOpts = runner.createOptionsFromCommandLine(inputs, generateOptionsForCapacitorBuild(inputs, options)); - - let serverUrl = options['livereload-url'] ? String(options['livereload-url']) : undefined; - - if (!serverUrl) { - const details = await runner.run(runnerOpts); - serverUrl = `${details.protocol || 'http'}://${details.externalAddress}:${details.port}`; - } - - const conf = await this.getGeneratedConfig(platform); - - onBeforeExit(async () => { - conf.resetServerUrl(); - }); - - conf.setServerUrl(serverUrl); - - if (platform === 'android') { - const manifest = await this.getAndroidManifest(); - - onBeforeExit(async () => { - await manifest.reset(); - }); - - manifest.enableCleartextTraffic(); - await manifest.save(); - } - - if (platform === 'ios' && !options['external']) { - const appInfo = await this.getiOSAppInfo(); - - onBeforeExit(async () => { - await appInfo.reset(); - }) - - appInfo.disableAppTransportSecurity(); - await appInfo.save(); - } - } catch (e: any) { - if (e instanceof RunnerException) { - throw new FatalException(e.message); - } - - throw e; - } - } - - async checkForPlatformInstallation(platform: string) { - if (!this.project) { - throw new FatalException('Cannot use Capacitor outside a project directory.'); - } - - if (platform) { - const capacitor = this.project.getIntegration('capacitor'); - - if (!capacitor) { - throw new FatalException('Cannot check platform installations--Capacitor not yet integrated.'); - } - - if (!(await this.isPlatformInstalled(platform))) { - await this.installPlatform(platform); - } - } - } - - async installPlatform(platform: string): Promise { - const version = await this.getCapacitorVersion(); - const installedPlatforms = await this.getInstalledPlatforms(); - - if (installedPlatforms.includes(platform)) { - throw new FatalException(`The ${input(platform)} platform is already installed!`); - } - - if (semver.gte(version, '3.0.0-alpha.1')) { - if (this.isCorePlatform(platform)) { - const [ manager, ...managerArgs ] = await pkgManagerArgs(this.env.config.get('npmClient'), { command: 'install', pkg: `@capacitor/${platform}@${version}`, saveDev: false }); - await this.env.shell.run(manager, managerArgs, { cwd: this.integration.root }); - } - } - - await this.runCapacitor(['add', platform]); - } - - protected async createOptionsFromCommandLine(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - const separatedArgs = options['--']; - const verbose = !!options['verbose']; - const conf = await this.getCapacitorConfig(); - - return { - '--': separatedArgs ? separatedArgs : [], - verbose, - ...conf, - }; - } -} diff --git a/packages/@ionic/cli/src/commands/capacitor/build.ts b/packages/@ionic/cli/src/commands/capacitor/build.ts deleted file mode 100644 index 7d8634346b..0000000000 --- a/packages/@ionic/cli/src/commands/capacitor/build.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { BaseError, Footnote, MetadataGroup, validators } from '@ionic/cli-framework'; - -import { CapacitorBuildHookName, CommandInstanceInfo, CommandLineInputs, CommandLineOptions, CommandMetadata, CommandMetadataOption, CommandPreRun } from '../../definitions'; -import { input } from '../../lib/color'; -import { FatalException, RunnerException } from '../../lib/errors'; -import { Hook, HookDeps } from '../../lib/hooks'; -import { getNativeIDEForPlatform } from '../../lib/integrations/capacitor/utils'; - -import { CapacitorCommand } from './base'; - -export class BuildCommand extends CapacitorCommand implements CommandPreRun { - async getMetadata(): Promise { - const groups: string[] = [MetadataGroup.BETA]; - const exampleCommands = [ - '', - 'android', - 'ios', - ].sort(); - - const options: CommandMetadataOption[] = [ - // Build Options - { - name: 'build', - summary: 'Do not invoke Ionic build', - type: Boolean, - default: true, - }, - { - name: 'open', - summary: 'Do not invoke Capacitor open', - type: Boolean, - default: true, - }, - ]; - - const footnotes: Footnote[] = [ - { - id: 'capacitor-native-config-docs', - url: 'https://capacitorjs.com/docs/basics/configuring-your-app', - shortUrl: 'https://ion.link/capacitor-native-config-docs', - }, - { - id: 'capacitor-ios-config-docs', - url: 'https://capacitorjs.com/docs/ios/configuration', - shortUrl: 'https://ion.link/capacitor-ios-config-docs', - }, - { - id: 'capacitor-android-config-docs', - url: 'https://capacitorjs.com/docs/android/configuration', - shortUrl: 'https://ion.link/capacitor-android-config-docs', - }, - ]; - - const buildRunner = this.project && await this.project.getBuildRunner(); - - if (buildRunner) { - const libmetadata = await buildRunner.getCommandMetadata(); - groups.push(...libmetadata.groups || []); - options.push(...libmetadata.options || []); - footnotes.push(...libmetadata.footnotes || []); - } - - return { - name: 'build', - type: 'project', - summary: 'Build an Ionic project for a given platform', - description: ` -${input('ionic capacitor build')} will do the following: -- Perform ${input('ionic build')} -- Copy web assets into the specified native platform -- Open the IDE for your native project (Xcode for iOS, Android Studio for Android) - -Once the web assets and configuration are copied into your native project, you can build your app using the native IDE. Unfortunately, programmatically building the native project is not yet supported. - -To configure your native project, see the common configuration docs[^capacitor-native-config-docs] as well as low-level configuration for iOS[^capacitor-ios-config-docs] and Android[^capacitor-android-config-docs]. - `, - footnotes, - exampleCommands, - inputs: [ - { - name: 'platform', - summary: `The platform to build for (e.g. ${['android', 'ios'].map(v => input(v)).join(', ')})`, - validators: [validators.required], - }, - ], - options, - groups, - }; - } - - async preRun(inputs: CommandLineInputs, options: CommandLineOptions, runinfo: CommandInstanceInfo): Promise { - await this.preRunChecks(runinfo); - - if (!inputs[0]) { - const platform = await this.env.prompt({ - type: 'list', - name: 'platform', - message: 'What platform would you like to build for?', - choices: ['android', 'ios'], - }); - - inputs[0] = platform.trim(); - } - - await this.checkForPlatformInstallation(inputs[0]); - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - if (!this.project) { - throw new FatalException(`Cannot run ${input('ionic capacitor build')} outside a project directory.`); - } - - const [ platform ] = inputs; - - try { - await this.runBuild(inputs, options); - } catch (e: any) { - if (e instanceof RunnerException) { - throw new FatalException(e.message); - } - - throw e; - } - - await this.runCapacitor(['sync', platform]); - - const hookDeps: HookDeps = { - config: this.env.config, - project: this.project, - shell: this.env.shell, - }; - - await this.runCapacitorBuildHook('capacitor:build:before', inputs, options, hookDeps); - - if (options['open']) { - this.env.log.nl(); - this.env.log.info(this.getContinueMessage(platform)); - this.env.log.nl(); - - await this.runCapacitor(['open', platform]); - } - } - - protected getContinueMessage(platform: string): string { - if (platform === 'electron') { - return 'Ready to be used in Electron!'; - } - - return ( - 'Ready for use in your Native IDE!\n' + - `To continue, build your project using ${getNativeIDEForPlatform(platform)}!` - ); - } - - private async runCapacitorBuildHook(name: CapacitorBuildHookName, inputs: CommandLineInputs, options: CommandLineOptions, e: HookDeps): Promise { - const hook = new CapacitorBuildHook(name, e); - const buildRunner = await e.project.requireBuildRunner(); - - try { - await hook.run({ - name: hook.name, - build: buildRunner.createOptionsFromCommandLine(inputs, options), - capacitor: await this.createOptionsFromCommandLine(inputs, options), - }); - } catch (e: any) { - if (e instanceof BaseError) { - throw new FatalException(e.message); - } - - throw e; - } - } -} - -class CapacitorBuildHook extends Hook { - readonly name: CapacitorBuildHookName; - - constructor(name: CapacitorBuildHookName, e: HookDeps) { - super(e); - - this.name = name; - } -} diff --git a/packages/@ionic/cli/src/commands/capacitor/copy.ts b/packages/@ionic/cli/src/commands/capacitor/copy.ts deleted file mode 100644 index b9af544437..0000000000 --- a/packages/@ionic/cli/src/commands/capacitor/copy.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { CommandMetadataOption } from '@ionic/cli-framework'; - -import { CommandInstanceInfo, CommandLineInputs, CommandLineOptions, CommandMetadata, CommandPreRun } from '../../definitions'; -import { input } from '../../lib/color'; - -import { CapacitorCommand } from './base'; -import * as semver from "semver"; - -export class CopyCommand extends CapacitorCommand implements CommandPreRun { - async getMetadata(): Promise { - const options: CommandMetadataOption[] = [ - { - name: 'build', - summary: 'Do not invoke an Ionic build', - type: Boolean, - default: true, - }, - { - name: 'inline', - summary: 'Use inline source maps (only available on capacitor 4.2.0+)', - type: Boolean, - default: false - } - ]; - - const runner = this.project && await this.project.getBuildRunner(); - - if (runner) { - const libmetadata = await runner.getCommandMetadata(); - options.push(...libmetadata.options || []); - } - - return { - name: 'copy', - type: 'project', - summary: 'Copy web assets to native platforms', - description: ` -${input('ionic capacitor copy')} will do the following: -- Perform an Ionic build, which compiles web assets -- Copy web assets to Capacitor native platform(s) - `, - inputs: [ - { - name: 'platform', - summary: `The platform to copy (e.g. ${['android', 'ios'].map(v => input(v)).join(', ')})`, - }, - ], - options, - }; - } - - async preRun(inputs: CommandLineInputs, options: CommandLineOptions, runinfo: CommandInstanceInfo): Promise { - await this.preRunChecks(runinfo); - - if (inputs[0]) { - await this.checkForPlatformInstallation(inputs[0]); - } - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - const [ platform ] = inputs; - - if (options.build) { - await this.runBuild(inputs, options); - } - - const args = ['copy']; - - const capVersion = await this.getCapacitorVersion(); - if(semver.gte(capVersion, "4.2.0") && options.inline) { - args.push("--inline") - } - - if (platform) { - args.push(platform); - } - - await this.runCapacitor(args); - } -} diff --git a/packages/@ionic/cli/src/commands/capacitor/index.ts b/packages/@ionic/cli/src/commands/capacitor/index.ts deleted file mode 100644 index 977fa4e450..0000000000 --- a/packages/@ionic/cli/src/commands/capacitor/index.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { strong } from '../../lib/color'; -import { CommandMap, Namespace } from '../../lib/namespace'; - -export class CapacitorNamespace extends Namespace { - async getMetadata() { - return { - name: 'capacitor', - summary: 'Capacitor functionality', - description: ` -These commands integrate with Capacitor, Ionic's new native layer project which provides an alternative to Cordova for native functionality in your app. - -Learn more about Capacitor: -- Main documentation: ${strong('https://ion.link/capacitor')} - `, - }; - } - - async getCommands(): Promise { - return new CommandMap([ - ['add', async () => { const { AddCommand } = await import('./add'); return new AddCommand(this); }], - ['build', async () => { const { BuildCommand } = await import('./build'); return new BuildCommand(this); }], - ['copy', async () => { const { CopyCommand } = await import('./copy'); return new CopyCommand(this); }], - ['open', async () => { const { OpenCommand } = await import('./open'); return new OpenCommand(this); }], - ['run', async () => { const { RunCommand } = await import('./run'); return new RunCommand(this); }], - ['sync', async () => { const { SyncCommand } = await import('./sync'); return new SyncCommand(this); }], - ['update', async () => { const { UpdateCommand } = await import('./update'); return new UpdateCommand(this); }], - ]); - } -} diff --git a/packages/@ionic/cli/src/commands/capacitor/open.ts b/packages/@ionic/cli/src/commands/capacitor/open.ts deleted file mode 100644 index a128ebb363..0000000000 --- a/packages/@ionic/cli/src/commands/capacitor/open.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { validators } from '@ionic/cli-framework'; - -import { CommandInstanceInfo, CommandLineInputs, CommandLineOptions, CommandMetadata, CommandPreRun } from '../../definitions'; -import { input } from '../../lib/color'; - -import { CapacitorCommand } from './base'; - -export class OpenCommand extends CapacitorCommand implements CommandPreRun { - async getMetadata(): Promise { - return { - name: 'open', - type: 'project', - summary: 'Open the IDE for a given native platform project', - description: ` -${input('ionic capacitor open')} will do the following: -- Open the IDE for your native project (Xcode for iOS, Android Studio for Android) - `, - inputs: [ - { - name: 'platform', - summary: `The platform to open (e.g. ${['android', 'ios'].map(v => input(v)).join(', ')})`, - validators: [validators.required], - }, - ], - }; - } - - async preRun(inputs: CommandLineInputs, options: CommandLineOptions, runinfo: CommandInstanceInfo): Promise { - await this.preRunChecks(runinfo); - - if (!inputs[0]) { - const platform = await this.env.prompt({ - type: 'list', - name: 'platform', - message: 'What platform would you like to open?', - choices: ['android', 'ios'], - }); - - inputs[0] = platform.trim(); - } - - await this.checkForPlatformInstallation(inputs[0]); - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - const [ platform ] = inputs; - const args = ['open']; - - if (platform) { - args.push(platform); - } - - await this.runCapacitor(args); - } -} diff --git a/packages/@ionic/cli/src/commands/capacitor/run.ts b/packages/@ionic/cli/src/commands/capacitor/run.ts deleted file mode 100644 index 0398a6e902..0000000000 --- a/packages/@ionic/cli/src/commands/capacitor/run.ts +++ /dev/null @@ -1,375 +0,0 @@ -import { BaseError, Footnote, validators } from '@ionic/cli-framework'; -import { sleepForever } from '@ionic/utils-process'; -import { columnar } from '@ionic/utils-terminal'; -import chalk from 'chalk'; -import { debug as Debug } from 'debug'; -import * as lodash from 'lodash'; -import * as semver from 'semver'; - -import { COLUMNAR_OPTIONS } from '../../constants'; -import { AnyBuildOptions, AnyServeOptions, CapacitorRunHookName, CommandInstanceInfo, CommandLineInputs, CommandLineOptions, CommandMetadata, CommandMetadataOption, CommandPreRun } from '../../definitions'; -import { ancillary, input, strong, weak } from '../../lib/color'; -import { FatalException } from '../../lib/errors'; -import { Hook, HookDeps } from '../../lib/hooks'; -import { getNativeIDEForPlatform, getVirtualDeviceNameForPlatform } from '../../lib/integrations/capacitor/utils'; -import { COMMON_SERVE_COMMAND_OPTIONS } from '../../lib/serve'; - -import { CapacitorCommand } from './base'; - -const debug = Debug('ionic:commands:capacitor:run'); - -const PLATFORMS = ['android', 'ios'] as const; - -interface NativeTarget { - id: string; - name: string; - api: string; -} - -export class RunCommand extends CapacitorCommand implements CommandPreRun { - async getMetadata(): Promise { - const groups: string[] = []; - const exampleCommands = [ - '', - 'android', - 'android -l --external', - 'ios --livereload --external', - 'ios --livereload-url=http://localhost:8100', - ].sort(); - - let options: CommandMetadataOption[] = [ - { - name: 'list', - summary: 'List all available targets', - type: Boolean, - groups: ['capacitor', 'native-run'], - hint: weak('[capacitor]'), - }, - { - name: 'target', - summary: `Deploy to a specific device by its ID (use ${input('--list')} to see all)`, - type: String, - groups: ['capacitor', 'native-run'], - hint: weak('[capacitor]'), - }, - { - name: 'open', - summary: `Open native IDE instead of using ${input('capacitor run')}`, - type: Boolean, - }, - // Build Options - { - name: 'build', - summary: 'Do not invoke Ionic build', - type: Boolean, - default: true, - }, - // Serve Options - ...COMMON_SERVE_COMMAND_OPTIONS.filter(o => !['livereload'].includes(o.name)).map(o => ({ ...o, hint: weak('(--livereload)') })), - { - name: 'livereload', - summary: 'Spin up dev server to live-reload www files', - type: Boolean, - aliases: ['l'], - }, - { - name: 'livereload-url', - summary: 'Provide a custom URL to the dev server', - spec: { value: 'url' }, - }, - ]; - - const footnotes: Footnote[] = [ - { - id: 'remote-debugging-docs', - url: 'https://ionicframework.com/docs/developer-resources/developer-tips', - shortUrl: 'https://ion.link/remote-debugging-docs', - }, - { - id: 'livereload-docs', - url: 'https://ionicframework.com/docs/cli/livereload', - shortUrl: 'https://ion.link/livereload-docs', - }, - ]; - - const buildRunner = this.project && await this.project.getBuildRunner(); - - if (buildRunner) { - const libmetadata = await buildRunner.getCommandMetadata(); - groups.push(...libmetadata.groups || []); - options.push(...libmetadata.options || []); - footnotes.push(...libmetadata.footnotes || []); - } - - return { - name: 'run', - type: 'project', - summary: 'Run an Ionic project on a connected device', - description: ` -${input('ionic capacitor run')} will do the following: -- Perform ${input('ionic build')} (or run the dev server from ${input('ionic serve')} with the ${input('--livereload')} option) -- Run ${input('capacitor run')} (or open IDE for your native project with the ${input('--open')} option) - -When using ${input('--livereload')} with hardware devices, remember that livereload needs an active connection between device and computer. In some scenarios, you may need to host the dev server on an external address using the ${input('--external')} option. See these docs[^livereload-docs] for more information. - -If you have multiple devices and emulators, you can target a specific one by ID with the ${input('--target')} option. You can list targets with ${input('--list')}. - -For Android and iOS, you can setup Remote Debugging on your device with browser development tools using these docs[^remote-debugging-docs]. - `, - footnotes, - exampleCommands, - inputs: [ - { - name: 'platform', - summary: `The platform to run (e.g. ${PLATFORMS.map(v => input(v)).join(', ')})`, - validators: [validators.required], - }, - ], - options, - groups, - }; - } - - isOldCapacitor = lodash.memoize(async (): Promise => { - const version = await this.getCapacitorVersion(); - return semver.lt(version, '3.0.0-alpha.7'); - }); - - async preRun(inputs: CommandLineInputs, options: CommandLineOptions, runinfo: CommandInstanceInfo): Promise { - await this.preRunChecks(runinfo); - - if (await this.isOldCapacitor()) { - this.env.log.warn( - `Support for Capacitor 1 and 2 is deprecated.\n` + - `Please update to the latest Capacitor. Visit the docs${ancillary('[1]')} for upgrade guides.\n\n` + - `${ancillary('[1]')}: ${strong('https://capacitorjs.com/docs/')}\n` - ); - - if (options['list']) { - throw new FatalException(`The ${input('--list')} option is not supported with your Capacitor version.`); - } - - if (options['target']) { - throw new FatalException(`The ${input('--target')} option is not supported with your Capacitor version.`); - } - } - - if (!inputs[0]) { - const platform = await this.env.prompt({ - type: 'list', - name: 'platform', - message: options['list'] ? 'What platform targets would you like to list?' : 'What platform would you like to run?', - choices: PLATFORMS, - }); - - inputs[0] = platform.trim(); - } - - if (options['livereload-url']) { - options['livereload'] = true; - } - - if (!options['build'] && options['livereload']) { - this.env.log.warn(`No livereload with ${input('--no-build')}.`); - options['livereload'] = false; - } - - await this.checkForPlatformInstallation(inputs[0]); - - if (!(await this.isOldCapacitor())) { - const targets = inputs[0] ? await this.getNativeTargets(inputs[0]) : []; - - if (options['list']) { - if (!inputs[0]) { - throw new FatalException(`The ${input('platform')} argument is required for the ${input('--list')} option.`); - } - - if (targets.length > 0) { - const rows = targets.map(t => [t.name, t.api, t.id]); - this.env.log.msg(columnar(rows, { ...COLUMNAR_OPTIONS, headers: ['Name', 'API', 'Target ID'] })); - } else { - this.env.log.info('No native targets found.'); - } - - throw new FatalException('', 0); - } - - if (!options['open']) { - const target = options['target']; - - if (typeof target === 'string') { - if (!targets.map(t => t.id).find(t => t === target)) { - throw new FatalException( - `${input(target)} is not a valid Target ID.\n` + - `Use the ${input('--list')} option to list all targets.\n` - ); - } - } else { - if (targets.length > 0) { - options['target'] = await this.env.prompt({ - type: 'list', - name: 'target', - message: 'Which device would you like to target?', - choices: targets.map(t => ({ name: `${t.name} (${t.id})`, value: t.id })), - }); - } else { - throw new FatalException(`No devices or emulators found`); - } - - if (!inputs[0]) { - throw new FatalException(`The ${input('platform')} argument is required.`); - } - - if (!options['target']) { - throw new FatalException(`The ${input('--target')} option is required.`); - } - } - } - } - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - if (!this.project) { - throw new FatalException(`Cannot run ${input('ionic capacitor run')} outside a project directory.`); - } - - const [platform] = inputs; - - const doLiveReload = !!options['livereload']; - const doOpenFlow = (await this.isOldCapacitor()) || options['open'] === true; - - if (doLiveReload) { - await this.runCapacitor(['sync', platform]); - await this.runServe(inputs, options); - - if (doOpenFlow) { - await this.runCapacitorOpenFlow(inputs, options); - - this.env.log.nl(); - this.env.log.info( - 'Development server will continue running until manually stopped.\n' + - chalk.yellow('Use Ctrl+C to quit this process') - ); - } else { - await this.runCapacitorRunFlow(inputs, options, { shouldSync: false }); - - this.env.log.nl(); - this.env.log.info( - `App deployed to device!\n` + - 'Development server will continue running until manually stopped.\n\n' + - chalk.yellow('Use Ctrl+C to quit this process') - ); - } - - await sleepForever(); - } else { - await this.runBuild(inputs, options); - - if (doOpenFlow) { - await this.runCapacitor(['sync', platform]); - await this.runCapacitorOpenFlow(inputs, options); - } else { - await this.runCapacitorRunFlow(inputs, options, { shouldSync: true }); - } - } - } - - protected getNativeTargets = lodash.memoize(async (platform: string): Promise => { - const args = ['run', platform, '--list', '--json']; - - debug('Getting native targets for %O with Capacitor CLI: %O', platform, args); - - const output = await this.env.shell.cmdinfo('capacitor', args, { cwd: this.integration.root }); - - if (!output) { - return []; - } - - const targets = JSON.parse(output); - - debug('%O native targets found', targets.length); - - return targets; - }); - - protected async runCapacitorOpenFlow(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - if (!this.project) { - throw new FatalException(`Cannot run ${input('ionic capacitor run')} outside a project directory.`); - } - - const [platform] = inputs; - - await this.runCapacitorRunHook('capacitor:run:before', inputs, options, { ...this.env, project: this.project }); - - if (options['open'] !== false) { - this.env.log.nl(); - this.env.log.info(this.getContinueMessage(platform)); - this.env.log.nl(); - - await this.runCapacitor(['open', platform]); - } - } - - protected getContinueMessage(platform: string): string { - if (platform === 'electron') { - return 'Ready to be used in Electron!'; - } - - return ( - 'Ready for use in your Native IDE!\n' + - `To continue, run your project on a device or ${getVirtualDeviceNameForPlatform(platform)} using ${getNativeIDEForPlatform(platform)}!` - ); - } - - protected async runCapacitorRunFlow(inputs: CommandLineInputs, options: CommandLineOptions, { shouldSync = true }: { shouldSync?: boolean } = {}): Promise { - if (!this.project) { - throw new FatalException(`Cannot run ${input('ionic capacitor run')} outside a project directory.`); - } - - const [platform] = inputs; - - await this.runCapacitorRunHook('capacitor:run:before', inputs, options, { ...this.env, project: this.project }); - await this.runCapacitor(['run', platform, ...(shouldSync ? [] : ['--no-sync']), '--target', String(options['target'])]); - } - - protected async runCapacitorRunHook(name: CapacitorRunHookName, inputs: CommandLineInputs, options: CommandLineOptions, e: HookDeps): Promise { - const hook = new CapacitorRunHook(name, e); - let serveOptions: AnyServeOptions | undefined; - let buildOptions: AnyBuildOptions | undefined; - - if (options['livereload']) { - const serveRunner = await e.project.requireServeRunner(); - - serveOptions = serveRunner.createOptionsFromCommandLine(inputs, options); - } else { - const buildRunner = await e.project.requireBuildRunner(); - - buildOptions = buildRunner.createOptionsFromCommandLine(inputs, options); - } - - try { - await hook.run({ - name: hook.name, - serve: serveOptions, - build: buildOptions, - capacitor: await this.createOptionsFromCommandLine(inputs, options), - }); - } catch (e: any) { - if (e instanceof BaseError) { - throw new FatalException(e.message); - } - - throw e; - } - } -} - -class CapacitorRunHook extends Hook { - readonly name: CapacitorRunHookName; - - constructor(name: CapacitorRunHookName, e: HookDeps) { - super(e); - - this.name = name; - } -} diff --git a/packages/@ionic/cli/src/commands/capacitor/sync.ts b/packages/@ionic/cli/src/commands/capacitor/sync.ts deleted file mode 100644 index e04ebe6091..0000000000 --- a/packages/@ionic/cli/src/commands/capacitor/sync.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { BaseError, CommandMetadataOption } from '@ionic/cli-framework'; - -import { CapacitorSyncHookName, CommandInstanceInfo, CommandLineInputs, CommandLineOptions, CommandMetadata, CommandPreRun } from '../../definitions'; -import { input } from '../../lib/color'; -import { FatalException } from '../../lib/errors'; -import { Hook, HookDeps } from '../../lib/hooks'; - -import { CapacitorCommand } from './base'; -import * as semver from 'semver'; - -export class SyncCommand extends CapacitorCommand implements CommandPreRun { - async getMetadata(): Promise { - const options: CommandMetadataOption[] = [ - { - name: 'build', - summary: 'Do not invoke an Ionic build', - type: Boolean, - default: true, - }, - { - name: 'inline', - summary: 'Use inline source maps (only available on capacitor 4.1.0+)', - type: Boolean, - default: false - } - ]; - - const runner = this.project && await this.project.getBuildRunner(); - - if (runner) { - const libmetadata = await runner.getCommandMetadata(); - options.push(...libmetadata.options || []); - } - - return { - name: 'sync', - type: 'project', - summary: 'Sync (copy + update) an Ionic project', - description: ` -${input('ionic capacitor sync')} will do the following: -- Perform an Ionic build, which compiles web assets -- Copy web assets to Capacitor native platform(s) -- Update Capacitor native platform(s) and dependencies -- Install any discovered Capacitor or Cordova plugins - `, - inputs: [ - { - name: 'platform', - summary: `The platform to sync (e.g. ${['android', 'ios'].map(v => input(v)).join(', ')})`, - }, - ], - options, - }; - } - - async preRun(inputs: CommandLineInputs, options: CommandLineOptions, runinfo: CommandInstanceInfo): Promise { - await this.preRunChecks(runinfo); - - if (inputs[0]) { - await this.checkForPlatformInstallation(inputs[0]); - } - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - if (!this.project) { - throw new FatalException(`Cannot run ${input('ionic capacitor sync')} outside a project directory.`); - } - - const [ platform ] = inputs; - - if (options.build) { - await this.runBuild(inputs, options); - } - - const args = ['sync']; - - const capVersion = await this.getCapacitorVersion(); - if(semver.gte(capVersion, "4.1.0") && options.inline) { - args.push("--inline") - } - - if (platform) { - args.push(platform); - } - - await this.runCapacitor(args); - - const hookDeps: HookDeps = { - config: this.env.config, - project: this.project, - shell: this.env.shell, - }; - - await this.runCapacitorSyncHook('capacitor:sync:after', inputs, options, hookDeps); - } - - private async runCapacitorSyncHook(name: CapacitorSyncHookName, inputs: CommandLineInputs, options: CommandLineOptions, e: HookDeps): Promise { - const hook = new CapacitorSyncHook(name, e); - const buildRunner = await e.project.requireBuildRunner(); - - try { - await hook.run({ - name: hook.name, - build: buildRunner.createOptionsFromCommandLine(inputs, options), - capacitor: await this.createOptionsFromCommandLine(inputs, options), - }); - } catch (e: any) { - if (e instanceof BaseError) { - throw new FatalException(e.message); - } - - throw e; - } - } -} - -class CapacitorSyncHook extends Hook { - readonly name: CapacitorSyncHookName; - - constructor(name: CapacitorSyncHookName, e: HookDeps) { - super(e); - - this.name = name; - } -} diff --git a/packages/@ionic/cli/src/commands/capacitor/update.ts b/packages/@ionic/cli/src/commands/capacitor/update.ts deleted file mode 100644 index 6895a9a4ee..0000000000 --- a/packages/@ionic/cli/src/commands/capacitor/update.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { CommandInstanceInfo, CommandLineInputs, CommandLineOptions, CommandMetadata, CommandPreRun } from '../../definitions'; -import { input } from '../../lib/color'; - -import { CapacitorCommand } from './base'; - -export class UpdateCommand extends CapacitorCommand implements CommandPreRun { - async getMetadata(): Promise { - return { - name: 'update', - type: 'project', - summary: 'Update Capacitor native platforms, install Capacitor/Cordova plugins', - description: ` -${input('ionic capacitor update')} will do the following: -- Update Capacitor native platform(s) and dependencies -- Install any discovered Capacitor or Cordova plugins - `, - inputs: [ - { - name: 'platform', - summary: `The platform to update (e.g. ${['android', 'ios'].map(v => input(v)).join(', ')})`, - }, - ], - }; - } - - async preRun(inputs: CommandLineInputs, options: CommandLineOptions, runinfo: CommandInstanceInfo): Promise { - await this.preRunChecks(runinfo); - - if (inputs[0]) { - await this.checkForPlatformInstallation(inputs[0]); - } - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - const [ platform ] = inputs; - const args = ['update']; - - if (platform) { - args.push(platform); - } - - await this.runCapacitor(args); - } -} diff --git a/packages/@ionic/cli/src/commands/completion.ts b/packages/@ionic/cli/src/commands/completion.ts deleted file mode 100644 index e656254216..0000000000 --- a/packages/@ionic/cli/src/commands/completion.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { MetadataGroup, ZshCompletionFormatter, getCompletionWords } from '@ionic/cli-framework'; -import { TERMINAL_INFO } from '@ionic/utils-terminal'; -import * as path from 'path'; - -import { CommandLineInputs, CommandLineOptions, CommandMetadata } from '../definitions'; -import { strong } from '../lib/color'; -import { Command } from '../lib/command'; -import { FatalException } from '../lib/errors'; - -export class CompletionCommand extends Command { - async getMetadata(): Promise { - return { - name: 'completion', - type: 'global', - summary: 'Enables tab-completion for Ionic CLI commands.', - description: ` -This command is experimental and only works for Z shell (zsh) and non-Windows platforms. - -To enable completions for the Ionic CLI, you can add the completion code that this command prints to your ${strong('~/.zshrc')} (or any other file loaded with your shell). See the examples. - `, - groups: [MetadataGroup.EXPERIMENTAL], - exampleCommands: [ - '', - '>> ~/.zshrc', - ], - }; - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - if (TERMINAL_INFO.windows) { - throw new FatalException('Completion is not supported on Windows shells.'); - } - - if (path.basename(TERMINAL_INFO.shell) !== 'zsh') { - throw new FatalException('Completion is currently only available for Z Shell (zsh).'); - } - - const words = options['--']; - - if (!words || words.length === 0) { - const namespace = this.namespace.root; - const formatter = new ZshCompletionFormatter({ namespace }); - - process.stdout.write(await formatter.format()); - - return; - } - - const ns = this.namespace.root; - const outputWords = await getCompletionWords(ns, words.slice(1)); - - process.stdout.write(outputWords.join(' ')); - } -} diff --git a/packages/@ionic/cli/src/commands/config/base.ts b/packages/@ionic/cli/src/commands/config/base.ts deleted file mode 100644 index 595a98062d..0000000000 --- a/packages/@ionic/cli/src/commands/config/base.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { BaseConfig } from '@ionic/cli-framework'; -import * as lodash from 'lodash'; -import * as util from 'util'; - -import { CommandLineInputs, CommandLineOptions, IConfig, IProject } from '../../definitions'; -import { failure, input, strong } from '../../lib/color'; -import { Command } from '../../lib/command'; -import { FatalException } from '../../lib/errors'; - -export interface BaseConfigContext { - json: boolean; - force: boolean; - root: boolean; - property?: string; - value?: any; -} - -export interface GlobalConfigContext extends BaseConfigContext { - global: true; - config: IConfig; -} - -export interface ProjectConfigContext extends BaseConfigContext { - global: false; - config: IProject['config']; -} - -export type ConfigContext = GlobalConfigContext | ProjectConfigContext; - -export abstract class BaseConfigCommand extends Command { - generateContext(inputs: CommandLineInputs, options: CommandLineOptions): ConfigContext { - const [ property, v ] = inputs; - const global = options['global'] ? true : false; - const json = options['json'] ? true : false; - const force = options['force'] ? true : false; - const root = options['root'] ? true : false; - const value = this.interpretValue(v, json); - const base: BaseConfigContext = { json, property, value, force, root }; - - if (global) { - if (root) { - this.env.log.warn(`${input('--root')} has no effect with ${input('--global')}: this command always operates at root for CLI config.`); - } - - return { global, config: this.env.config, ...base }; - } else { - if (!this.project) { - throw new FatalException( - `Sorry--this won't work outside an Ionic project directory.\n` + - `Did you mean to operate on global config using ${input('--global')}?` - ); - } - - return { global, config: this.project.config, ...base }; - } - } - - jsonStringify(v: any): string { - try { - const serialized = JSON.stringify(v); - - if (typeof serialized === 'undefined') { - throw new FatalException(`Cannot serialize value: ${strong(v)}`); - } - - return serialized; - } catch (e: any) { - throw new FatalException(`Cannot serialize value: ${strong(v)}`); - } - } - - interpretValue(v?: string, expectJson = false): any { - if (typeof v === 'undefined') { - return; - } - - try { - // '12345e6' (a possible App ID) is interpreted as a number in scientific - // notation during JSON.parse, so don't try - if (!v.match(/^\d+e\d+$/)) { - v = JSON.parse(v); - } - } catch (e: any) { - if (e.name !== 'SyntaxError') { - throw e; - } - - if (expectJson) { - throw new FatalException(`${input('--json')}: ${input(String(v))} is invalid JSON: ${failure(e.toString())}`); - } - } - - return v; - } -} - -interface FlexibleConfigFile { [key: string]: any; } - -class FlexibleConfig extends BaseConfig { - provideDefaults() { - return {}; - } -} - -export function getConfig(ctx: ConfigContext): FlexibleConfigFile { - return ctx.root ? new FlexibleConfig(ctx.config.p) : ctx.config; -} - -export function getConfigValue(ctx: ConfigContext): any { - const { c } = getConfig(ctx); - - if (ctx.global) { // Global config is flattened - return ctx.property ? c[ctx.property] : c; - } else { - return ctx.property ? lodash.get(c, ctx.property) : c; - } -} - -export function setConfigValue(ctx: ConfigContext & { property: string; originalValue: any; }): void { - const conf = getConfig(ctx); - - if (ctx.originalValue && typeof ctx.originalValue === 'object' && !ctx.force) { - throw new FatalException( - `Sorry--will not override objects or arrays without ${input('--force')}.\n` + - `Value of ${input(ctx.property)} is: ${strong(util.inspect(ctx.originalValue, { colors: false }))}` - ); - } - - if (ctx.global) { // Global config is flattened - conf.set(ctx.property, ctx.value); - } else { - const { c } = conf; - lodash.set(c, ctx.property, ctx.value); - conf.c = c; - } -} - -export function unsetConfigValue(ctx: ConfigContext & { property: string; }): void { - const conf = getConfig(ctx); - - if (ctx.global) { // Global config is flattened - conf.unset(ctx.property); - } else { - const { c } = conf; - lodash.unset(c, ctx.property); - conf.c = c; - } -} diff --git a/packages/@ionic/cli/src/commands/config/get.ts b/packages/@ionic/cli/src/commands/config/get.ts deleted file mode 100644 index 6a814a757f..0000000000 --- a/packages/@ionic/cli/src/commands/config/get.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { MetadataGroup } from '@ionic/cli-framework'; -import { strcmp } from '@ionic/cli-framework/utils/string'; -import { columnar, prettyPath } from '@ionic/utils-terminal'; -import chalk from 'chalk'; -import * as lodash from 'lodash'; -import * as util from 'util'; - -import { COLUMNAR_OPTIONS, PROJECT_FILE } from '../../constants'; -import { CommandLineInputs, CommandLineOptions, CommandMetadata } from '../../definitions'; -import { input, strong, weak } from '../../lib/color'; - -import { BaseConfigCommand, ConfigContext, getConfigValue } from './base'; - -export class ConfigGetCommand extends BaseConfigCommand { - async getMetadata(): Promise { - const projectFile = this.project ? prettyPath(this.project.filePath) : PROJECT_FILE; - - return { - name: 'get', - type: 'global', - summary: 'Print config values', - description: ` -This command reads and prints configuration values from the project's ${strong(projectFile)} file. It can also operate on the global CLI configuration (${strong('~/.ionic/config.json')}) using the ${input('--global')} option. - -For nested properties, separate nest levels with dots. For example, the property name ${input('integrations.cordova')} will look in the ${strong('integrations')} object for the ${strong('cordova')} property. - -Without a ${input('property')} argument, this command prints out the entire config. - -For multi-app projects, this command is scoped to the current project by default. To operate at the root of the project configuration file instead, use the ${input('--root')} option. - -If you are using this command programmatically, you can use the ${input('--json')} option. - -This command will sanitize config output for known sensitive fields (disabled when using ${input('--json')}). - `, - inputs: [ - { - name: 'property', - summary: 'The property name you wish to get', - }, - ], - options: [ - { - name: 'global', - summary: 'Use global CLI config', - type: Boolean, - aliases: ['g'], - }, - { - name: 'json', - summary: 'Output config values in JSON', - type: Boolean, - groups: [MetadataGroup.ADVANCED], - }, - { - name: 'root', - summary: `Operate on root of ${strong(projectFile)}`, - type: Boolean, - hint: weak('[multi-app]'), - groups: [MetadataGroup.ADVANCED], - }, - ], - exampleCommands: ['', 'id', '--global user.email', '-g npmClient'], - }; - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - const ctx = this.generateContext(inputs, options); - const conf = getConfigValue(ctx); - - this.printConfig(ctx, conf); - } - - printConfig(ctx: ConfigContext, v: any): void { - const { global, json } = ctx; - - if (json) { - process.stdout.write(this.jsonStringify(v)); - } else { - if (global && v && typeof v === 'object') { - const columns = lodash.entries(v) - .map(([key, value]) => [key, this.sanitizeEntry(key, value)]) - .map(([key, value]) => [strong(key), util.inspect(value, { colors: chalk.level > 0 })]); - - columns.sort((a, b) => strcmp(a[0], b[0])); - - this.env.log.rawmsg(columnar(columns, COLUMNAR_OPTIONS)); - } else { - this.env.log.rawmsg(util.inspect(v, { depth: Infinity, colors: chalk.level > 0 })); - } - } - } - - sanitizeEntry(key: string, value: any): typeof value { - if (key.includes('tokens')) { - return '*****'; - } - - return value; - } -} diff --git a/packages/@ionic/cli/src/commands/config/index.ts b/packages/@ionic/cli/src/commands/config/index.ts deleted file mode 100644 index ca1aba0fb8..0000000000 --- a/packages/@ionic/cli/src/commands/config/index.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { prettyPath } from '@ionic/utils-terminal'; - -import { PROJECT_FILE } from '../../constants'; -import { input, strong } from '../../lib/color'; -import { CommandMap, Namespace } from '../../lib/namespace'; - -export class ConfigNamespace extends Namespace { - async getMetadata() { - const projectFile = this.project ? prettyPath(this.project.filePath) : PROJECT_FILE; - - return { - name: 'config', - summary: 'Manage CLI and project config values', - description: ` -These commands are used to programmatically read, write, and delete CLI and project config values. - -By default, these commands use your project's ${strong(prettyPath(projectFile))} file. - -To use these commands for the global CLI config file (${strong('~/.ionic/config.json')}), use the ${input('--global')} flag. - `, - }; - } - - async getCommands(): Promise { - return new CommandMap([ - ['get', async () => { const { ConfigGetCommand } = await import('./get'); return new ConfigGetCommand(this); }], - ['set', async () => { const { ConfigSetCommand } = await import('./set'); return new ConfigSetCommand(this); }], - ['unset', async () => { const { ConfigUnsetCommand } = await import('./unset'); return new ConfigUnsetCommand(this); }], - ['delete', 'unset'], - ['del', 'unset'], - ]); - } -} diff --git a/packages/@ionic/cli/src/commands/config/set.ts b/packages/@ionic/cli/src/commands/config/set.ts deleted file mode 100644 index 26f7cedfcb..0000000000 --- a/packages/@ionic/cli/src/commands/config/set.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { MetadataGroup, validators } from '@ionic/cli-framework'; -import { prettyPath } from '@ionic/utils-terminal'; - -import { PROJECT_FILE } from '../../constants'; -import { CommandLineInputs, CommandLineOptions, CommandMetadata } from '../../definitions'; -import { input, strong, weak } from '../../lib/color'; -import { FatalException } from '../../lib/errors'; - -import { BaseConfigCommand, getConfigValue, setConfigValue } from './base'; - -export class ConfigSetCommand extends BaseConfigCommand { - async getMetadata(): Promise { - const projectFile = this.project ? prettyPath(this.project.filePath) : PROJECT_FILE; - - return { - name: 'set', - type: 'global', - summary: 'Set config values', - description: ` -This command writes configuration values to the project's ${strong(prettyPath(projectFile))} file. It can also operate on the global CLI configuration (${strong('~/.ionic/config.json')}) using the ${input('--global')} option. - -For nested properties, separate nest levels with dots. For example, the property name ${input('integrations.cordova')} will look in the ${strong('integrations')} object for the ${strong('cordova')} property. - -For multi-app projects, this command is scoped to the current project by default. To operate at the root of the project configuration file instead, use the ${input('--root')} option. - -This command will attempt to coerce ${input('value')} into a suitable JSON type. If it is JSON-parsable, such as ${input('123')}, ${input('true')}, ${input('[]')}, etc., then it takes the parsed result. Otherwise, the value is interpreted as a string. For stricter input, use ${input('--json')}, which will error with non-JSON values. - -By default, if ${input('property')} exists and is an object or an array, the value is not overwritten. To disable this check and always overwrite the property, use ${input('--force')}. - `, - inputs: [ - { - name: 'property', - summary: 'The property name you wish to set', - validators: [validators.required], - }, - { - name: 'value', - summary: 'The new value of the given property', - validators: [validators.required], - }, - ], - options: [ - { - name: 'global', - summary: 'Use global CLI config', - type: Boolean, - aliases: ['g'], - }, - { - name: 'json', - summary: `Always interpret ${input('value')} as JSON`, - type: Boolean, - groups: [MetadataGroup.ADVANCED], - }, - { - name: 'force', - summary: 'Always overwrite existing values', - type: Boolean, - groups: [MetadataGroup.ADVANCED], - }, - { - name: 'root', - summary: `Operate on root of ${strong(prettyPath(projectFile))}`, - type: Boolean, - hint: weak('[multi-app]'), - groups: [MetadataGroup.ADVANCED], - }, - ], - exampleCommands: ['name newAppName', 'name "\\"newAppName\\"" --json', '-g interactive false'], - }; - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - const ctx = this.generateContext(inputs, options); - const { property } = ctx; - - if (typeof property === 'undefined') { - throw new FatalException(`Cannot set config to ${input(ctx.value)} without a property.`); - } - - const originalValue = getConfigValue(ctx); - setConfigValue({ ...ctx, property, originalValue }); - - if (ctx.value !== originalValue) { - this.env.log.ok(`${input(property)} set to ${input(JSON.stringify(ctx.value))}!`); - } else { - this.env.log.info(`${input(property)} is already set to ${input(JSON.stringify(ctx.value))}.`); - } - } -} diff --git a/packages/@ionic/cli/src/commands/config/unset.ts b/packages/@ionic/cli/src/commands/config/unset.ts deleted file mode 100644 index ce9f12a3b1..0000000000 --- a/packages/@ionic/cli/src/commands/config/unset.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { MetadataGroup, validators } from '@ionic/cli-framework'; -import { prettyPath } from '@ionic/utils-terminal'; - -import { PROJECT_FILE } from '../../constants'; -import { CommandLineInputs, CommandLineOptions, CommandMetadata } from '../../definitions'; -import { input, strong, weak } from '../../lib/color'; -import { FatalException } from '../../lib/errors'; - -import { BaseConfigCommand, getConfigValue, unsetConfigValue } from './base'; - -export class ConfigUnsetCommand extends BaseConfigCommand { - async getMetadata(): Promise { - const projectFile = this.project ? prettyPath(this.project.filePath) : PROJECT_FILE; - - return { - name: 'unset', - type: 'global', - summary: 'Delete config values', - description: ` -This command deletes configuration values from the project's ${strong(prettyPath(projectFile))} file. It can also operate on the global CLI configuration (${strong('~/.ionic/config.json')}) using the ${input('--global')} option. - -For nested properties, separate nest levels with dots. For example, the property name ${input('integrations.cordova')} will look in the ${strong('integrations')} object for the ${strong('cordova')} property. - -For multi-app projects, this command is scoped to the current project by default. To operate at the root of the project configuration file instead, use the ${input('--root')} option. - `, - inputs: [ - { - name: 'property', - summary: 'The property name you wish to delete', - validators: [validators.required], - }, - ], - options: [ - { - name: 'global', - summary: 'Use global CLI config', - type: Boolean, - aliases: ['g'], - }, - { - name: 'root', - summary: `Operate on root of ${strong(prettyPath(projectFile))}`, - type: Boolean, - hint: weak('[multi-app]'), - groups: [MetadataGroup.ADVANCED], - }, - ], - exampleCommands: ['', 'type', '--global git.setup', '-g interactive'], - }; - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - const ctx = this.generateContext(inputs, options); - const { property } = ctx; - - if (typeof property === 'undefined') { - throw new FatalException(`Cannot unset config entry without a property.`); - } - - const propertyExists = typeof getConfigValue(ctx) !== 'undefined'; - unsetConfigValue({ ...ctx, property }); - - if (propertyExists) { - this.env.log.ok(`${input(property)} unset!`); - } else { - this.env.log.warn(`Property ${input(property)} does not exist--cannot unset.`); - } - } -} diff --git a/packages/@ionic/cli/src/commands/cordova/base.ts b/packages/@ionic/cli/src/commands/cordova/base.ts deleted file mode 100644 index 08ef5b894b..0000000000 --- a/packages/@ionic/cli/src/commands/cordova/base.ts +++ /dev/null @@ -1,221 +0,0 @@ -import { MetadataGroup } from '@ionic/cli-framework'; -import { mkdirp, pathExists } from '@ionic/utils-fs'; -import { ERROR_COMMAND_NOT_FOUND, ERROR_SIGNAL_EXIT, SubprocessError } from '@ionic/utils-subprocess'; -import { prettyPath } from '@ionic/utils-terminal'; -import * as lodash from 'lodash'; -import * as path from 'path'; - -import { CommandInstanceInfo, CommandMetadataOption, IShellRunOptions, ProjectIntegration } from '../../definitions'; -import { input, strong, weak } from '../../lib/color'; -import { Command } from '../../lib/command'; -import { FatalException } from '../../lib/errors'; -import { getFullCommandParts, runCommand } from '../../lib/executor'; -import { pkgManagerArgs } from '../../lib/utils/npm'; - -export const CORDOVA_COMPILE_OPTIONS: CommandMetadataOption[] = [ - { - name: 'debug', - summary: 'Mark as a debug build', - type: Boolean, - groups: ['cordova', 'cordova-cli'], - hint: weak('[cordova]'), - }, - { - name: 'release', - summary: 'Mark as a release build', - type: Boolean, - groups: ['cordova', 'cordova-cli'], - hint: weak('[cordova]'), - }, - { - name: 'device', - summary: 'Deploy build to a device', - type: Boolean, - groups: ['cordova', 'cordova-cli', 'native-run'], - hint: weak('[cordova/native-run]'), - }, - { - name: 'emulator', - summary: 'Deploy build to an emulator', - type: Boolean, - groups: ['cordova', 'cordova-cli', 'native-run'], - hint: weak('[cordova/native-run]'), - }, - { - name: 'buildConfig', - summary: 'Use the specified build configuration', - groups: [MetadataGroup.ADVANCED, 'cordova', 'cordova-cli'], - hint: weak('[cordova]'), - spec: { value: 'file' }, - }, -]; - -export const CORDOVA_RUN_OPTIONS: readonly CommandMetadataOption[] = [ - ...CORDOVA_COMPILE_OPTIONS, - { - name: 'target', - summary: `Deploy build to a device (use ${input('--list')} to see all)`, - type: String, - groups: [MetadataGroup.ADVANCED, 'cordova', 'cordova-cli', 'native-run'], - hint: weak('[cordova/native-run]'), - }, -]; - -export const CORDOVA_BUILD_EXAMPLE_COMMANDS = [ - 'ios', - 'ios --prod --release', - 'ios --prod --release -- --developmentTeam="ABCD" --codeSignIdentity="iPhone Developer" --packageType="app-store"', - 'ios --buildConfig=build.json', - 'ios --prod --release --buildConfig=build.json', - 'android', - 'android --prod --release -- -- --keystore=filename.keystore --alias=myalias', - 'android --prod --release -- -- --minSdkVersion=21', - 'android --prod --release -- -- --versionCode=55', - 'android --prod --release -- -- --gradleArg=-PcdvBuildMultipleApks=true', - 'android --buildConfig=build.json', - 'android --prod --release --buildConfig=build.json', -]; - -export abstract class CordovaCommand extends Command { - private _integration?: Required; - - protected get integration(): Required { - if (!this.project) { - throw new FatalException(`Cannot use Cordova outside a project directory.`); - } - - if (!this._integration) { - this._integration = this.project.requireIntegration('cordova'); - } - - return this._integration; - } - - protected async checkCordova(runinfo: CommandInstanceInfo) { - if (!this.project) { - throw new FatalException('Cannot use Cordova outside a project directory.'); - } - - const cordova = this.project.getIntegration('cordova'); - - if (!cordova) { - const capacitor = this.project.getIntegration('capacitor'); - - if (capacitor && capacitor.enabled) { - throw new FatalException( - `Refusing to use Cordova inside a Capacitor project.\n` + - `Are you looking for the ${input('ionic capacitor')} commands? See ${input('ionic capacitor --help')}\n\n` + - `If you are switching from Capacitor to Cordova, run: ${input('ionic integrations disable capacitor')}\n` - ); - } - - const { confirmCordovaUsage } = await import('../../lib/integrations/cordova/utils'); - const confirm = await confirmCordovaUsage(this.env); - - if (!confirm) { - throw new FatalException('', 0); - } - - await runCommand(runinfo, ['integrations', 'enable', 'cordova']); - } - } - - protected async preRunChecks(runinfo: CommandInstanceInfo): Promise { - const { checkForUnsupportedProject } = await import('../../lib/integrations/cordova/utils'); - const { loadCordovaConfig } = await import('../../lib/integrations/cordova/config'); - - if (!this.project) { - throw new FatalException('Cannot use Cordova outside a project directory.'); - } - - const parts = getFullCommandParts(runinfo.location); - const alias = lodash.last(parts); - - await checkForUnsupportedProject(this.project.type, alias); - await this.checkCordova(runinfo); - - // Check for www folder - if (this.project.directory) { - const wwwPath = path.join(this.integration.root, 'www'); - const wwwExists = await pathExists(wwwPath); // TODO: hard-coded - - if (!wwwExists) { - const tasks = this.createTaskChain(); - - tasks.next(`Creating ${strong(prettyPath(wwwPath))} directory for you`); - await mkdirp(wwwPath); - tasks.end(); - } - } - - const conf = await loadCordovaConfig(this.integration); - conf.resetContentSrc(); - await conf.save(); - } - - protected async runCordova(argList: string[], { fatalOnNotFound = false, truncateErrorOutput = 5000, ...options }: IShellRunOptions = {}): Promise { - if (!this.project) { - throw new FatalException('Cannot use Cordova outside a project directory.'); - } - - try { - await this.env.shell.run('cordova', argList, { fatalOnNotFound, truncateErrorOutput, cwd: this.integration.root, ...options }); - } catch (e: any) { - if (e instanceof SubprocessError) { - if (e.code === ERROR_COMMAND_NOT_FOUND) { - const installArgs = await pkgManagerArgs(this.env.config.get('npmClient'), { command: 'install', pkg: 'cordova', global: true }); - throw new FatalException( - `The Cordova CLI was not found on your PATH. Please install Cordova globally:\n` + - `${input(installArgs.join(' '))}\n` - ); - } - - if (e.code === ERROR_SIGNAL_EXIT) { - return; - } - } - - if (options.fatalOnError) { - this.env.log.nl(); - this.env.log.error('Cordova encountered an error.\nYou may get more insight by running the Cordova command above directly.\n'); - } - - throw e; - } - } - - protected async checkForPlatformInstallation(platform: string, { promptToInstall = !['android', 'ios'].includes(platform), promptToInstallRefusalMsg = `Cannot run this command for the ${input(platform)} platform unless it is installed.` }: { promptToInstall?: boolean; promptToInstallRefusalMsg?: string; } = {}): Promise { - if (!this.project) { - throw new FatalException('Cannot use Cordova outside a project directory.'); - } - - if (platform) { - const { getPlatforms } = await import('../../lib/integrations/cordova/project'); - const { confirmCordovaBrowserUsage } = await import('../../lib/integrations/cordova/utils'); - - const platforms = await getPlatforms(this.integration.root); - - if (!platforms.includes(platform)) { - if (platform === 'browser') { - const confirm = await confirmCordovaBrowserUsage(this.env); - - if (!confirm) { - throw new FatalException(promptToInstallRefusalMsg); - } - } - - const confirm = promptToInstall ? await this.env.prompt({ - message: `Platform ${input(platform)} is not installed! Would you like to install it?`, - type: 'confirm', - name: 'confirm', - }) : true; - - if (confirm) { - await this.runCordova(['platform', 'add', platform, '--save']); - } else { - throw new FatalException(promptToInstallRefusalMsg); - } - } - } - } -} diff --git a/packages/@ionic/cli/src/commands/cordova/build.ts b/packages/@ionic/cli/src/commands/cordova/build.ts deleted file mode 100644 index c95ea50a2e..0000000000 --- a/packages/@ionic/cli/src/commands/cordova/build.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { CommandMetadataOption, Footnote, validators } from '@ionic/cli-framework'; - -import { CommandInstanceInfo, CommandLineInputs, CommandLineOptions, CommandMetadata, CommandPreRun } from '../../definitions'; -import { input, strong } from '../../lib/color'; -import { FatalException, RunnerException } from '../../lib/errors'; -import { filterArgumentsForCordova, generateOptionsForCordovaBuild } from '../../lib/integrations/cordova/utils'; - -import { CORDOVA_BUILD_EXAMPLE_COMMANDS, CORDOVA_COMPILE_OPTIONS, CordovaCommand } from './base'; - -export class BuildCommand extends CordovaCommand implements CommandPreRun { - async getMetadata(): Promise { - const exampleCommands = CORDOVA_BUILD_EXAMPLE_COMMANDS.sort(); - const options: CommandMetadataOption[] = [ - // Build Options - { - name: 'build', - summary: 'Do not invoke an Ionic build', - type: Boolean, - default: true, - }, - ...CORDOVA_COMPILE_OPTIONS, - ]; - - const footnotes: Footnote[] = [ - { - id: 'cordova-android-using-flags', - url: 'https://cordova.apache.org/docs/en/latest/guide/platforms/android/index.html#using-flags', - }, - { - id: 'cordova-ios-using-flags', - url: 'https://cordova.apache.org/docs/en/latest/guide/platforms/ios/index.html#using-flags', - }, - ]; - - const runner = this.project && await this.project.getBuildRunner(); - - if (runner) { - const libmetadata = await runner.getCommandMetadata(); - options.push(...(libmetadata.options || []).filter(o => o.groups && o.groups.includes('cordova'))); - footnotes.push(...libmetadata.footnotes || []); - } - - return { - name: 'build', - type: 'project', - summary: 'Use Cordova to build for Android and iOS platform targets', - description: ` -Like running ${input('cordova build')} directly, ${input('ionic cordova build')} also builds web assets from ${input('ionic build')} and provides friendly checks for Android and iOS platforms. - -To pass additional options to the Cordova CLI, use the ${input('--')} separator after the Ionic CLI arguments. - -The Cordova CLI requires a separator for platform-specific arguments for Android builds[^cordova-android-using-flags], so an additional separator is required for the Ionic CLI, but it is not required for iOS builds[^cordova-ios-using-flags]. See the example commands for usage with separators. To avoid using flags, consider using ${input('--buildConfig')} with a ${strong('build.json')} file. - `, - footnotes, - exampleCommands, - inputs: [ - { - name: 'platform', - summary: `The platform to build (e.g. ${['android', 'ios'].map(v => input(v)).join(', ')})`, - validators: [validators.required], - }, - ], - options, - }; - } - - async preRun(inputs: CommandLineInputs, options: CommandLineOptions, runinfo: CommandInstanceInfo): Promise { - await this.preRunChecks(runinfo); - - if (!inputs[0]) { - const platform = await this.env.prompt({ - type: 'input', - name: 'platform', - message: `What platform would you like to build (${['android', 'ios'].map(v => input(v)).join(', ')}):`, - }); - - inputs[0] = platform.trim(); - } - - await this.checkForPlatformInstallation(inputs[0]); - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - const metadata = await this.getMetadata(); - - if (!this.project) { - throw new FatalException(`Cannot run ${input('ionic cordova build')} outside a project directory.`); - } - - if (options.build) { - try { - const runner = await this.project.requireBuildRunner(); - const runnerOpts = runner.createOptionsFromCommandLine(inputs, generateOptionsForCordovaBuild(metadata, inputs, options)); - await runner.run(runnerOpts); - } catch (e: any) { - if (e instanceof RunnerException) { - throw new FatalException(e.message); - } - - throw e; - } - } - - const cordovaArgs = filterArgumentsForCordova(metadata, options); - await this.runCordova(cordovaArgs, { stdio: 'inherit' }); - } -} diff --git a/packages/@ionic/cli/src/commands/cordova/compile.ts b/packages/@ionic/cli/src/commands/cordova/compile.ts deleted file mode 100644 index cacc917f04..0000000000 --- a/packages/@ionic/cli/src/commands/cordova/compile.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { validators } from '@ionic/cli-framework'; - -import { CommandInstanceInfo, CommandLineInputs, CommandLineOptions, CommandMetadata, CommandPreRun } from '../../definitions'; -import { input } from '../../lib/color'; -import { filterArgumentsForCordova } from '../../lib/integrations/cordova/utils'; - -import { CORDOVA_COMPILE_OPTIONS, CordovaCommand } from './base'; - -export class CompileCommand extends CordovaCommand implements CommandPreRun { - async getMetadata(): Promise { - return { - name: 'compile', - type: 'project', - summary: 'Compile native platform code', - description: ` -Like running ${input('cordova compile')} directly, but provides friendly checks. - `, - exampleCommands: [ - 'ios', - 'ios --device', - 'android', - ], - inputs: [ - { - name: 'platform', - summary: `The platform to compile (${['android', 'ios'].map(v => input(v)).join(', ')})`, - validators: [validators.required], - }, - ], - options: [ - ...CORDOVA_COMPILE_OPTIONS, - ], - }; - } - - async preRun(inputs: CommandLineInputs, options: CommandLineOptions, runinfo: CommandInstanceInfo): Promise { - await this.preRunChecks(runinfo); - - if (!inputs[0]) { - const platform = await this.env.prompt({ - type: 'input', - name: 'platform', - message: `What platform would you like to compile (${['android', 'ios'].map(v => input(v)).join(', ')}):`, - }); - - inputs[0] = platform.trim(); - } - - await this.checkForPlatformInstallation(inputs[0]); - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - const metadata = await this.getMetadata(); - await this.runCordova(filterArgumentsForCordova(metadata, options), { stdio: 'inherit' }); - } -} diff --git a/packages/@ionic/cli/src/commands/cordova/emulate.ts b/packages/@ionic/cli/src/commands/cordova/emulate.ts deleted file mode 100644 index e5626262f7..0000000000 --- a/packages/@ionic/cli/src/commands/cordova/emulate.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { CommandMetadata } from '../../definitions'; - -import { RunCommand } from './run'; - -export class EmulateCommand extends RunCommand { - async getMetadata(): Promise { - const metadata = await super.getMetadata(); - - return { - ...metadata, - name: 'emulate', - summary: 'Emulate an Ionic project on a simulator/emulator', - }; - } -} diff --git a/packages/@ionic/cli/src/commands/cordova/index.ts b/packages/@ionic/cli/src/commands/cordova/index.ts deleted file mode 100644 index 3cf5169e7e..0000000000 --- a/packages/@ionic/cli/src/commands/cordova/index.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { strong } from '../../lib/color'; -import { CommandMap, Namespace } from '../../lib/namespace'; - -export class CordovaNamespace extends Namespace { - async getMetadata() { - return { - name: 'cordova', - summary: 'Cordova functionality', - description: ` -These commands integrate with Apache Cordova, which brings native functionality to your app. - -Cordova Reference documentation: -- Overview: ${strong('https://cordova.apache.org/docs/en/latest/guide/overview/index.html')} -- CLI documentation: ${strong('https://cordova.apache.org/docs/en/latest/reference/cordova-cli/')} - `, - }; - } - - async getCommands(): Promise { - return new CommandMap([ - ['build', async () => { const { BuildCommand } = await import('./build'); return new BuildCommand(this); }], - ['compile', async () => { const { CompileCommand } = await import('./compile'); return new CompileCommand(this); }], - ['emulate', async () => { const { EmulateCommand } = await import('./emulate'); return new EmulateCommand(this); }], - ['platform', async () => { const { PlatformCommand } = await import('./platform'); return new PlatformCommand(this); }], - ['plugin', async () => { const { PluginCommand } = await import('./plugin'); return new PluginCommand(this); }], - ['prepare', async () => { const { PrepareCommand } = await import('./prepare'); return new PrepareCommand(this); }], - ['resources', async () => { const { ResourcesCommand } = await import('./resources'); return new ResourcesCommand(this); }], - ['run', async () => { const { RunCommand } = await import('./run'); return new RunCommand(this); }], - ['requirements', async () => { const { RequirementsCommand } = await import('./requirements'); return new RequirementsCommand(this); }], - ['platforms', 'platform'], - ['plugins', 'plugin'], - ['res', 'resources'], - ]); - } -} diff --git a/packages/@ionic/cli/src/commands/cordova/platform.ts b/packages/@ionic/cli/src/commands/cordova/platform.ts deleted file mode 100644 index e6385ddb0d..0000000000 --- a/packages/@ionic/cli/src/commands/cordova/platform.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { contains, validate, validators } from '@ionic/cli-framework'; - -import { CommandInstanceInfo, CommandLineInputs, CommandLineOptions, CommandMetadata, CommandPreRun } from '../../definitions'; -import { input } from '../../lib/color'; -import { SUPPORTED_PLATFORMS, createCordovaResNotFoundMessage, findCordovaRes } from '../../lib/cordova-res'; -import { FatalException } from '../../lib/errors'; -import { runCommand } from '../../lib/executor'; - -import { CordovaCommand } from './base'; - -export class PlatformCommand extends CordovaCommand implements CommandPreRun { - async getMetadata(): Promise { - return { - name: 'platform', - type: 'project', - summary: 'Manage Cordova platform targets', - description: ` -Like running ${input('cordova platform')} directly, but adds default Ionic icons and splash screen resources (during ${input('add')}) and provides friendly checks. - `, - exampleCommands: ['', 'add ios', 'add android', 'rm ios'], - inputs: [ - { - name: 'action', - summary: `${input('add')}, ${input('remove')}, or ${input('update')} a platform; ${input('ls')}, ${input('check')}, or ${input('save')} all project platforms`, - }, - { - name: 'platform', - summary: `The platform that you would like to add (${['android', 'ios'].map(v => input(v)).join(', ')})`, - }, - ], - options: [ - { - name: 'resources', - summary: `Do not pregenerate icons and splash screen resources (corresponds to ${input('add')})`, - type: Boolean, - default: true, - }, - ], - }; - } - - async preRun(inputs: CommandLineInputs, options: CommandLineOptions, runinfo: CommandInstanceInfo): Promise { - await this.preRunChecks(runinfo); - - if (options['r'] || options['noresources']) { - options['resources'] = false; - } - - inputs[0] = !inputs[0] ? 'ls' : inputs[0]; - inputs[0] = (inputs[0] === 'rm') ? 'remove' : inputs[0]; - inputs[0] = (inputs[0] === 'list') ? 'ls' : inputs[0]; - - validate(inputs[0], 'action', [contains(['add', 'remove', 'update', 'ls', 'check', 'save'], {})]); - - // If the action is list, check, or save, then just end here. - if (['ls', 'check', 'save'].includes(inputs[0])) { - await this.runCordova(['platform', inputs[0]], {}); - throw new FatalException('', 0); - } - - if (!inputs[1]) { - const platform = await this.env.prompt({ - type: 'input', - name: 'platform', - message: `What platform would you like to ${inputs[0]} (${['android', 'ios'].map(v => input(v)).join(', ')}):`, - }); - - inputs[1] = platform.trim(); - } - - validate(inputs[1], 'platform', [validators.required]); - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions, runinfo: CommandInstanceInfo): Promise { - const { getPlatforms } = await import('../../lib/integrations/cordova/project'); - const { confirmCordovaBrowserUsage, filterArgumentsForCordova } = await import('../../lib/integrations/cordova/utils'); - - if (!this.project) { - throw new FatalException(`Cannot run ${input('ionic cordova platform')} outside a project directory.`); - } - - const [ action, platformName ] = inputs; - - const platforms = await getPlatforms(this.integration.root); - - if (action === 'add') { - if (platforms.includes(platformName)) { - this.env.log.msg(`Platform ${platformName} already exists.`); - return; - } - - if (platformName === 'browser') { - const confirm = await confirmCordovaBrowserUsage(this.env); - - if (!confirm) { - return; - } - } - } - - const metadata = await this.getMetadata(); - const cordovaArgs = filterArgumentsForCordova(metadata, options); - - await this.runCordova(cordovaArgs, {}); - - if (action === 'add' && options['resources'] && SUPPORTED_PLATFORMS.includes(platformName)) { - const args = ['cordova', 'resources', platformName, '--force']; - const p = await findCordovaRes(); - - if (p) { - await runCommand(runinfo, args); - } else { - this.env.log.warn(await createCordovaResNotFoundMessage(this.env.config.get('npmClient'))); - this.env.log.warn( - `Cannot generate resources without ${input('cordova-res')} installed.\n` + - `Once installed, you can generate resources with the following command:\n\n` + - input(['ionic', ...args].join(' ')) - ); - } - } - } -} diff --git a/packages/@ionic/cli/src/commands/cordova/plugin.ts b/packages/@ionic/cli/src/commands/cordova/plugin.ts deleted file mode 100644 index da5cd23671..0000000000 --- a/packages/@ionic/cli/src/commands/cordova/plugin.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { MetadataGroup, contains, validate, validators } from '@ionic/cli-framework'; - -import { CommandInstanceInfo, CommandLineInputs, CommandLineOptions, CommandMetadata, CommandPreRun } from '../../definitions'; -import { input } from '../../lib/color'; -import { FatalException } from '../../lib/errors'; -import { filterArgumentsForCordova } from '../../lib/integrations/cordova/utils'; -import { pkgManagerArgs } from '../../lib/utils/npm'; - -import { CordovaCommand } from './base'; - -export class PluginCommand extends CordovaCommand implements CommandPreRun { - async getMetadata(): Promise { - return { - name: 'plugin', - type: 'project', - summary: 'Manage Cordova plugins', - description: ` -Like running ${input('cordova plugin')} directly, but provides friendly checks. - `, - exampleCommands: ['', 'add cordova-plugin-inappbrowser@latest', 'add phonegap-plugin-push --variable SENDER_ID=XXXXX', 'rm cordova-plugin-camera'], - inputs: [ - { - name: 'action', - summary: `${input('add')} or ${input('remove')} a plugin; ${input('ls')} or ${input('save')} all project plugins`, - }, - { - name: 'plugin', - summary: `The name of the plugin (corresponds to ${input('add')} and ${input('remove')})`, - }, - ], - options: [ - { - name: 'force', - summary: `Force overwrite the plugin if it exists (corresponds to ${input('add')})`, - type: Boolean, - groups: [MetadataGroup.ADVANCED, 'cordova', 'cordova-cli'], - }, - { - name: 'variable', - summary: 'Specify plugin variables', - groups: ['cordova', 'cordova-cli'], - spec: { value: 'KEY=VALUE' }, - }, - ], - }; - } - - async preRun(inputs: CommandLineInputs, options: CommandLineOptions, runinfo: CommandInstanceInfo): Promise { - if (!this.project) { - throw new FatalException('Cannot use Cordova outside a project directory.'); - } - - const capacitor = this.project.getIntegration('capacitor'); - - if (capacitor && capacitor.enabled) { - const pkg = inputs[1] ? inputs[1] : ''; - const installArgs = await pkgManagerArgs(this.env.config.get('npmClient'), { command: 'install', pkg, save: false, saveExact: false }); - const uninstallArgs = await pkgManagerArgs(this.env.config.get('npmClient'), { command: 'uninstall', pkg, save: false }); - - throw new FatalException( - `Refusing to run ${input('ionic cordova plugin')} inside a Capacitor project.\n` + - `In Capacitor projects, Cordova plugins are just regular npm dependencies.\n\n` + - `- To add a plugin, use ${input(installArgs.join(' '))}\n` + - `- To remove, use ${input(uninstallArgs.join(' '))}\n` - ); - } - - await this.preRunChecks(runinfo); - - inputs[0] = !inputs[0] ? 'ls' : inputs[0]; - inputs[0] = (inputs[0] === 'rm') ? 'remove' : inputs[0]; - inputs[0] = (inputs[0] === 'list') ? 'ls' : inputs[0]; - - validate(inputs[0], 'action', [contains(['add', 'remove', 'ls', 'save'], {})]); - - // If the action is list then lets just end here. - if (['ls', 'save'].includes(inputs[0])) { - await this.runCordova(['plugin', inputs[0]], {}); - throw new FatalException('', 0); - } - - if (!inputs[1]) { - const plugin = await this.env.prompt({ - message: `What plugin would you like to ${inputs[0]}:`, - type: 'input', - name: 'plugin', - }); - - inputs[1] = plugin; - } - - validate(inputs[1], 'plugin', [validators.required]); - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - const metadata = await this.getMetadata(); - const cordovaArgs = filterArgumentsForCordova(metadata, options); - - await this.runCordova(cordovaArgs, {}); - } -} diff --git a/packages/@ionic/cli/src/commands/cordova/prepare.ts b/packages/@ionic/cli/src/commands/cordova/prepare.ts deleted file mode 100644 index 80c4a4af78..0000000000 --- a/packages/@ionic/cli/src/commands/cordova/prepare.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { CommandMetadataOption, Footnote } from '@ionic/cli-framework'; - -import { CommandInstanceInfo, CommandLineInputs, CommandLineOptions, CommandMetadata, CommandPreRun } from '../../definitions'; -import { input, strong } from '../../lib/color'; -import { FatalException, RunnerException } from '../../lib/errors'; -import { filterArgumentsForCordova, generateOptionsForCordovaBuild } from '../../lib/integrations/cordova/utils'; - -import { CordovaCommand } from './base'; - -export class PrepareCommand extends CordovaCommand implements CommandPreRun { - async getMetadata(): Promise { - const options: CommandMetadataOption[] = [ - { - name: 'build', - summary: 'Do not invoke an Ionic build', - type: Boolean, - default: true, - }, - ]; - - const footnotes: Footnote[] = []; - - const runner = this.project && await this.project.getBuildRunner(); - - if (runner) { - const libmetadata = await runner.getCommandMetadata(); - options.push(...libmetadata.options || []); - footnotes.push(...libmetadata.footnotes || []); - } - - return { - name: 'prepare', - type: 'project', - summary: 'Copies assets to Cordova platforms, preparing them for native builds', - description: ` -${input('ionic cordova prepare')} will do the following: - -- Perform an Ionic build, which compiles web assets to ${strong('www/')}. -- Copy the ${strong('www/')} directory into your Cordova platforms. -- Transform ${strong('config.xml')} into platform-specific manifest files. -- Copy icons and splash screens from ${strong('resources/')} to into your Cordova platforms. -- Copy plugin files into specified platforms. - -You may wish to use ${input('ionic cordova prepare')} if you run your project with Android Studio or Xcode. - `, - footnotes, - exampleCommands: ['', 'ios', 'android'], - inputs: [ - { - name: 'platform', - summary: `The platform you would like to prepare (e.g. ${['android', 'ios'].map(v => input(v)).join(', ')})`, - }, - ], - options, - }; - } - - async preRun(inputs: CommandLineInputs, options: CommandLineOptions, runinfo: CommandInstanceInfo): Promise { - await this.preRunChecks(runinfo); - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - const { loadCordovaConfig } = await import('../../lib/integrations/cordova/config'); - const { getPlatforms } = await import('../../lib/integrations/cordova/project'); - const [ platform ] = inputs; - - if (!this.project) { - throw new FatalException(`Cannot run ${input('ionic cordova prepare')} outside a project directory.`); - } - - if (platform) { - await this.checkForPlatformInstallation(platform, { - promptToInstall: true, - promptToInstallRefusalMsg: ( - `Cannot prepare for ${input(platform)} unless the platform is installed.\n` + - `Did you mean just ${input('ionic cordova prepare')}?\n` - ), - }); - } else { - const conf = await loadCordovaConfig(this.integration); - const platforms = await getPlatforms(this.integration.root); - const configuredPlatforms = conf.getConfiguredPlatforms(); - - if (configuredPlatforms.length === 0 && platforms.length === 0) { - this.env.log.warn( - `No platforms added to this project. Cannot prepare native platforms without any installed.\n` + - `Run ${input('ionic cordova platform add ')} to add native platforms.` - ); - - throw new FatalException('', 0); - } - } - - const metadata = await this.getMetadata(); - - if (options.build) { - const buildOptions = generateOptionsForCordovaBuild(metadata, inputs, options); - - if (buildOptions['platform']) { - try { - const runner = await this.project.requireBuildRunner(); - const runnerOpts = runner.createOptionsFromCommandLine(inputs, buildOptions); - await runner.run(runnerOpts); - } catch (e: any) { - if (e instanceof RunnerException) { - throw new FatalException(e.message); - } - - throw e; - } - } else { - this.env.log.warn( - `Cannot perform Ionic build without ${input('platform')}. Falling back to just ${input('cordova prepare')}.\n` + - `Please supply a ${input('platform')} (e.g. ${['android', 'ios'].map(v => input(v)).join(', ')}) so the Ionic CLI can build web assets. The ${input('--no-build')} option will hide this warning.` - ); - - this.env.log.nl(); - } - } - - await this.runCordova(filterArgumentsForCordova(metadata, options), { stdio: 'inherit' }); - } -} diff --git a/packages/@ionic/cli/src/commands/cordova/requirements.ts b/packages/@ionic/cli/src/commands/cordova/requirements.ts deleted file mode 100644 index 6ecf992d76..0000000000 --- a/packages/@ionic/cli/src/commands/cordova/requirements.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { CommandInstanceInfo, CommandLineInputs, CommandLineOptions, CommandMetadata, CommandPreRun } from '../../definitions'; -import { isExitCodeException } from '../../guards'; -import { input } from '../../lib/color'; -import { FatalException } from '../../lib/errors'; -import { filterArgumentsForCordova } from '../../lib/integrations/cordova/utils'; - -import { CordovaCommand } from './base'; - -export class RequirementsCommand extends CordovaCommand implements CommandPreRun { - async getMetadata(): Promise { - return { - name: 'requirements', - type: 'project', - summary: 'Checks and print out all the requirements for platforms', - description: ` -Like running ${input('cordova requirements')} directly, but provides friendly checks. - `, - inputs: [ - { - name: 'platform', - summary: `The platform for which you would like to gather requirements (${['android', 'ios'].map(v => input(v)).join(', ')})`, - }, - ], - }; - } - - async preRun(inputs: CommandLineInputs, options: CommandLineOptions, runinfo: CommandInstanceInfo): Promise { - await this.preRunChecks(runinfo); - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - const [ platform ] = inputs; - - if (!this.project) { - throw new FatalException(`Cannot run ${input('ionic cordova requirements')} outside a project directory.`); - } - - await this.checkForPlatformInstallation(platform, { - promptToInstall: true, - promptToInstallRefusalMsg: ( - `Can't gather requirements for ${input(platform)} unless the platform is installed.\n` + - `Did you mean just ${input('ionic cordova requirements')}?\n` - ), - }); - - const metadata = await this.getMetadata(); - - try { - await this.runCordova(filterArgumentsForCordova(metadata, options), { showError: false, fatalOnError: false }); - } catch (e: any) { - if (e.fatal || !isExitCodeException(e)) { - throw e; - } - - throw new FatalException(); - } - } -} diff --git a/packages/@ionic/cli/src/commands/cordova/resources.ts b/packages/@ionic/cli/src/commands/cordova/resources.ts deleted file mode 100644 index 10d64affd2..0000000000 --- a/packages/@ionic/cli/src/commands/cordova/resources.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { CommandLineInputs, CommandLineOptions, CommandMetadata } from '../../definitions'; -import { ancillary, input, strong } from '../../lib/color'; -import { SUPPORTED_PLATFORMS, createCordovaResArgs, runCordovaRes } from '../../lib/cordova-res'; -import { FatalException } from '../../lib/errors'; - -import { CordovaCommand } from './base'; - -export class ResourcesCommand extends CordovaCommand { - async getMetadata(): Promise { - return { - name: 'resources', - type: 'project', - summary: 'Automatically create icon and splash screen resources', - description: ` -Generate perfectly sized icons and splash screens from PNG source images for your Cordova platforms with this command. - -The source image for icons should ideally be at least ${strong('1024×1024px')} and located at ${strong('resources/icon.png')}. The source image for splash screens should ideally be at least ${strong('2732×2732px')} and located at ${strong('resources/splash.png')}. If you used ${input('ionic start')}, there should already be default Ionic resources in the ${strong('resources/')} directory, which you can overwrite. - -You can also generate platform-specific icons and splash screens by placing them in the respective ${strong('resources//')} directory. For example, to generate an icon for Android, place your image at ${strong('resources/android/icon.png')}. - -For best results, the splash screen's artwork should roughly fit within a square (${strong('1200×1200px')}) at the center of the image. You can use ${strong('https://code.ionicframework.com/resources/splash.psd')} as a template for your splash screen. - -${input('ionic cordova resources')} will automatically update your ${strong('config.xml')} to reflect the changes in the generated images, which Cordova then configures. - -This command uses the ${input('cordova-res')} utility[^cordova-res-repo] to generate resources locally. - -Cordova reference documentation: -- Icons: ${strong('https://cordova.apache.org/docs/en/latest/config_ref/images.html')} -- Splash Screens: ${strong('https://cordova.apache.org/docs/en/latest/reference/cordova-plugin-splashscreen/')} - `, - footnotes: [ - { - id: 'cordova-res-repo', - url: 'https://github.com/ionic-team/cordova-res', - }, - ], - exampleCommands: ['', ...SUPPORTED_PLATFORMS], - inputs: [ - { - name: 'platform', - summary: `The platform for which you would like to generate resources (${SUPPORTED_PLATFORMS.map(v => input(v)).join(', ')})`, - }, - ], - options: [ - { - name: 'icon', - summary: 'Generate icon resources', - type: Boolean, - aliases: ['i'], - }, - { - name: 'splash', - summary: 'Generate splash screen resources', - type: Boolean, - aliases: ['s'], - }, - ], - }; - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - if (!this.project) { - throw new FatalException(`Cannot run ${input('ionic cordova resources')} outside a project directory.`); - } - - if (options['cordova-res'] === false) { - this.env.log.warn( - `The ${input('--no-cordova-res')} option has been removed.\n` + - `The Ionic image generation server has been shut down and the online resource generation flow no longer exists. Please migrate to ${strong('cordova-res')}${ancillary('[1]')}, the resource generation tool by Ionic that runs on your computer.\n\n` + - `${ancillary('[1]')}: ${strong('https://github.com/ionic-team/cordova-res')}\n` - ); - } - - const platform = inputs[0] ? String(inputs[0]) : undefined; - - await runCordovaRes(this.env, createCordovaResArgs({ platform }, options), { cwd: this.integration.root }); - } -} diff --git a/packages/@ionic/cli/src/commands/cordova/run.ts b/packages/@ionic/cli/src/commands/cordova/run.ts deleted file mode 100644 index 5662d59e31..0000000000 --- a/packages/@ionic/cli/src/commands/cordova/run.ts +++ /dev/null @@ -1,362 +0,0 @@ -import { Footnote, MetadataGroup, validators } from '@ionic/cli-framework'; -import { onBeforeExit, sleepForever } from '@ionic/utils-process'; -import { debug as Debug } from 'debug'; -import * as lodash from 'lodash'; - -import { CommandInstanceInfo, CommandLineInputs, CommandLineOptions, CommandMetadata, CommandMetadataOption, CommandPreRun, IShellRunOptions, ServeDetails } from '../../definitions'; -import { COMMON_BUILD_COMMAND_OPTIONS } from '../../lib/build'; -import { input, strong, weak } from '../../lib/color'; -import { FatalException, RunnerException } from '../../lib/errors'; -import { getPackagePath } from '../../lib/integrations/cordova/project'; -import { filterArgumentsForCordova, generateOptionsForCordovaBuild } from '../../lib/integrations/cordova/utils'; -import { SUPPORTED_PLATFORMS, checkNativeRun, createNativeRunArgs, createNativeRunListArgs, getNativeTargets, runNativeRun } from '../../lib/native-run'; -import { COMMON_SERVE_COMMAND_OPTIONS, LOCAL_ADDRESSES } from '../../lib/serve'; -import { createPrefixedWriteStream } from '../../lib/utils/logger'; - -import { CORDOVA_BUILD_EXAMPLE_COMMANDS, CORDOVA_RUN_OPTIONS, CordovaCommand } from './base'; - -const debug = Debug('ionic:commands:run'); - -const NATIVE_RUN_OPTIONS: readonly CommandMetadataOption[] = [ - { - name: 'native-run', - summary: `Do not use ${input('native-run')} to run the app; use Cordova instead`, - type: Boolean, - default: true, - groups: ['native-run'], - hint: weak('[native-run]'), - }, - { - name: 'connect', - summary: 'Tie the running app to the process', - type: Boolean, - groups: ['native-run'], - hint: weak('[native-run] (--livereload)'), - }, - { - name: 'json', - summary: `Output targets in JSON`, - type: Boolean, - groups: [MetadataGroup.ADVANCED, 'native-run'], - hint: weak('[native-run] (--list)'), - }, -]; - -export class RunCommand extends CordovaCommand implements CommandPreRun { - async getMetadata(): Promise { - const groups: string[] = []; - const exampleCommands = [ - ...CORDOVA_BUILD_EXAMPLE_COMMANDS, - 'android -l', - 'ios --livereload --external', - 'ios --livereload-url=http://localhost:8100', - ].sort(); - - let options: CommandMetadataOption[] = [ - { - name: 'list', - summary: 'List all available targets', - type: Boolean, - groups: ['cordova', 'cordova-cli', 'native-run'], - }, - // Build Options - { - name: 'build', - summary: 'Do not invoke Ionic build', - type: Boolean, - default: true, - }, - ...COMMON_BUILD_COMMAND_OPTIONS.filter(o => !['engine', 'platform'].includes(o.name)), - // Serve Options - ...COMMON_SERVE_COMMAND_OPTIONS.filter(o => !['livereload'].includes(o.name)).map(o => ({ ...o, hint: weak('(--livereload)') })), - { - name: 'livereload', - summary: 'Spin up dev server to live-reload www files', - type: Boolean, - aliases: ['l'], - }, - { - name: 'livereload-url', - summary: 'Provide a custom URL to the dev server', - spec: { value: 'url' }, - }, - ]; - - const footnotes: Footnote[] = [ - { - id: 'remote-debugging-docs', - url: 'https://ionicframework.com/docs/developer-resources/developer-tips', - shortUrl: 'https://ion.link/remote-debugging-docs', - }, - { - id: 'livereload-docs', - url: 'https://ionicframework.com/docs/cli/livereload', - shortUrl: 'https://ion.link/livereload-docs', - }, - { - id: 'native-run-repo', - url: 'https://github.com/ionic-team/native-run', - }, - ]; - - const serveRunner = this.project && await this.project.getServeRunner(); - const buildRunner = this.project && await this.project.getBuildRunner(); - - if (buildRunner) { - const libmetadata = await buildRunner.getCommandMetadata(); - groups.push(...libmetadata.groups || []); - options.push(...(libmetadata.options || []).filter(o => o.groups && o.groups.includes('cordova'))); - footnotes.push(...libmetadata.footnotes || []); - } - - if (serveRunner) { - const libmetadata = await serveRunner.getCommandMetadata(); - const existingOpts = options.map(o => o.name); - groups.push(...libmetadata.groups || []); - const runnerOpts = (libmetadata.options || []) - .filter(o => !existingOpts.includes(o.name) && o.groups && o.groups.includes('cordova')) - .map(o => ({ ...o, hint: `${o.hint ? `${o.hint} ` : ''}${weak('(--livereload)')}` })); - options = lodash.uniqWith([...runnerOpts, ...options], (optionA, optionB) => optionA.name === optionB.name); - footnotes.push(...libmetadata.footnotes || []); - } - - // Cordova Options - options.push(...CORDOVA_RUN_OPTIONS); - - // `native-run` Options - options.push(...NATIVE_RUN_OPTIONS); - - return { - name: 'run', - type: 'project', - summary: 'Run an Ionic project on a connected device', - description: ` -Build your app and deploy it to devices and emulators using this command. Optionally specify the ${input('--livereload')} option to use the dev server from ${input('ionic serve')} for livereload functionality. - -This command will first use ${input('ionic build')} to build web assets (or ${input('ionic serve')} with the ${input('--livereload')} option). Then, ${input('cordova build')} is used to compile and prepare your app. Finally, the ${input('native-run')} utility[^native-run-repo] is used to run your app on a device. To use Cordova for this process instead, use the ${input('--no-native-run')} option. - -If you have multiple devices and emulators, you can target a specific one with the ${input('--target')} option. You can list targets with ${input('--list')}. - -For Android and iOS, you can setup Remote Debugging on your device with browser development tools using these docs[^remote-debugging-docs]. - -When using ${input('--livereload')} with hardware devices, remember that livereload needs an active connection between device and computer. In some scenarios, you may need to host the dev server on an external address using the ${input('--external')} option. See these docs[^livereload-docs] for more information. - -Just like with ${input('ionic cordova build')}, you can pass additional options to the Cordova CLI using the ${input('--')} separator. To pass additional options to the dev server, consider using ${input('ionic serve')} separately and using the ${input('--livereload-url')} option. - `, - footnotes, - exampleCommands, - inputs: [ - { - name: 'platform', - summary: `The platform to run (e.g. ${['android', 'ios'].map(v => input(v)).join(', ')})`, - validators: [validators.required], - }, - ], - options, - groups, - }; - } - - async preRun(inputs: CommandLineInputs, options: CommandLineOptions, runinfo: CommandInstanceInfo): Promise { - if (options['native-run']) { - await this.checkNativeRun(); - } - - await this.preRunChecks(runinfo); - - const metadata = await this.getMetadata(); - - if (options['noproxy']) { - this.env.log.warn(`The ${input('--noproxy')} option has been deprecated. Please use ${input('--no-proxy')}.`); - options['proxy'] = false; - } - - if (options['x']) { - options['proxy'] = false; - } - - if (options['livereload-url']) { - options['livereload'] = true; - } - - if (!options['build'] && options['livereload']) { - this.env.log.warn(`No livereload with ${input('--no-build')}.`); - options['livereload'] = false; - } - - // If we're using the emulate command, and if --device and --emulator are - // not used, we should set the --emulator flag to mark intent. - if (!options['device'] && !options['emulator'] && metadata.name === 'emulate') { - options['emulator'] = true; - } - - if (options['list']) { - if (options['native-run']) { - const args = createNativeRunListArgs(inputs, options); - await this.runNativeRun(args); - } else { - const args = filterArgumentsForCordova(metadata, options); - await this.runCordova(['run', ...args.slice(1)], {}); - } - - throw new FatalException('', 0); - } - - if (!inputs[0]) { - const p = await this.env.prompt({ - type: 'input', - name: 'platform', - message: `What platform would you like to run (${['android', 'ios'].map(v => input(v)).join(', ')}):`, - }); - - inputs[0] = p.trim(); - } - - const [platform] = inputs; - - if (platform && options['native-run'] && !SUPPORTED_PLATFORMS.includes(platform)) { - this.env.log.warn(`${input(platform)} is not supported by ${input('native-run')}. Using Cordova to run the app.`); - options['native-run'] = false; - } - - // If we're using native-run, and if --device and --emulator are not used, - // we can detect if hardware devices are plugged in and prefer them over - // any virtual devices the host has. - if (options['native-run'] && !options['device'] && !options['emulator'] && platform) { - const platformTargets = await getNativeTargets(this.env, platform); - const { devices } = platformTargets; - - debug(`Native platform devices: %O`, devices); - - if (devices.length > 0) { - this.env.log.info(`Hardware device(s) found for ${input(platform)}. Using ${input('--device')}.`); - options['device'] = true; - } - } - - await this.checkForPlatformInstallation(platform); - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - try { - if (options['livereload']) { - await this.runServeDeploy(inputs, options); - } else { - await this.runBuildDeploy(inputs, options); - } - } catch (e: any) { - if (e instanceof RunnerException) { - throw new FatalException(e.message); - } - - throw e; - } - } - - protected async runServeDeploy(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - const { loadCordovaConfig } = await import('../../lib/integrations/cordova/config'); - const metadata = await this.getMetadata(); - - if (!this.project) { - throw new FatalException(`Cannot run ${input(`ionic cordova ${metadata.name}`)} outside a project directory.`); - } - - const runner = await this.project.requireServeRunner(); - const runnerOpts = runner.createOptionsFromCommandLine(inputs, generateOptionsForCordovaBuild(metadata, inputs, options)); - - /** - * With the --livereload-url option, this command won't perform a serve. If - * this is the case, details will be undefined. - */ - let details: ServeDetails | undefined; - let serverUrl = options['livereload-url'] ? String(options['livereload-url']) : undefined; - - if (!serverUrl) { - details = await runner.run(runnerOpts); - - if (details.externallyAccessible === false && !options['native-run']) { - const extra = LOCAL_ADDRESSES.includes(details.externalAddress) ? '\nEnsure you have proper port forwarding setup from your device to your computer.' : ''; - this.env.log.warn(`Your device or emulator may not be able to access ${strong(details.externalAddress)}.${extra}\n\n`); - } - - serverUrl = `${details.protocol || 'http'}://${details.externalAddress}:${details.port}`; - } - - const conf = await loadCordovaConfig(this.integration); - - onBeforeExit(async () => { - conf.resetContentSrc(); - await conf.save(); - }); - - conf.writeContentSrc(serverUrl); - await conf.save(); - - const cordovalogws = createPrefixedWriteStream(this.env.log, weak(`[cordova]`)); - const buildOpts: IShellRunOptions = { stream: cordovalogws }; - // ignore very verbose compiler output on stdout unless --verbose - buildOpts.stdio = options['verbose'] ? 'inherit' : ['pipe', 'ignore', 'pipe']; - - if (options['native-run']) { - const [platform] = inputs; - - await this.runCordova(filterArgumentsForCordova({ ...metadata, name: 'build' }, options), buildOpts); - - const packagePath = await getPackagePath(this.integration.root, conf.getProjectInfo().name, platform, { emulator: !options['device'], release: !!options['release'] }); - const forwardedPorts = details ? runner.getUsedPorts(runnerOpts, details) : []; - - await this.runNativeRun(createNativeRunArgs({ packagePath, platform, forwardedPorts }, options)); - } else { - await this.runCordova(filterArgumentsForCordova(metadata, options), buildOpts); - await sleepForever(); - } - } - - protected async runBuildDeploy(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - const { loadCordovaConfig } = await import('../../lib/integrations/cordova/config'); - const metadata = await this.getMetadata(); - - if (!this.project) { - throw new FatalException(`Cannot run ${input(`ionic cordova ${metadata.name}`)} outside a project directory.`); - } - - if (options.build) { - try { - const runner = await this.project.requireBuildRunner(); - const runnerOpts = runner.createOptionsFromCommandLine(inputs, generateOptionsForCordovaBuild(metadata, inputs, options)); - await runner.run(runnerOpts); - } catch (e: any) { - if (e instanceof RunnerException) { - throw new FatalException(e.message); - } - - throw e; - } - } - - if (options['native-run']) { - const conf = await loadCordovaConfig(this.integration); - const [platform] = inputs; - - await this.runCordova(filterArgumentsForCordova({ ...metadata, name: 'build' }, options), { stdio: 'inherit' }); - - const packagePath = await getPackagePath(this.integration.root, conf.getProjectInfo().name, platform, { emulator: !options['device'], release: !!options['release'] }); - - await this.runNativeRun(createNativeRunArgs({ packagePath, platform }, { ...options, connect: false })); - } else { - await this.runCordova(filterArgumentsForCordova(metadata, options), { stdio: 'inherit' }); - } - } - - protected async checkNativeRun(): Promise { - await checkNativeRun(this.env); - } - - protected async runNativeRun(args: readonly string[]): Promise { - if (!this.project) { - throw new FatalException(`Cannot run ${input('ionic cordova run/emulate')} outside a project directory.`); - } - - await runNativeRun(this.env, args, { cwd: this.integration.root }); - } -} diff --git a/packages/@ionic/cli/src/commands/enterprise/index.ts b/packages/@ionic/cli/src/commands/enterprise/index.ts deleted file mode 100644 index 8af38ffc72..0000000000 --- a/packages/@ionic/cli/src/commands/enterprise/index.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { MetadataGroup } from '@ionic/cli-framework'; - -import { CommandMap, Namespace } from '../../lib/namespace'; - -export class EnterpriseNamespace extends Namespace { - async getMetadata() { - return { - name: 'enterprise', - summary: 'Manage Ionic Enterprise features', - description: ` -Commands to help manage Ionic Enterprise[^enterprise-edition] subscriptions. - `, - footnotes: [ - { - id: 'enterprise-edition', - url: 'https://ionicframework.com/enterprise-edition', - shortUrl: 'https://ion.link/enterprise', - }, - ], - groups: [MetadataGroup.PAID], - }; - } - - async getCommands(): Promise { - return new CommandMap([ - ['register', async () => { const { RegisterCommand } = await import('./register'); return new RegisterCommand(this); }], - ]); - } -} diff --git a/packages/@ionic/cli/src/commands/enterprise/register.ts b/packages/@ionic/cli/src/commands/enterprise/register.ts deleted file mode 100644 index 96dc4de275..0000000000 --- a/packages/@ionic/cli/src/commands/enterprise/register.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { MetadataGroup } from '@ionic/cli-framework'; - -import { CommandInstanceInfo, CommandLineInputs, CommandLineOptions, CommandMetadata } from '../../definitions'; -import { input } from '../../lib/color'; -import { Command } from '../../lib/command'; -import { FatalException } from '../../lib/errors'; -import { runCommand } from '../../lib/executor'; - -export class RegisterCommand extends Command { - async getMetadata(): Promise { - return { - name: 'register', - type: 'project', - groups: [MetadataGroup.PAID], - summary: 'Register your Product Key with this app', - options: [ - { - name: 'app-id', - summary: 'The Ionic App ID', - spec: { - value: 'id', - }, - }, - { - name: 'key', - summary: 'The Product Key', - }, - ], - }; - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions, runinfo: CommandInstanceInfo): Promise { - if (!this.project) { - throw new FatalException(`Cannot run ${input('ionic enterprise register')} outside a project directory.`); - } - - const appId = options['app-id'] ? String(options['app-id']) : undefined; - const key = options['key'] ? String(options['key']) : undefined; - - const extra = ['--']; - - if (key) { - extra.push('--key', key); - } - - if (appId) { - extra.push('--app-id', appId); - } - - await runCommand(runinfo, ['integrations', 'enable', 'enterprise', ...extra.length > 1 ? extra : [] ]); - } -} diff --git a/packages/@ionic/cli/src/commands/generate.ts b/packages/@ionic/cli/src/commands/generate.ts deleted file mode 100644 index 884b84d8ad..0000000000 --- a/packages/@ionic/cli/src/commands/generate.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { Footnote, MetadataGroup } from '@ionic/cli-framework'; - -import { CommandLineInputs, CommandLineOptions, CommandMetadata, CommandMetadataInput, CommandMetadataOption, CommandPreRun } from '../definitions'; -import { failure, input, strong } from '../lib/color'; -import { Command } from '../lib/command'; -import { FatalException } from '../lib/errors'; -import { prettyProjectName } from '../lib/project'; - -export class GenerateCommand extends Command implements CommandPreRun { - async getMetadata(): Promise { - const inputs: CommandMetadataInput[] = []; - const options: CommandMetadataOption[] = []; - const exampleCommands = ['']; - const footnotes: Footnote[] = []; - const groups: string[] = []; - - let description = this.project - ? failure(`Generators are not supported in this project type (${strong(prettyProjectName(this.project.type))}).`) - : failure('Generators help is available within an Ionic project directory.'); - - const runner = this.project && await this.project.getGenerateRunner(); - - if (runner) { - const libmetadata = await runner.getCommandMetadata(); - groups.push(...libmetadata.groups || []); - inputs.push(...libmetadata.inputs || []); - options.push(...libmetadata.options || []); - description = (libmetadata.description || '').trim(); - footnotes.push(...libmetadata.footnotes || []); - exampleCommands.push(...libmetadata.exampleCommands || []); - } else { - groups.push(MetadataGroup.HIDDEN); - } - - return { - name: 'generate', - type: 'project', - summary: 'Create Pages, Components, & Angular Features', - description, - footnotes, - inputs, - options, - groups, - exampleCommands, - }; - } - - async preRun(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - const runner = this.project && await this.project.getGenerateRunner(); - - if (runner) { - await runner.ensureCommandLine(inputs, options); - } - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - if (!this.project) { - throw new FatalException(`Cannot run ${input('ionic generate')} outside a project directory.`); - } - - const runner = await this.project.requireGenerateRunner(); - const opts = runner.createOptionsFromCommandLine(inputs, options); - await runner.run(opts); - } -} diff --git a/packages/@ionic/cli/src/commands/git/clone.ts b/packages/@ionic/cli/src/commands/git/clone.ts deleted file mode 100644 index 7917a80d59..0000000000 --- a/packages/@ionic/cli/src/commands/git/clone.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { MetadataGroup, validators } from '@ionic/cli-framework'; - -import { CommandLineInputs, CommandLineOptions, CommandMetadata } from '../../definitions'; -import { Command } from '../../lib/command'; - -// import { formatGitRepoUrl } from '../../lib/git'; - -export class GitCloneCommand extends Command { - async getMetadata(): Promise { - return { - name: 'clone', - type: 'global', - summary: 'Clones an Ionic app git repository to your computer', - inputs: [ - { - name: 'id', - summary: 'The ID of the Ionic Appflow app to clone', - validators: [validators.required], - }, - { - name: 'path', - summary: 'The destination directory of the cloned app', - }, - ], - groups: [MetadataGroup.HIDDEN], // TODO: make part of start? - }; - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - // let [ id, destination ] = inputs; - - // const appLoader = new App(this.env.session.getUserToken(), this.env.client); - // const app = await appLoader.load(id); - // const remote = await formatGitRepoUrl(this.env.config, app.id); - - // if (!destination) { - // destination = app.slug ? app.slug : app.id; - // } - - // destination = path.resolve(destination); - - // await this.env.shell.run('git', ['clone', '-o', 'ionic', remote, destination], { stdio: 'inherit' }); - - // this.env.log.ok(`Your app has been cloned to ${strong(prettyPath(destination))}!`); - } -} diff --git a/packages/@ionic/cli/src/commands/git/index.ts b/packages/@ionic/cli/src/commands/git/index.ts deleted file mode 100644 index 59550c89ee..0000000000 --- a/packages/@ionic/cli/src/commands/git/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { MetadataGroup } from '@ionic/cli-framework'; - -import { CommandMap, Namespace } from '../../lib/namespace'; - -export class GitNamespace extends Namespace { - async getMetadata() { - return { - name: 'git', - summary: 'Commands relating to managing Appflow git', - groups: [MetadataGroup.PAID], - }; - } - - async getCommands(): Promise { - return new CommandMap([ - ['clone', async () => { const { GitCloneCommand } = await import('./clone'); return new GitCloneCommand(this); }], - ['remote', async () => { const { GitRemoteCommand } = await import('./remote'); return new GitRemoteCommand(this); }], - ]); - } -} diff --git a/packages/@ionic/cli/src/commands/git/remote.ts b/packages/@ionic/cli/src/commands/git/remote.ts deleted file mode 100644 index 362d9586a2..0000000000 --- a/packages/@ionic/cli/src/commands/git/remote.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { MetadataGroup } from '@ionic/cli-framework'; - -import { CommandLineInputs, CommandLineOptions, CommandMetadata } from '../../definitions'; -import { input, strong } from '../../lib/color'; -import { Command } from '../../lib/command'; -import { FatalException } from '../../lib/errors'; - -export class GitRemoteCommand extends Command { - async getMetadata(): Promise { - const dashUrl = this.env.config.getDashUrl(); - - return { - name: 'remote', - type: 'project', - groups: [MetadataGroup.PAID], - summary: 'Adds/updates the Appflow git remote to your local Ionic app', - description: ` -This command is used by ${input('ionic link')} when Appflow is used as the git host. - -${input('ionic git remote')} will check the local repository for whether or not the git remote is properly set up. This command operates on the ${strong('ionic')} remote. For advanced configuration, see ${strong('Settings')} => ${strong('Git')} in the app settings of the Dashboard[^dashboard]. - `, - footnotes: [ - { - id: 'dashboard', - url: dashUrl, - }, - ], - }; - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - const { AppClient } = await import('../../lib/app'); - const { addIonicRemote, getIonicRemote, initializeRepo, isRepoInitialized, setIonicRemote } = await import('../../lib/git'); - - if (!this.project) { - throw new FatalException(`Cannot run ${input('ionic git remote')} outside a project directory.`); - } - - const token = await this.env.session.getUserToken(); - const id = await this.project.requireAppflowId(); - const appClient = new AppClient(token, this.env); - const app = await appClient.load(id); - - if (!app.repo_url) { - throw new FatalException(`Missing ${strong('repo_url')} property in app.`); - } - - if (!(await isRepoInitialized(this.project.directory))) { - await initializeRepo({ shell: this.env.shell }, this.project.directory); - - this.env.log.warn( - `Initializing a git repository for your project.\n` + - `Before your first ${input('git push ionic master')}, you'll want to commit all the files in your project:\n\n` + - `${input('git commit -a -m "Initial commit"')}\n` - ); - } - - const remote = app.repo_url; - const found = await getIonicRemote({ shell: this.env.shell }, this.project.directory); - - if (found) { - if (remote === found) { - this.env.log.msg(`Existing remote ${strong('ionic')} found.`); - } else { - await setIonicRemote({ shell: this.env.shell }, this.project.directory, remote); - this.env.log.ok(`Updated remote ${strong('ionic')}.`); - } - } else { - await addIonicRemote({ shell: this.env.shell }, this.project.directory, remote); - this.env.log.ok(`Added remote ${strong('ionic')}.`); - } - } -} diff --git a/packages/@ionic/cli/src/commands/help.ts b/packages/@ionic/cli/src/commands/help.ts deleted file mode 100644 index 8a7739520b..0000000000 --- a/packages/@ionic/cli/src/commands/help.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { MetadataGroup } from '@ionic/cli-framework'; - -import { CommandLineInputs, CommandLineOptions, CommandMetadata } from '../definitions'; -import { isCommand } from '../guards'; -import { input } from '../lib/color'; -import { Command } from '../lib/command'; - -export class HelpCommand extends Command { - async getMetadata(): Promise { - return { - name: 'help', - type: 'global', - summary: 'Provides help for a certain command', - exampleCommands: ['start'], - inputs: [ - { - name: 'command', - summary: 'The command you desire help with', - }, - ], - options: [ - { - name: 'json', - summary: 'Print help in JSON format', - type: Boolean, - }, - ], - groups: [MetadataGroup.HIDDEN], - }; - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - const { CommandSchemaHelpFormatter, CommandStringHelpFormatter, NamespaceSchemaHelpFormatter, NamespaceStringHelpFormatter } = await import('../lib/help'); - const location = await this.namespace.locate(inputs); - - if (isCommand(location.obj)) { - const formatterOptions = { location, command: location.obj }; - const formatter = options['json'] ? new CommandSchemaHelpFormatter(formatterOptions) : new CommandStringHelpFormatter(formatterOptions); - this.env.log.rawmsg(await formatter.format()); - } else { - if (location.args.length > 0) { - this.env.log.error( - `Unable to find command: ${input(inputs.join(' '))}` + - (this.project ? '' : '\nYou may need to be in an Ionic project directory.') - ); - } - - const now = new Date(); - const version = this.env.ctx.version; - const suffix = now.getMonth() === 9 && now.getDate() === 31 ? ' 🎃' : ''; - - const formatterOptions = { - inProject: this.project ? true : false, - version: version + suffix, - location, - namespace: location.obj, - }; - - const formatter = options['json'] ? new NamespaceSchemaHelpFormatter(formatterOptions) : new NamespaceStringHelpFormatter(formatterOptions); - this.env.log.rawmsg(await formatter.format()); - } - } -} diff --git a/packages/@ionic/cli/src/commands/index.ts b/packages/@ionic/cli/src/commands/index.ts deleted file mode 100644 index 4b8dcf6db8..0000000000 --- a/packages/@ionic/cli/src/commands/index.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { IProject, IonicEnvironment } from '../definitions'; -import { CommandMap, Namespace, NamespaceMap } from '../lib/namespace'; - -export interface IonicEnvironmentDeps { - readonly env: IonicEnvironment; - readonly project?: IProject; -} - -export class IonicNamespace extends Namespace { - protected _env: IonicEnvironment; - protected _project: IProject | undefined; - - constructor({ env, project }: IonicEnvironmentDeps) { - super(undefined); - this._env = env; - this._project = project; - } - - get project(): IProject | undefined { - return this._project; - } - - set project(p: IProject | undefined) { - this._project = p; - } - - get env(): IonicEnvironment { - return this._env; - } - - set env(env: IonicEnvironment) { - this._env = env; - } - - async getMetadata() { - return { - name: 'ionic', - summary: '', - }; - } - - async getNamespaces(): Promise { - return new NamespaceMap([ - ['config', async () => { const { ConfigNamespace } = await import('./config/index'); return new ConfigNamespace(this); }], - ['cordova', async () => { const { CordovaNamespace } = await import('./cordova/index'); return new CordovaNamespace(this); }], - ['capacitor', async () => { const { CapacitorNamespace } = await import('./capacitor/index'); return new CapacitorNamespace(this); }], - ['live-update', async () => { const { LiveUpdatesNamespace } = await import('./live-update/index'); return new LiveUpdatesNamespace(this); }], - ['git', async () => { const { GitNamespace } = await import('./git/index'); return new GitNamespace(this); }], - ['ssl', async () => { const { SSLNamespace } = await import('./ssl/index'); return new SSLNamespace(this); }], - ['ssh', async () => { const { SSHNamespace } = await import('./ssh/index'); return new SSHNamespace(this); }], - ['monitoring', async () => { const { MonitoringNamespace } = await import('./monitoring/index'); return new MonitoringNamespace(this); }], - ['integrations', async () => { const { IntegrationsNamespace } = await import('./integrations/index'); return new IntegrationsNamespace(this); }], - ['enterprise', async () => { const { EnterpriseNamespace } = await import('./enterprise/index'); return new EnterpriseNamespace(this); }], - ['cap', 'capacitor'], - ['cdv', 'cordova'], - ['i', 'integrations'], - ['integration', 'integrations'], - ]); - } - - async getCommands(): Promise { - return new CommandMap([ - ['build', async () => { const { BuildCommand } = await import('./build'); return new BuildCommand(this); }], - ['completion', async () => { const { CompletionCommand } = await import('./completion'); return new CompletionCommand(this); }], - ['generate', async () => { const { GenerateCommand } = await import('./generate'); return new GenerateCommand(this); }], - ['help', async () => { const { HelpCommand } = await import('./help'); return new HelpCommand(this); }], - ['info', async () => { const { InfoCommand } = await import('./info'); return new InfoCommand(this); }], - ['init', async () => { const { InitCommand } = await import('./init'); return new InitCommand(this); }], - ['ionitron', async () => { const { IonitronCommand } = await import('./ionitron'); return new IonitronCommand(this); }], - ['link', async () => { const { LinkCommand } = await import('./link'); return new LinkCommand(this); }], - ['login', async () => { const { LoginCommand } = await import('./login'); return new LoginCommand(this); }], - ['logout', async () => { const { LogoutCommand } = await import('./logout'); return new LogoutCommand(this); }], - ['repair', async () => { const { RepairCommand } = await import('./repair'); return new RepairCommand(this); }], - ['serve', async () => { const { ServeCommand } = await import('./serve'); return new ServeCommand(this); }], - ['share', async () => { const { ShareCommand } = await import('./share'); return new ShareCommand(this); }], - ['signup', async () => { const { SignupCommand } = await import('./signup'); return new SignupCommand(this); }], - ['start', async () => { const { StartCommand } = await import('./start'); return new StartCommand(this); }], - ['state', async () => { const { StateCommand } = await import('./state'); return new StateCommand(this); }], - ['telemetry', async () => { const { TelemetryCommand } = await import('./telemetry'); return new TelemetryCommand(this); }], - ['version', async () => { const { VersionCommand } = await import('./version'); return new VersionCommand(this); }], - ['g', 'generate'], - ['s', 'serve'], - ]); - } -} diff --git a/packages/@ionic/cli/src/commands/info.ts b/packages/@ionic/cli/src/commands/info.ts deleted file mode 100644 index caef5fe3d3..0000000000 --- a/packages/@ionic/cli/src/commands/info.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { strcmp } from '@ionic/cli-framework/utils/string'; -import { columnar } from '@ionic/utils-terminal'; -import * as lodash from 'lodash'; - -import { CommandLineInputs, CommandLineOptions, CommandMetadata, InfoItem, InfoItemGroup } from '../definitions'; -import { input, strong, weak } from '../lib/color'; -import { Command } from '../lib/command'; - -const INFO_GROUPS: readonly InfoItemGroup[] = ['ionic', 'capacitor', 'cordova', 'utility', 'system', 'environment']; - -export class InfoCommand extends Command { - async getMetadata(): Promise { - return { - name: 'info', - type: 'global', - summary: 'Print project, system, and environment information', - description: ` -This command is an easy way to share information about your setup. If applicable, be sure to run ${input('ionic info')} within your project directory to display even more information. - `, - options: [ - { - name: 'json', - summary: 'Print system/environment info in JSON format', - type: Boolean, - }, - ], - }; - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - const { json } = options; - - const items = (await this.env.getInfo()).filter(item => item.hidden !== true); - - if (json) { - process.stdout.write(JSON.stringify(items)); - } else { - const groupedInfo: Map = new Map( - INFO_GROUPS.map((group): [typeof group, InfoItem[]] => [group, items.filter(item => item.group === group)]) - ); - - const sortInfo = (a: InfoItem, b: InfoItem): number => { - if (a.name[0] === '@' && b.name[0] !== '@') { - return 1; - } - - if (a.name[0] !== '@' && b.name[0] === '@') { - return -1; - } - - return strcmp(a.name.toLowerCase(), b.name.toLowerCase()); - }; - - const projectPath = this.project && this.project.directory; - - const splitInfo = (ary: InfoItem[]) => ary - .sort(sortInfo) - .map((item): [string, string] => [` ${item.name}${item.flair ? ' ' + weak('(' + item.flair + ')') : ''}`, weak(item.value) + (item.path && projectPath && !item.path.startsWith(projectPath) ? ` ${weak('(' + item.path + ')')}` : '')]); - - const format = (details: [string, string][]) => columnar(details, { vsep: ':' }); - - if (!projectPath) { - this.env.log.warn('You are not in an Ionic project directory. Project context may be missing.'); - } - - this.env.log.nl(); - - for (const [ group, info ] of groupedInfo.entries()) { - if (info.length > 0) { - this.env.log.rawmsg(`${strong(`${lodash.startCase(group)}:`)}\n\n`); - this.env.log.rawmsg(`${format(splitInfo(info))}\n\n`); - } - } - } - } -} diff --git a/packages/@ionic/cli/src/commands/init.ts b/packages/@ionic/cli/src/commands/init.ts deleted file mode 100644 index 8a2fdc6c97..0000000000 --- a/packages/@ionic/cli/src/commands/init.ts +++ /dev/null @@ -1,231 +0,0 @@ -import { MetadataGroup, validators } from '@ionic/cli-framework'; -import { slugify } from '@ionic/cli-framework/utils/string'; -import { prettyPath } from '@ionic/utils-terminal'; -import * as path from 'path'; - -import { MODERN_PROJECT_TYPES, PROJECT_FILE } from '../constants'; -import { CommandLineInputs, CommandLineOptions, CommandMetadata, IProject, ProjectType } from '../definitions'; -import { input, strong, weak } from '../lib/color'; -import { Command } from '../lib/command'; -import { FatalException } from '../lib/errors'; -import { MultiProjectConfig, ProjectDetails, createProjectFromDetails, prettyProjectName } from '../lib/project'; - -export class InitCommand extends Command { - async getMetadata(): Promise { - return { - name: 'init', - type: 'global', - summary: 'Initialize existing projects with Ionic', - description: ` -This command will initialize an Ionic app within the current directory. Usually, this means an ${input(PROJECT_FILE)} file is created. If used within a multi-app project, the app is initialized in the root ${input(PROJECT_FILE)}. - -${input('ionic init')} will prompt for a project name and then proceed to determine the type of your project. You can specify the ${input('name')} argument and ${input('--type')} option to provide these values via command-line. - -If the ${input('--multi-app')} flag is specified, this command will initialize your project as a multi-app project, allowing for apps within monorepos and unconventional repository structures. See the multi-app docs[^multi-app-docs] for details. Once a multi-app project is initialized, you can run ${input('ionic init')} again within apps in your project to initialize them. - `, - exampleCommands: [ - '', - '"My App"', - '"My App" --type=angular', - '--multi-app', - ], - inputs: [ - { - name: 'name', - summary: `The name of your project (e.g. ${input('myApp')}, ${input('"My App"')})`, - }, - ], - options: [ - { - name: 'type', - summary: `Type of project (e.g. ${MODERN_PROJECT_TYPES.map(type => input(type)).join(', ')})`, - }, - { - name: 'force', - summary: 'Initialize even if a project already exists', - type: Boolean, - aliases: ['f'], - default: false, - }, - { - name: 'multi-app', - summary: 'Initialize a multi-app project', - type: Boolean, - default: false, - }, - { - name: 'project-id', - summary: 'Specify a slug for your app', - groups: [MetadataGroup.ADVANCED], - spec: { value: 'slug' }, - hint: weak('[multi-app]'), - }, - { - name: 'default', - summary: 'Mark the initialized app as the default project', - type: Boolean, - groups: [MetadataGroup.ADVANCED], - hint: weak('[multi-app]'), - }, - ], - groups: [MetadataGroup.BETA], - footnotes: [ - { - id: 'multi-app-docs', - url: 'https://ionicframework.com/docs/cli/configuration#multi-app-projects', - shortUrl: 'https://ion.link/multi-app-docs', - }, - ], - }; - } - - async preRun(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - const force = options['force'] ? true : false; - - if (this.project && !force) { - // TODO: check for existing project config in multi-app - - if (this.project.details.context === 'app' || (this.project.details.context === 'multiapp' && options['multi-app'])) { - throw new FatalException( - `Existing Ionic project file found: ${strong(prettyPath(this.project.filePath))}\n` + - `You can re-initialize your project using the ${input('--force')} option.` - ); - } - } - - if (!options['multi-app']) { - if (!inputs[0]) { - const name = await this.env.prompt({ - type: 'input', - name: 'name', - message: 'Project name:', - validate: v => validators.required(v), - }); - - inputs[0] = name; - } - - if (!options['type']) { - const details = new ProjectDetails({ rootDirectory: this.env.ctx.execPath, e: this.env }); - options['type'] = await details.getTypeFromDetection(); - } - - if (!options['type']) { - if (this.env.flags.interactive) { - this.env.log.warn( - `Could not determine project type.\n` + - `Please choose a project type from the list.` - ); - this.env.log.nl(); - } - - const type = await this.env.prompt({ - type: 'list', - name: 'type', - message: 'Project type:', - choices: MODERN_PROJECT_TYPES.map(t => ({ - name: `${prettyProjectName(t)} (${input(t)})`, - value: t, - })), - }); - - options['type'] = type; - } - } - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - if (options['multi-app']) { - await this.initializeMultiProject(inputs, options); - } else { - await this.initializeApp(inputs, options); - } - - this.env.log.ok('Your Ionic project has been initialized!'); - } - - async initializeMultiProject(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - const configPath = this.getProjectFilePath(); - const config = new MultiProjectConfig(configPath); - - config.c = { projects: {} }; - } - - async initializeApp(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - const name = inputs[0] ? inputs[0].trim() : ''; - const type = options['type'] ? String(options['type']) as ProjectType : undefined; - const projectId = options['project-id'] ? String(options['project-id']) : slugify(name); // TODO validate --project-id - - if (!name) { - throw new FatalException( - `Project name not specified.\n` + - `Please specify ${input('name')}, the first argument of ${input('ionic init')}. See ${input('ionic init --help')} for details.` - ); - } - - if (!type) { - throw new FatalException( - `Could not determine project type.\n` + - `Please specify ${input('--type')}. See ${input('ionic init --help')} for details.` - ); - } - - let project: IProject | undefined; - - if (this.project && this.project.details.context === 'multiapp') { - const configPath = path.resolve(this.project.rootDirectory, PROJECT_FILE); - const projectRoot = path.relative(this.project.rootDirectory, this.env.ctx.execPath); - const config = new MultiProjectConfig(configPath); - - if (!projectRoot) { - if (this.env.flags.interactive) { - this.env.log.warn( - `About to initialize app in the root directory of your multi-app project.\n` + - `Please confirm that you want your app initialized in the root of your multi-app project. If this wasn't intended, please ${input('cd')} into the appropriate directory and run ${input('ionic init')} again.\n` - ); - } - - const confirm = await this.env.prompt({ - type: 'confirm', - message: 'Continue?', - default: false, - }); - - if (!confirm) { - throw new FatalException(`Not initializing app in root directory.`); - } - } - - const defaultProject = config.get('defaultProject'); - - if (!defaultProject && typeof options['default'] !== 'boolean') { - const confirm = await this.env.prompt({ - type: 'confirm', - message: `Would you like to make this app the default project?`, - default: true, - }); - - if (confirm) { - options['default'] = true; - } - } - - if (options['default']) { - config.set('defaultProject', projectId); - } - - project = await createProjectFromDetails({ context: 'multiapp', configPath, id: projectId, type, errors: [] }, this.env); - project.config.set('root', projectRoot); - } else { - const configPath = this.getProjectFilePath(); - project = await createProjectFromDetails({ context: 'app', configPath, type, errors: [] }, this.env); - } - - project.config.set('name', name); - project.config.set('type', type); - } - - getProjectFilePath(): string { - return path.resolve(this.env.ctx.execPath, PROJECT_FILE); - } -} diff --git a/packages/@ionic/cli/src/commands/integrations/disable.ts b/packages/@ionic/cli/src/commands/integrations/disable.ts deleted file mode 100644 index cdb715e792..0000000000 --- a/packages/@ionic/cli/src/commands/integrations/disable.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { BaseError, contains, validators } from '@ionic/cli-framework'; - -import { CommandLineInputs, CommandLineOptions, CommandMetadata } from '../../definitions'; -import { isIntegrationName } from '../../guards'; -import { input } from '../../lib/color'; -import { Command } from '../../lib/command'; -import { FatalException } from '../../lib/errors'; -import { INTEGRATION_NAMES } from '../../lib/integrations'; - -export class IntegrationsDisableCommand extends Command { - async getMetadata(): Promise { - return { - name: 'disable', - type: 'project', - summary: 'Disable an integration', - description: ` -Integrations, such as Cordova, can be disabled with this command. - `, - inputs: [ - { - name: 'name', - summary: `The integration to disable (e.g. ${INTEGRATION_NAMES.map(i => input(i)).join(', ')})`, - validators: [validators.required, contains(INTEGRATION_NAMES, {})], - }, - ], - }; - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - const [ name ] = inputs; - - if (!this.project) { - throw new FatalException(`Cannot run ${input('ionic integrations disable')} outside a project directory.`); - } - - if (!isIntegrationName(name)) { - throw new FatalException(`Don't know about ${input(name)} integration!`); - } - - const integration = await this.project.createIntegration(name); - - try { - if (!integration.isAdded() || !integration.isEnabled()) { - this.env.log.info(`Integration ${input(name)} already disabled.`); - } else { - await integration.disable(); - this.env.log.ok(`Integration ${input(name)} disabled!`); - } - } catch (e: any) { - if (e instanceof BaseError) { - throw new FatalException(e.message); - } - - throw e; - } - } -} diff --git a/packages/@ionic/cli/src/commands/integrations/enable.ts b/packages/@ionic/cli/src/commands/integrations/enable.ts deleted file mode 100644 index e8ea0b9415..0000000000 --- a/packages/@ionic/cli/src/commands/integrations/enable.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { BaseError, contains, validators } from '@ionic/cli-framework'; -import * as path from 'path'; - -import { CommandLineInputs, CommandLineOptions, CommandMetadata } from '../../definitions'; -import { isIntegrationName } from '../../guards'; -import { input } from '../../lib/color'; -import { Command } from '../../lib/command'; -import { FatalException } from '../../lib/errors'; -import { INTEGRATION_NAMES } from '../../lib/integrations'; - -export class IntegrationsEnableCommand extends Command { - async getMetadata(): Promise { - return { - name: 'enable', - type: 'project', - summary: 'Add & enable integrations to your app', - description: ` -Integrations, such as Cordova, can be enabled with this command. If the integration has never been added to the project, ${input('ionic integrations enable')} will download and add the integration. - -Integrations can be re-added with the ${input('--add')} option. - `, - inputs: [ - { - name: 'name', - summary: `The integration to enable (e.g. ${INTEGRATION_NAMES.map(i => input(i)).join(', ')})`, - validators: [validators.required, contains(INTEGRATION_NAMES, {})], - }, - ], - options: [ - { - name: 'add', - summary: 'Download and add the integration even if enabled', - type: Boolean, - }, - { - name: 'root', - summary: 'Specify an alternative destination to download into when adding', - spec: { value: 'path' }, - }, - { - name: 'quiet', - summary: 'Less verbose output, ignore integration errors', - type: Boolean, - }, - ], - }; - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - const [ name ] = inputs; - const { add, quiet } = options; - - if (!this.project) { - throw new FatalException(`Cannot run ${input('ionic integrations enable')} outside a project directory.`); - } - - const root = options['root'] - ? path.resolve(this.project.rootDirectory, String(options['root'])) - : this.project.rootDirectory; - - if (!isIntegrationName(name)) { - throw new FatalException(`Don't know about ${input(name)} integration!`); - } - - const integration = await this.project.createIntegration(name); - - try { - if (!integration.isAdded() || add) { - await integration.add({ - root, - enableArgs: options['--'] ? options['--'] : undefined, - quiet: Boolean(quiet), - }); - - this.env.log.ok(`Integration ${input(integration.name)} added!`); - } else { - const wasEnabled = integration.config.get('enabled') !== false; - - // We still need to run this whenever this command is run to make sure - // everything is good with the integration. - await integration.enable(); - - if (wasEnabled) { - this.env.log.info(`Integration ${input(integration.name)} already enabled.`); - } else { - this.env.log.ok(`Integration ${input(integration.name)} enabled!`); - } - } - } catch (e: any) { - if (e instanceof BaseError) { - if (quiet) { - this.env.log.error(e.message); - } else { - throw new FatalException(e.message); - } - } else { - throw e; - } - } - } -} diff --git a/packages/@ionic/cli/src/commands/integrations/index.ts b/packages/@ionic/cli/src/commands/integrations/index.ts deleted file mode 100644 index 691ddab91e..0000000000 --- a/packages/@ionic/cli/src/commands/integrations/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { CommandMap, Namespace } from '../../lib/namespace'; - -export class IntegrationsNamespace extends Namespace { - async getMetadata() { - return { - name: 'integrations', - summary: 'Manage various integrations in your app', - description: 'Integrations, such as Cordova, can be enabled or disabled in your app with these commands.', - }; - } - - async getCommands(): Promise { - return new CommandMap([ - ['enable', async () => { const { IntegrationsEnableCommand } = await import('./enable'); return new IntegrationsEnableCommand(this); }], - ['disable', async () => { const { IntegrationsDisableCommand } = await import('./disable'); return new IntegrationsDisableCommand(this); }], - ['list', async () => { const { IntegrationsListCommand } = await import('./list'); return new IntegrationsListCommand(this); }], - ['ls', 'list'], - ['en', 'enable'], - ['add', 'enable'], - ['dis', 'disable'], - ['delete', 'disable'], - ['del', 'disable'], - ['remove', 'disable'], - ['rm', 'disable'], - ]); - } -} diff --git a/packages/@ionic/cli/src/commands/integrations/list.ts b/packages/@ionic/cli/src/commands/integrations/list.ts deleted file mode 100644 index f629df1ca0..0000000000 --- a/packages/@ionic/cli/src/commands/integrations/list.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { columnar } from '@ionic/utils-terminal'; -import chalk from 'chalk'; - -import { CommandLineInputs, CommandLineOptions, CommandMetadata, IntegrationName } from '../../definitions'; -import { input, strong } from '../../lib/color'; -import { Command } from '../../lib/command'; -import { FatalException } from '../../lib/errors'; -import { INTEGRATION_NAMES } from '../../lib/integrations'; - -export class IntegrationsListCommand extends Command { - async getMetadata(): Promise { - return { - name: 'list', - type: 'project', - summary: 'List available and active integrations in your app', - description: ` -This command will print the status of integrations in Ionic projects. Integrations can be ${strong('enabled')} (added and enabled), ${strong('disabled')} (added but disabled), and ${strong('not added')} (never added to the project). - -- To enable or add integrations, see ${input('ionic integrations enable --help')} -- To disable integrations, see ${input('ionic integrations disable --help')} - `, - }; - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - const { project } = this; - - if (!project) { - throw new FatalException(`Cannot run ${input('ionic integrations list')} outside a project directory.`); - } - - const integrations = await Promise.all(INTEGRATION_NAMES.map(async name => project.createIntegration(name))); - - const status = (name: IntegrationName) => { - const c = project.config.get('integrations')[name]; - - if (c) { - if (c.enabled === false) { - return chalk.dim.red('disabled'); - } - - return chalk.green('enabled'); - } - - return chalk.dim('not added'); - }; - - this.env.log.rawmsg(columnar(integrations.map(i => [input(i.name), i.summary, status(i.name)]), { headers: ['name', 'summary', 'status'] })); - } -} diff --git a/packages/@ionic/cli/src/commands/ionitron.ts b/packages/@ionic/cli/src/commands/ionitron.ts deleted file mode 100644 index b6afa4877c..0000000000 --- a/packages/@ionic/cli/src/commands/ionitron.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { MetadataGroup } from '@ionic/cli-framework'; - -import { CommandLineInputs, CommandLineOptions, CommandMetadata } from '../definitions'; -import { Command } from '../lib/command'; - -export class IonitronCommand extends Command { - async getMetadata(): Promise { - return { - name: 'ionitron', - type: 'global', - summary: 'Print random ionitron messages', - options: [ - { - name: 'es', - summary: 'Print in spanish', - type: Boolean, - }, - ], - groups: [MetadataGroup.HIDDEN], - }; - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - const { getIonitronString, ionitronStatements } = await import('../lib/ionitron'); - - const locale = options['es'] ? 'es' : 'en'; - const localeStatements = ionitronStatements[locale]; - const statement = localeStatements[Math.floor(Math.random() * (localeStatements.length))]; - - this.env.log.rawmsg(getIonitronString(statement)); - } -} diff --git a/packages/@ionic/cli/src/commands/link.ts b/packages/@ionic/cli/src/commands/link.ts deleted file mode 100644 index d9e4e5ad63..0000000000 --- a/packages/@ionic/cli/src/commands/link.ts +++ /dev/null @@ -1,622 +0,0 @@ -import { MetadataGroup, validators } from '@ionic/cli-framework'; -import { createPromptChoiceSeparator } from '@ionic/cli-framework-prompts'; -import { prettyPath } from '@ionic/utils-terminal'; -import { debug as Debug } from 'debug'; - -import { PROJECT_FILE } from '../constants'; -import { App, CommandInstanceInfo, CommandLineInputs, CommandLineOptions, CommandMetadata, CommandPreRun, GithubBranch, GithubRepo } from '../definitions'; -import { isSuperAgentError } from '../guards'; -import { ancillary, input, strong, weak } from '../lib/color'; -import { Command } from '../lib/command'; -import { FatalException } from '../lib/errors'; -import { runCommand } from '../lib/executor'; -import { openUrl } from '../lib/open'; - -const debug = Debug('ionic:commands:link'); - -const CHOICE_CREATE_NEW_APP = 'createNewApp'; -const CHOICE_NEVERMIND = 'nevermind'; - -const CHOICE_RELINK = 'relink'; -const CHOICE_LINK_EXISTING_APP = 'linkExistingApp'; - -const CHOICE_IONIC = 'ionic'; -const CHOICE_GITHUB = 'github'; -const CHOICE_SKIP = 'skip' - -const CHOICE_MASTER_ONLY = 'master'; -const CHOICE_SPECIFIC_BRANCHES = 'specific'; - -export class LinkCommand extends Command implements CommandPreRun { - async getMetadata(): Promise { - const projectFile = this.project ? prettyPath(this.project.filePath) : PROJECT_FILE; - - return { - name: 'link', - type: 'project', - groups: [MetadataGroup.PAID], - summary: 'Connect local apps to Ionic', - description: ` -Link apps on Appflow to local Ionic projects with this command. - -If the ${input('id')} argument is excluded, this command will prompt you to select an app from Appflow. - -Appflow uses a git-based workflow to manage app updates. During the linking process, select ${strong('GitHub')} (recommended) or ${strong('Appflow')} as a git host. See our documentation[^appflow-git-basics] for more information. - -Ultimately, this command sets the ${strong('id')} property in ${strong(prettyPath(projectFile))}, which marks this app as linked. - -If you are having issues linking, please get in touch with our Support[^support-request]. - `, - footnotes: [ - { - id: 'appflow-git-basics', - url: 'https://ionicframework.com/docs/appflow/basics/git', - shortUrl: 'https://ion.link/appflow-git-basics', - }, - { - id: 'support-request', - url: 'https://ion.link/support-request', - }, - ], - exampleCommands: ['', 'a1b2c3d4'], - inputs: [ - { - name: 'id', - summary: `The Appflow ID of the app to link (e.g. ${input('a1b2c3d4')})`, - }, - ], - options: [ - { - name: 'name', - summary: 'The app name to use during the linking of a new app', - groups: [MetadataGroup.HIDDEN], - }, - { - name: 'create', - summary: 'Create a new app on Ionic Appflow and link it with this local Ionic project', - type: Boolean, - groups: [MetadataGroup.HIDDEN], - }, - { - name: 'pro-id', - summary: 'Specify an app ID from the Ionic Appflow to link', - groups: [MetadataGroup.DEPRECATED, MetadataGroup.HIDDEN], - spec: { value: 'id' }, - }, - ], - }; - } - - async preRun(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - const { create } = options; - - if (inputs[0] && create) { - throw new FatalException(`Sorry--cannot use both ${input('id')} and ${input('--create')}. You must either link an existing app or create a new one.`); - } - - const id = options['pro-id'] ? String(options['pro-id']) : undefined; - - if (id) { - inputs[0] = id; - } - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions, runinfo: CommandInstanceInfo): Promise { - const { promptToLogin } = await import('../lib/session'); - - if (!this.project) { - throw new FatalException(`Cannot run ${input('ionic link')} outside a project directory.`); - } - - let id: string | undefined = inputs[0]; - let { create } = options; - - const idFromConfig = this.project.config.get('id'); - - if (idFromConfig) { - if (id && idFromConfig === id) { - this.env.log.msg(`Already linked with app ${input(id)}.`); - return; - } - - const msg = id ? - `Are you sure you want to link it to ${input(id)} instead?` : - `Would you like to run link again?`; - - const confirm = await this.env.prompt({ - type: 'confirm', - name: 'confirm', - message: `App ID ${input(idFromConfig)} is already set up with this app. ${msg}`, - }); - - if (!confirm) { - this.env.log.msg('Not linking.'); - return; - } - } - - if (!this.env.session.isLoggedIn()) { - await promptToLogin(this.env); - } - - if (!id && !create) { - const choices = [ - { - name: `Link ${idFromConfig ? 'a different' : 'an existing'} app on Ionic Appflow`, - value: CHOICE_LINK_EXISTING_APP, - }, - { - name: 'Create a new app on Ionic Appflow', - value: CHOICE_CREATE_NEW_APP, - }, - ]; - - if (idFromConfig) { - choices.unshift({ - name: `Relink ${input(idFromConfig)}`, - value: CHOICE_RELINK, - }); - } - - const result = await this.env.prompt({ - type: 'list', - name: 'whatToDo', - message: 'What would you like to do?', - choices, - }); - - if (result === CHOICE_CREATE_NEW_APP) { - create = true; - id = undefined; - } else if (result === CHOICE_LINK_EXISTING_APP) { - const tasks = this.createTaskChain(); - tasks.next(`Looking up your apps`); - const apps: App[] = []; - - const appClient = await this.getAppClient(); - const paginator = appClient.paginate(); - - for (const r of paginator) { - const res = await r; - apps.push(...res.data); - } - - tasks.end(); - - if (apps.length === 0) { - const confirm = await this.env.prompt({ - type: 'confirm', - name: 'confirm', - message: `No apps found. Would you like to create a new app on Ionic Appflow?`, - }); - - if (!confirm) { - throw new FatalException(`Cannot link without an app selected.`); - } - - create = true; - id = undefined; - } else { - const choice = await this.chooseApp(apps); - - if (choice === CHOICE_NEVERMIND) { - this.env.log.info('Not linking app.'); - id = undefined; - } else { - id = choice; - } - } - } else if (result === CHOICE_RELINK) { - id = idFromConfig; - } - } - - if (create) { - let name = options['name'] ? String(options['name']) : undefined; - - if (!name) { - name = await this.env.prompt({ - type: 'input', - name: 'name', - message: 'Please enter a name for your new app:', - validate: v => validators.required(v), - }); - } - - id = await this.createApp({ name }, runinfo); - } else if (id) { - const app = await this.lookUpApp(id); - await this.linkApp(app, runinfo); - } - } - - private async getAppClient() { - const { AppClient } = await import('../lib/app'); - const token = await this.env.session.getUserToken(); - return new AppClient(token, this.env); - } - - private async getUserClient() { - const { UserClient } = await import('../lib/user'); - const token = await this.env.session.getUserToken(); - return new UserClient(token, this.env); - } - - async lookUpApp(id: string): Promise { - const tasks = this.createTaskChain(); - tasks.next(`Looking up app ${input(id)}`); - - const appClient = await this.getAppClient(); - const app = await appClient.load(id); // Make sure the user has access to the app - - tasks.end(); - - return app; - } - - async createApp({ name }: { name: string; }, runinfo: CommandInstanceInfo): Promise { - const appClient = await this.getAppClient(); - const org_id = this.env.config.get('org.id'); - const app = await appClient.create({ name, org_id }); - - await this.linkApp(app, runinfo); - - return app.id; - } - - async linkApp(app: App, runinfo: CommandInstanceInfo) { - // TODO: load connections - - // TODO: check for git availability before this - - this.env.log.nl(); - - this.env.log.info( - `Ionic Appflow uses a git-based workflow to manage app updates.\n` + - `You will be prompted to set up the git host and repository for this new app. See the docs${ancillary('[1]')} for more information.\n\n` + - `${ancillary('[1]')}: ${strong('https://ion.link/appflow-git-basics')}` - ); - - this.env.log.nl(); - - const service = await this.env.prompt({ - type: 'list', - name: 'gitService', - message: 'Which git host would you like to use?', - choices: [ - { - name: 'GitHub', - value: CHOICE_GITHUB, - }, - { - name: 'Ionic Appflow', - value: CHOICE_IONIC, - }, - { - name: 'Don\'t use a git host', - value: CHOICE_SKIP, - }, - ], - }); - - let githubUrl: string | undefined; - if (service === CHOICE_IONIC) { - if (!this.env.config.get('git.setup')) { - await runCommand(runinfo, ['ssh', 'setup']); - } - - await runCommand(runinfo, ['config', 'set', 'id', `"${app.id}"`, '--json']); - await runCommand(runinfo, ['git', 'remote']); - } else { - if (service === CHOICE_GITHUB) { - githubUrl = await this.linkGithub(app); - } - - await runCommand(runinfo, ['config', 'set', 'id', `"${app.id}"`, '--json']); - } - - this.env.log.ok(`Project linked with app ${input(app.id)}!`); - if (service === CHOICE_GITHUB) { - this.env.log.info( - `Here are some additional links that can help you with you first push to GitHub:\n` + - `${strong('Adding GitHub as a remote')}:\n\t${strong('https://help.github.com/articles/adding-a-remote/')}\n\n` + - `${strong('Pushing to a remote')}:\n\t${strong('https://help.github.com/articles/pushing-to-a-remote/')}\n\n` + - `${strong('Working with branches')}:\n\t${strong('https://guides.github.com/introduction/flow/')}\n\n` + - `${strong('More comfortable with a GUI? Try GitHub Desktop!')}\n\t${strong('https://desktop.github.com/')}` - ); - - if (githubUrl) { - this.env.log.info( - `You can now push to one of your branches on GitHub to trigger a build in Ionic Appflow!\n` + - `If you haven't added GitHub as your origin you can do so by running:\n\n` + - `${input('git remote add origin ' + githubUrl)}\n\n` + - `You can find additional links above to help if you're having issues.` - ); - } - } - } - - async linkGithub(app: App): Promise { - const { id } = this.env.session.getUser(); - - const userClient = await this.getUserClient(); - const user = await userClient.load(id, { fields: ['oauth_identities'] }); - - if (!user.oauth_identities || !user.oauth_identities.github) { - await this.oAuthProcess(id); - } - - if (await this.needsAssociation(app, user.id)) { - await this.confirmGithubRepoExists(); - const repoId = await this.selectGithubRepo(); - const branches = await this.selectGithubBranches(repoId); - return this.connectGithub(app, repoId, branches); - } - } - - async confirmGithubRepoExists() { - let confirm = false; - - this.env.log.nl(); - this.env.log.info(strong(`In order to link to a GitHub repository the repository must already exist on GitHub.`)); - this.env.log.info( - `${strong('If the repository does not exist please create one now before continuing.')}\n` + - `If you're not familiar with Git you can learn how to set it up with GitHub here:\n\n` + - strong(`https://help.github.com/articles/set-up-git/ \n\n`) + - `You can find documentation on how to create a repository on GitHub and push to it here:\n\n` + - strong(`https://help.github.com/articles/create-a-repo/`) - ); - - confirm = await this.env.prompt({ - type: 'confirm', - name: 'confirm', - message: 'Does the repository exist on GitHub?', - }); - - if (!confirm) { - throw new FatalException(`Repo must exist on GitHub in order to link. Please create the repo and run ${input('ionic link')} again.`); - } - } - - async oAuthProcess(userId: number) { - const userClient = await this.getUserClient(); - - let confirm = false; - - this.env.log.nl(); - this.env.log.info( - `GitHub OAuth setup required.\n` + - `To continue, we need you to authorize Ionic Appflow with your GitHub account. ` + - `A browser will open and prompt you to complete the authorization request. ` + - `When finished, please return to the CLI to continue linking your app.` - ); - - confirm = await this.env.prompt({ - type: 'confirm', - name: 'ready', - message: 'Open browser:', - }); - - if (!confirm) { - throw new FatalException(`GitHub OAuth setup is required to link to GitHub repository. Please run ${input('ionic link')} again when ready.`); - } - - const url = await userClient.oAuthGithubLogin(userId); - await openUrl(url); - - confirm = await this.env.prompt({ - type: 'confirm', - name: 'ready', - message: 'Authorized and ready to continue:', - }); - - if (!confirm) { - throw new FatalException(`GitHub OAuth setup is required to link to GitHub repository. Please run ${input('ionic link')} again when ready.`); - } - } - - async needsAssociation(app: App, userId: number): Promise { - const appClient = await this.getAppClient(); - - if (app.association && app.association.repository.html_url) { - this.env.log.msg(`App ${input(app.id)} already connected to ${strong(app.association.repository.html_url)}`); - - const confirm = await this.env.prompt({ - type: 'confirm', - name: 'confirm', - message: 'Would you like to connect a different repo?', - }); - - if (!confirm) { - return false; - } - - try { - // TODO: maybe we can use a PUT instead of DELETE now + POST later? - await appClient.deleteAssociation(app.id); - } catch (e: any) { - if (isSuperAgentError(e)) { - if (e.response.status === 401) { - await this.oAuthProcess(userId); - await appClient.deleteAssociation(app.id); - return true; - } else if (e.response.status === 404) { - debug(`DELETE ${app.id} GitHub association not found`); - return true; - } - } - - throw e; - } - } - - return true; - } - - async connectGithub(app: App, repoId: number, branches: string[]): Promise { - const appClient = await this.getAppClient(); - - try { - const association = await appClient.createAssociation(app.id, { repoId, type: 'github', branches }); - this.env.log.ok(`App ${input(app.id)} connected to ${strong(association.repository.html_url)}`); - return association.repository.html_url; - } catch (e: any) { - if (isSuperAgentError(e) && e.response.status === 403) { - throw new FatalException(e.response.body.error.message); - } - } - } - - formatRepoName(fullName: string) { - const [org, name] = fullName.split('/'); - - return `${weak(`${org} /`)} ${name}`; - } - - async chooseApp(apps: App[]): Promise { - const { formatName } = await import('../lib/app'); - - const neverMindChoice = { - name: strong('Nevermind'), - id: CHOICE_NEVERMIND, - value: CHOICE_NEVERMIND, - org: null, - }; - - const linkedApp = await this.env.prompt({ - type: 'list', - name: 'linkedApp', - message: 'Which app would you like to link', - choices: [ - ...apps.map(app => ({ - name: `${formatName(app)} ${weak(`(${app.id})`)}`, - value: app.id, - })), - createPromptChoiceSeparator(), - neverMindChoice, - createPromptChoiceSeparator(), - ], - }); - - return linkedApp; - } - - async selectGithubRepo(): Promise { - const user = this.env.session.getUser(); - const userClient = await this.getUserClient(); - - const tasks = this.createTaskChain(); - const task = tasks.next('Looking up your GitHub repositories'); - - const paginator = userClient.paginateGithubRepositories(user.id); - const repos: GithubRepo[] = []; - - try { - for (const r of paginator) { - const res = await r; - repos.push(...res.data); - - task.msg = `Looking up your GitHub repositories: ${strong(String(repos.length))} found`; - } - } catch (e: any) { - tasks.fail(); - - if (isSuperAgentError(e) && e.response.status === 401) { - await this.oAuthProcess(user.id); - return this.selectGithubRepo(); - } - - throw e; - } - - tasks.end(); - - const repoId = await this.env.prompt({ - type: 'list', - name: 'githubRepo', - message: 'Which GitHub repository would you like to link?', - choices: repos.map(repo => ({ - name: this.formatRepoName(repo.full_name), - value: String(repo.id), - })), - }); - - return Number(repoId); - } - - async selectGithubBranches(repoId: number): Promise { - this.env.log.nl(); - this.env.log.info(strong(`By default Ionic Appflow links only to the ${input('master')} branch.`)); - this.env.log.info( - `${strong('If you\'d like to link to another branch or multiple branches you\'ll need to select each branch to connect to.')}\n` + - `If you're not familiar with on working with branches in GitHub you can read about them here:\n\n` + - strong(`https://guides.github.com/introduction/flow/ \n\n`) - ); - - const choice = await this.env.prompt({ - type: 'list', - name: 'githubMultipleBranches', - message: 'Which would you like to do?', - choices: [ - { - name: `Link to master branch only`, - value: CHOICE_MASTER_ONLY, - }, - { - name: `Link to specific branches`, - value: CHOICE_SPECIFIC_BRANCHES, - }, - ], - }); - - switch (choice) { - case CHOICE_MASTER_ONLY: - return ['master']; - case CHOICE_SPECIFIC_BRANCHES: - // fall through and begin prompting to choose branches - break; - default: - throw new FatalException('Aborting. No branch choice specified.'); - } - - const user = this.env.session.getUser(); - const userClient = await this.getUserClient(); - const paginator = userClient.paginateGithubBranches(user.id, repoId); - const tasks = this.createTaskChain(); - const task = tasks.next('Looking for available branches'); - const availableBranches: GithubBranch[] = []; - try { - for (const r of paginator) { - const res = await r; - availableBranches.push(...res.data); - - task.msg = `Looking up the available branches on your GitHub repository: ${strong(String(availableBranches.length))} found`; - } - } catch (e: any) { - tasks.fail(); - throw e; - } - tasks.end(); - - const choices = availableBranches.map(branch => ({ - name: branch.name, - value: branch.name, - checked: branch.name === 'master', - })); - - if (choices.length === 0) { - this.env.log.warn(`No branches found for the repository. Linking to ${input('master')} branch.`); - return ['master']; - } - - const selectedBranches = await this.env.prompt({ - type: 'checkbox', - name: 'githubBranches', - message: 'Which branch would you like to link?', - choices, - default: ['master'], - }); - - return selectedBranches; - } -} diff --git a/packages/@ionic/cli/src/commands/live-update/add.ts b/packages/@ionic/cli/src/commands/live-update/add.ts deleted file mode 100644 index b25dc7fba6..0000000000 --- a/packages/@ionic/cli/src/commands/live-update/add.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { MetadataGroup } from '@ionic/cli-framework'; - -import { CommandInstanceInfo, CommandLineInputs, CommandLineOptions, CommandMetadata } from '../../definitions'; -import { input } from '../../lib/color'; -import { runCommand } from '../../lib/executor'; - -import { LiveUpdatesConfCommand } from './core'; - -export class AddCommand extends LiveUpdatesConfCommand { - async getMetadata(): Promise { - - return { - name: 'add', - type: 'project', - groups: [MetadataGroup.PAID], - summary: 'Adds Ionic Live Updates plugin to the project', - description: ` -This command adds the Ionic Live Updates plugin (${input('cordova-plugin-ionic')}) for both Capacitor and Cordova projects. - -For Capacitor projects it runs all the steps necessary to install the plugin, sync with the native projects and add the configuration to the proper iOS and Android configuration files. - -For Cordova projects it just takes care of running the proper Cordova CLI command with the submitted parameters. - `, - exampleCommands: [ - '', - '--app-id=abcd1234 --channel-name="Master" --update-method=background', - '--max-store=2 --min-background-duration=30', - '--app-id=abcd1234 --channel-name="Master" --update-method=background --max-store=2 --min-background-duration=30', - ], - options: [ - { - name: 'app-id', - summary: 'Your Appflow app ID', - type: String, - spec: { value: 'id' }, - }, - { - name: 'channel-name', - summary: 'The channel to check for updates from', - type: String, - spec: { value: 'name' }, - }, - { - name: 'update-method', - summary: 'The update method that dictates the behavior of the plugin', - type: String, - spec: { value: 'name' }, - }, - { - name: 'max-store', - summary: 'The maximum number of downloaded versions to store on the device', - type: String, - groups: [MetadataGroup.ADVANCED], - spec: { value: 'quantity' }, - default: '2', - }, - { - name: 'min-background-duration', - summary: 'The minimum duration after which the app checks for an update in the background', - type: String, - groups: [MetadataGroup.ADVANCED], - spec: { value: 'seconds' }, - default: '30', - }, - { - name: 'update-api', - summary: 'The location of the Appflow API', - type: String, - groups: [MetadataGroup.HIDDEN], - spec: { value: 'url' }, - default: 'https://api.ionicjs.com', - }, - ], - }; - } - - protected buildCordovaLiveUpdateOptions(options: CommandLineOptions): string[] { - const optionsToCordova = { - 'app-id': 'APP_ID', - 'channel-name': 'CHANNEL_NAME', - 'update-method': 'UPDATE_METHOD', - 'max-store': 'MAX_STORE', - 'min-background-duration': 'MIN_BACKGROUND_DURATION', - 'update-api': 'UPDATE_API', - }; - const outputOptions = []; - for (const [optionKey, cordovaKey] of Object.entries(optionsToCordova)) { - if (options[optionKey]) { - outputOptions.push(`--variable`); - outputOptions.push(`${cordovaKey}=${options[optionKey]}`); - } - } - return outputOptions; - } - - async preRun(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - // check if there are native integration installed - await this.requireNativeIntegration(); - await this.preRunCheckInputs(options); - } - - async addPlugin(options: CommandLineOptions, runinfo: CommandInstanceInfo, integration?: string) { - if (integration === 'cordova') { - let addPluginCommand = ['cordova', 'plugin', 'add', 'cordova-plugin-ionic']; - const userOptions = this.buildCordovaLiveUpdateOptions(options); - if (userOptions) { - addPluginCommand = addPluginCommand.concat(userOptions); - } - await runCommand(runinfo, addPluginCommand); - } - if (integration === 'capacitor') { - const { pkgManagerArgs } = await import('../../lib/utils/npm'); - const [ installer, ...installerArgs ] = await pkgManagerArgs( - this.env.config.get('npmClient'), - { command: 'install', pkg: 'cordova-plugin-ionic' } - ); - // install the plugin with npm - await this.env.shell.run(installer, installerArgs, { stdio: 'inherit' }); - } - - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions, runinfo: CommandInstanceInfo): Promise { - const integration = await this.getAppIntegration(); - - // check if it is already installed - const alreadyAdded = await this.checkLiveUpdatesInstalled(); - if (!alreadyAdded) { - await this.addPlugin(options, runinfo, integration); - } else { - this.env.log.warn("Live Updates plugin already added. Reconfiguring only."); - } - - if (integration === 'capacitor') { - // generate the manifest - await runCommand(runinfo, ['live-update', 'manifest']); - // run capacitor sync - await runCommand(runinfo, ['capacitor', 'sync']); - // update the ios project if present - await this.addConfToIosPlist(options); - // update the android project if present - await this.addConfToAndroidString(options); - } - - this.env.log.ok(`Ionic Live Updates plugin added to the project!\n`); - } - -} diff --git a/packages/@ionic/cli/src/commands/live-update/capacitor.ts b/packages/@ionic/cli/src/commands/live-update/capacitor.ts deleted file mode 100644 index d1fe2cedd7..0000000000 --- a/packages/@ionic/cli/src/commands/live-update/capacitor.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { BaseConfig } from "@ionic/cli-framework"; -import lodash = require("lodash"); - -export interface CapacitorConfig { - appId?: string; - appName?: string; - webDir?: string; - server?: { - url?: string; - originalUrl?: string; - }; -} - -export interface CapacitorCLIConfig { - android: { - platformDirAbs: string; - srcMainDirAbs: string; - assetsDirAbs: string; - }; - ios: { - platformDirAbs: string; - nativeTargetDirAbs: string; - }; - app: { - extConfig: CapacitorConfig; - }; -} - -export class CapacitorJSONConfig extends BaseConfig { - constructor(p: string) { - super(p, { spaces: "\t" }); - } - - provideDefaults(config: CapacitorConfig): CapacitorConfig { - return config; - } - - setServerUrl(url: string): void { - const serverConfig = this.get("server") || {}; - - if (typeof serverConfig.url === "string") { - serverConfig.originalUrl = serverConfig.url; - } - - serverConfig.url = url; - - this.set("server", serverConfig); - } - - resetServerUrl(): void { - const serverConfig = this.get("server") || {}; - - delete serverConfig.url; - - if (serverConfig.originalUrl) { - serverConfig.url = serverConfig.originalUrl; - delete serverConfig.originalUrl; - } - - if (lodash.isEmpty(serverConfig)) { - this.unset("server"); - } else { - this.set("server", serverConfig); - } - } -} diff --git a/packages/@ionic/cli/src/commands/live-update/configure.ts b/packages/@ionic/cli/src/commands/live-update/configure.ts deleted file mode 100644 index 078d9f5c3a..0000000000 --- a/packages/@ionic/cli/src/commands/live-update/configure.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { CommandLineInputs, CommandLineOptions, MetadataGroup } from '@ionic/cli-framework'; - -import { CommandInstanceInfo, CommandMetadata } from '../../definitions'; -import { input } from '../../lib/color'; -import { FatalException } from '../../lib/errors'; - - -import { LiveUpdatesConfCommand } from './core'; - -export class ConfigureCommand extends LiveUpdatesConfCommand { - async getMetadata(): Promise { - return { - name: 'configure', - type: 'project', - groups: [MetadataGroup.PAID], - summary: 'Overrides Ionic Live Updates plugin configuration', - description: ` -This command overrides configuration for the Ionic Live Updates plugin (${input('cordova-plugin-ionic')}) in Capacitor projects. - -For Capacitor projects, if the plugin is already installed, it overrides the configuration variables in the native projects. - -For Cordova projects this is not implemented because it is better to reinstall the plugin with the different parameters and let Cordova deal with the changes. - `, - exampleCommands: [ - '', - '--app-id=abcd1234 --channel-name="Master" --update-method=background', - '--max-store=2 --min-background-duration=30', - '--app-id=abcd1234 --channel-name="Master" --update-method=background --max-store=2 --min-background-duration=30', - 'android', - 'ios', - ], - options: [ - { - name: 'app-id', - summary: 'Your Appflow app ID', - type: String, - spec: { value: 'id' }, - }, - { - name: 'channel-name', - summary: 'The channel to check for updates from', - type: String, - spec: { value: 'name' }, - }, - { - name: 'update-method', - summary: 'The update method that dictates the behavior of the plugin', - type: String, - spec: { value: 'name' }, - }, - { - name: 'max-store', - summary: 'The maximum number of downloaded versions to store on the device', - type: String, - groups: [MetadataGroup.ADVANCED], - spec: { value: 'quantity' }, - }, - { - name: 'min-background-duration', - summary: 'The minimum duration after which the app checks for an update in the background', - type: String, - groups: [MetadataGroup.ADVANCED], - spec: { value: 'seconds' }, - }, - { - name: 'update-api', - summary: 'The location of the Appflow API', - type: String, - groups: [MetadataGroup.HIDDEN], - spec: { value: 'url' }, - }, - ], - inputs: [ - { - name: 'platform', - summary: `The native platform (e.g. ${['ios', 'android'].map(v => input(v)).join(', ')})`, - }, - ], - }; - } - - async preRun(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - // check if it is already installed - const alreadyAdded = await this.checkLiveUpdatesInstalled(); - if (!alreadyAdded) { - throw new FatalException( - `Live Updates (cordova-plugin-ionic) not yet installed.\n` + - `Please run ${input('ionic live-update add')}` - ); - } - // check if there are native integration installed - await this.requireNativeIntegration(); - // check that if an input is provided, it is valid - if (inputs[0] && !['ios', 'android'].includes(inputs[0])) { - throw new FatalException(`Only ${input('ios')} or ${input('android')} can be used.`); - } - await this.preRunCheckInputs(options); - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions, runinfo: CommandInstanceInfo): Promise { - const integration = await this.getAppIntegration(); - if (integration === 'cordova') { - throw new FatalException( - `Live Updates (cordova-plugin-ionic) configuration cannot be overridden for a Cordova project.\n` + - `Please uninstall the plugin and run ${input('ionic live-update add')} again.` - ); - } - if (integration === 'capacitor') { - // check if there is an input that matches only one - const updateIos = !inputs[0] || (inputs[0] === 'ios'); - const updateAndroid = !inputs[0] || (inputs[0] === 'android'); - - // update the ios project if present - let printOkMessage = false; - if (updateIos) { - printOkMessage = await this.addConfToIosPlist(options); - } - // update the android project if present - if (updateAndroid) { - printOkMessage = await this.addConfToAndroidString(options); - } - if (printOkMessage) { - this.env.log.ok(`Live Updates (cordova-plugin-ionic) configs successfully overridden for the project\n`); - } - } - } -} diff --git a/packages/@ionic/cli/src/commands/live-update/core.ts b/packages/@ionic/cli/src/commands/live-update/core.ts deleted file mode 100644 index cd8ce14faa..0000000000 --- a/packages/@ionic/cli/src/commands/live-update/core.ts +++ /dev/null @@ -1,469 +0,0 @@ -import { CommandLineOptions, combine, contains, validators } from '@ionic/cli-framework'; -import { pathExists, pathWritable, readFile, writeFile } from '@ionic/utils-fs'; -import * as et from 'elementtree'; -import * as path from 'path'; - -import { input, strong } from '../../lib/color'; -import { Command } from '../../lib/command'; -import { FatalException } from '../../lib/errors'; - -export abstract class LiveUpdatesCoreCommand extends Command { - protected async getAppIntegration(): Promise { - if (this.project) { - if (this.project.getIntegration('capacitor') !== undefined) { - return 'capacitor'; - } - if (this.project.getIntegration('cordova') !== undefined) { - return 'cordova'; - } - } - return undefined; - } - - protected async requireNativeIntegration(): Promise { - const integration = await this.getAppIntegration(); - - if (!integration) { - throw new FatalException( - `It looks like your app isn't integrated with Capacitor or Cordova.\n` + - `In order to use the Ionic Live Updates plugin, you will need to integrate your app with Capacitor or Cordova. See the docs for setting up native projects:\n\n` + - `iOS: ${strong('https://ionicframework.com/docs/building/ios')}\n` + - `Android: ${strong('https://ionicframework.com/docs/building/android')}\n` - ); - } - } -} - -export abstract class LiveUpdatesConfCommand extends LiveUpdatesCoreCommand { - - protected readonly optionsToPlistKeys: {[key: string]: string} = { - 'app-id': 'IonAppId', - 'channel-name': 'IonChannelName', - 'update-method': 'IonUpdateMethod', - 'max-store': 'IonMaxVersions', - 'min-background-duration': 'IonMinBackgroundDuration', - 'update-api': 'IonApi', - }; - protected readonly optionsToStringXmlKeys = { - 'app-id': 'ionic_app_id', - 'channel-name': 'ionic_channel_name', - 'update-method': 'ionic_update_method', - 'max-store': 'ionic_max_versions', - 'min-background-duration': 'ionic_min_background_duration', - 'update-api': 'ionic_update_api', - }; - - protected readonly requiredOptionsDefaults: {[key: string]: string} = { - 'max-store': '2', - 'min-background-duration': '30', - 'update-api': 'https://api.ionicjs.com', - } - - protected readonly requiredOptionsFromPlistVal: {[key: string]: string} = { - 'IonMaxVersions': 'max-store', - 'IonMinBackgroundDuration': 'min-background-duration', - 'IonApi': 'update-api', - } - - protected readonly requiredOptionsFromXmlVal = { - 'ionic_max_versions': 'max-store', - 'ionic_min_background_duration': 'min-background-duration', - 'ionic_update_api': 'update-api', - } - - protected async getAppId(): Promise { - if (this.project) { - return this.project.config.get('id'); - } - return undefined; - } - - protected async checkLiveUpdatesInstalled(): Promise { - if (!this.project) { - return false; - } - const packageJson = await this.project.requirePackageJson(); - return packageJson.dependencies ? 'cordova-plugin-ionic' in packageJson.dependencies : false; - } - - protected printPlistInstructions(options: CommandLineOptions): void { - let outputString = `You will need to manually modify the Info.plist for your iOS project.\n Please add the following content to your Info.plist file:\n`; - for (const [optionKey, pKey] of Object.entries(this.optionsToPlistKeys)) { - outputString = `${outputString}${pKey}\n${options[optionKey]}\n`; - } - this.env.log.warn(outputString); - } - - protected printStringXmlInstructions(options: CommandLineOptions): void { - let outputString = `You will need to manually modify the string.xml for your Android project.\n Please add the following content to your string.xml file:\n`; - for (const [optionKey, pKey] of Object.entries(this.optionsToPlistKeys)) { - outputString = `${outputString}${options[optionKey]}\n`; - } - this.env.log.warn(outputString); - } - - protected async getIosCapPlist(): Promise { - if (!this.project) { - return ''; - } - const capIntegration = this.project.getIntegration('capacitor'); - if (!capIntegration) { - return ''; - } - // check first if iOS exists - if (!await pathExists(path.join(capIntegration.root, 'ios'))) { - return ''; - } - const assumedPlistPath = path.join(capIntegration.root, 'ios', 'App', 'App', 'Info.plist'); - if (!await pathWritable(assumedPlistPath)) { - throw new Error('The iOS Info.plist could not be found.'); - } - return assumedPlistPath; - } - - protected async getAndroidCapString(): Promise { - if (!this.project) { - return ''; - } - const capIntegration = this.project.getIntegration('capacitor'); - if (!capIntegration) { - return ''; - } - // check first if iOS exists - if (!await pathExists(path.join(capIntegration.root, 'android'))) { - return ''; - } - const assumedStringXmlPath = path.join(capIntegration.root, 'android', 'app', 'src', 'main', 'res', 'values', 'strings.xml'); - if (!await pathWritable(assumedStringXmlPath)) { - throw new Error('The Android string.xml could not be found.'); - } - return assumedStringXmlPath; - } - - protected async addConfToIosPlist(options: CommandLineOptions): Promise { - let plistPath; - try { - plistPath = await this.getIosCapPlist(); - } catch (e: any) { - this.env.log.warn(e.message); - this.printPlistInstructions(options); - return false; - } - if (!plistPath) { - this.env.log.warn( - `No ${strong('Capacitor iOS')} project found\n` + - `You will need to rerun ${input('ionic live-update configure')} if you add it later.\n` - ); - return false; - } - // try to load the plist file first - let plistData; - try { - const plistFile = await readFile(plistPath); - plistData = plistFile.toString(); - } catch (e: any) { - this.env.log.error(`The iOS Info.plist could not be read.`); - this.printPlistInstructions(options); - return false; - } - // parse it with elementtree - let etree; - try { - etree = et.parse(plistData); - } catch (e: any) { - this.env.log.error(`Impossible to parse the XML in the Info.plist`); - this.printPlistInstructions(options); - return false; - } - // check that it is an actual plist file (root tag plist and first child dict) - const root = etree.getroot(); - if (root.tag !== 'plist') { - this.env.log.error(`Info.plist is not a valid plist file because the root is not a tag`); - this.printPlistInstructions(options); - return false; - } - const pdict = root.find('./dict'); - if (!pdict) { - this.env.log.error(`Info.plist is not a valid plist file because the first child is not a tag`); - this.printPlistInstructions(options); - return false; - } - // check which options are set (configure might not have all of them set) - const setOptions: { [key: string]: string } = {}; - - for (const [optionKey, plistKey] of Object.entries(this.optionsToPlistKeys)) { - if (options[optionKey]) { - setOptions[optionKey] = plistKey; - } - } - if (Object.entries(setOptions).length === 0) { - this.env.log.warn(`No new options detected for Info.plist`); - return false; - } - - // because elementtree has limited XPath support we cannot just run a smart selection, so we need to loop over all the elements - const pdictChildren = pdict.getchildren(); - // there is no way to refer to a first right sibling in elementtree, so we use flags - let removeNextStringTag = false; - let existingRequiredKeys = []; - for (const element of pdictChildren) { - - // find required options and keep track of what is already existing - if ((element.tag === 'key') && (element.text) && this.requiredOptionsFromPlistVal[element.text as string] != undefined) { - existingRequiredKeys.push(this.requiredOptionsFromPlistVal[element.text as string]) - } - - // we remove all the existing element if there - if ((element.tag === 'key') && (element.text) && Object.values(setOptions).includes(element.text as string)) { - pdict.remove(element); - removeNextStringTag = true; - continue; - } - - // and remove the first right sibling (this will happen at the next iteration of the loop - if ((element.tag === 'string') && removeNextStringTag) { - pdict.remove(element); - removeNextStringTag = false; - } - } - - // set any missing required keys to default - for (const key of Object.keys(this.requiredOptionsDefaults)) { - if (existingRequiredKeys.includes(key)) { - continue; - } - setOptions[key] = this.optionsToPlistKeys[key]; - if (!options[key]) { - options[key] = this.requiredOptionsDefaults[key]; - } - } - - // add again the new settings - for (const [optionKey, plistKey] of Object.entries(setOptions)) { - const plistValue = options[optionKey]; - if (!plistValue) { - throw new FatalException(`This should never have happened: a parameter is missing so we cannot write the Info.plist`); - } - const pkey = et.SubElement(pdict, 'key'); - pkey.text = plistKey; - const pstring = et.SubElement(pdict, 'string'); - pstring.text = plistValue; - } - // finally write back the modified plist - const newXML = etree.write({ - encoding: 'utf-8', - indent: 2, - xml_declaration: false, - }); - // elementtree cannot write a doctype, so little hack - const xmlToWrite = `\n` + - `\n` + - newXML; - try { - await writeFile(plistPath, xmlToWrite, { encoding: 'utf-8' }); - } catch (e: any) { - this.env.log.error(`Changes to Info.plist could not be written.`); - this.printPlistInstructions(options); - } - this.env.log.ok(`cordova-plugin-ionic variables correctly added to the iOS project`); - return true; - } - - protected async addConfToAndroidString(options: CommandLineOptions): Promise { - let stringXmlPath; - try { - stringXmlPath = await this.getAndroidCapString(); - } catch (e: any) { - this.env.log.warn(e.message); - this.printPlistInstructions(options); - return false; - } - if (!stringXmlPath) { - this.env.log.warn( - `No ${strong('Capacitor Android')} project found\n`+ - `You will need to rerun ${input('ionic live-update configure')} if you add it later.\n`); - return false; - } - // try to load the plist file first - let stringData; - try { - const stringFile = await readFile(stringXmlPath); - stringData = stringFile.toString(); - } catch (e: any) { - this.env.log.error(`The Android string.xml could not be read.`); - this.printStringXmlInstructions(options); - return false; - } - // parse it with elementtree - let etree; - try { - etree = et.parse(stringData); - } catch (e: any) { - this.env.log.error(`Impossible to parse the XML in the string.xml`); - this.printStringXmlInstructions(options); - return false; - } - // check that it is an actual string.xml file (root tag is resources) - const root = etree.getroot(); - if (root.tag !== 'resources') { - this.env.log.error(`string.xml is not a valid android string.xml file because the root is not a tag`); - this.printStringXmlInstructions(options); - return false; - } - // check which options are set (configure might not have all of them set) - const setOptions: { [key: string]: string } = {}; - for (const [optionKey, plistKey] of Object.entries(this.optionsToStringXmlKeys)) { - if (options[optionKey]) { - setOptions[optionKey] = plistKey; - } - } - if (Object.entries(setOptions).length === 0) { - this.env.log.warn(`No new options detected for string.xml`); - return false; - } - for (const [optionKey, stringKey] of Object.entries(setOptions)) { - let element = root.find(`./string[@name="${stringKey}"]`); - // if the tag already exists, just update the content - if (element) { - element.text = options[optionKey] as string; - } else { - // otherwise create the tag - element = et.SubElement(root, 'string'); - element.set('name', stringKey); - element.text = options[optionKey] as string; - } - } - - // make sure required keys are set - for (const [stringKey, optionKey] of Object.entries(this.requiredOptionsFromXmlVal)) { - let element = root.find(`./string[@name="${stringKey}"]`); - // if the tag already exists, just update the content - if (element) { - continue; - } else { - // otherwise create the tag and set to default - element = et.SubElement(root, 'string'); - element.set('name', stringKey); - console.log(optionKey, 'opoitn key'); - element.text = this.requiredOptionsDefaults[optionKey]; - } - } - - // write back the modified plist - const newXML = etree.write({ - encoding: 'utf-8', - indent: 2, - }); - try { - await writeFile(stringXmlPath, newXML, { encoding: 'utf-8' }); - } catch (e: any) { - this.env.log.error(`Changes to string.xml could not be written.`); - this.printStringXmlInstructions(options); - } - this.env.log.ok(`cordova-plugin-ionic variables correctly added to the Android project`); - return true; - } - - protected async preRunCheckInputs(options: CommandLineOptions): Promise { - const updateMethodList: string[] = ['auto', 'background', 'none']; - const defaultUpdateMethod = 'background'; - // handle the app-id option in case the user wants to override it - if (!options['app-id'] && this.env.flags.interactive) { - const appId = await this.getAppId(); - if (!appId) { - this.env.log.warn( - `No app ID found in the project.\n` + - `Consider running ${input('ionic link')} to connect local apps to Ionic.\n` - ); - } - const appIdOption = await this.env.prompt({ - type: 'input', - name: 'app-id', - message: `Appflow App ID:`, - default: appId, - }); - options['app-id'] = appIdOption; - } - - if (!options['channel-name'] && this.env.flags.interactive) { - options['channel-name'] = await this.env.prompt({ - type: 'input', - name: 'channel-name', - message: `Channel Name:`, - validate: v => validators.required(v), - }); - } - - // validate that the update-method is allowed - let overrideUpdateMethodChoice = false; - if (options['update-method'] && !updateMethodList.includes(options['update-method'] as string)) { - if (this.env.flags.interactive) { - this.env.log.nl(); - this.env.log.warn( - `${input(options['update-method'] as string)} is not a valid update method.\n` + - `Please choose a different value for ${input('--update-method')}. Valid update methods are: ${updateMethodList.map(m => input(m)).join(', ')}\n` - ); - } - overrideUpdateMethodChoice = true; - } - if ((!options['update-method'] || overrideUpdateMethodChoice) && this.env.flags.interactive) { - options['update-method'] = await this.env.prompt({ - type: 'list', - name: 'update-method', - choices: updateMethodList, - message: `Update Method:`, - default: defaultUpdateMethod, - validate: v => combine(validators.required, contains(updateMethodList, {}))(v), - }); - } - - // check advanced options if present - if (options['max-store'] && validators.numeric(options['max-store'] as string) !== true) { - if (this.env.flags.interactive) { - this.env.log.nl(); - this.env.log.warn( - `${input(options['max-store'] as string)} is not a valid value for the maximum number of versions to store.\n` + - `Please specify an integer for ${input('--max-store')}.\n` - ); - } - options['max-store'] = await this.env.prompt({ - type: 'input', - name: 'max-store', - message: `Max Store:`, - validate: v => combine(validators.required, validators.numeric)(v), - }); - } - - if (options['min-background-duration'] && validators.numeric(options['min-background-duration'] as string) !== true) { - if (this.env.flags.interactive) { - this.env.log.nl(); - this.env.log.warn( - `${input(options['min-background-duration'] as string)} is not a valid value for the number of seconds to wait before checking for updates in the background.\n` + - `Please specify an integer for ${input('--min-background-duration')}.\n` - ); - } - options['min-background-duration'] = await this.env.prompt({ - type: 'input', - name: 'min-background-duration', - message: `Min Background Duration:`, - validate: v => combine(validators.required, validators.numeric)(v), - }); - } - if (options['update-api'] && validators.url(options['update-api'] as string) !== true) { - if (this.env.flags.interactive) { - this.env.log.nl(); - this.env.log.warn( - `${input(options['update-api'] as string)} is not a valid value for the URL of the API to use.\n` + - `Please specify a valid URL for ${input('--update-api')}.\n` - ); - } - options['update-api'] = await this.env.prompt({ - type: 'input', - name: 'update-api', - message: `Update Url:`, - validate: v => combine(validators.required, validators.url)(v), - }); - } - } - -} diff --git a/packages/@ionic/cli/src/commands/live-update/index.ts b/packages/@ionic/cli/src/commands/live-update/index.ts deleted file mode 100644 index e5cef4677b..0000000000 --- a/packages/@ionic/cli/src/commands/live-update/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { MetadataGroup } from '@ionic/cli-framework'; - -import { strong } from '../../lib/color'; -import { CommandMap, Namespace } from '../../lib/namespace'; - -export class LiveUpdatesNamespace extends Namespace { - async getMetadata() { - return { - name: 'live-update', - summary: 'Ionic Live Updates functionality', - description: ` -These commands integrate with Ionic Cloud to configure the Live Updates plugin in your project and run remote builds. - -Ionic Live Updates documentation: -- Overview: ${strong('https://ion.link/appflow-deploy-docs')} -`, - groups: [MetadataGroup.PAID], - }; - } - - async getCommands(): Promise { - return new CommandMap([ - ['add', async () => { const { AddCommand } = await import('./add'); return new AddCommand(this); }], - ['configure', async () => { const { ConfigureCommand } = await import('./configure'); return new ConfigureCommand(this); }], - ['manifest', async () => { const { LiveUpdatesManifestCommand } = await import('./manifest'); return new LiveUpdatesManifestCommand(this); }], - ]); - } -} diff --git a/packages/@ionic/cli/src/commands/live-update/manifest.ts b/packages/@ionic/cli/src/commands/live-update/manifest.ts deleted file mode 100644 index 18cf415957..0000000000 --- a/packages/@ionic/cli/src/commands/live-update/manifest.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { MetadataGroup } from '@ionic/cli-framework'; -import { LOGGER_LEVELS } from '@ionic/cli-framework-output'; -import { map } from '@ionic/utils-array'; -import { pathExists, readdirp, stat, writeFile } from '@ionic/utils-fs'; -import { prettyPath } from '@ionic/utils-terminal'; -import * as crypto from 'crypto'; -import * as fs from 'fs'; -import lodash = require('lodash'); -import * as path from 'path'; -import { debug as Debug } from 'debug'; - -import { CommandMetadata } from '../../definitions'; -import { input } from '../../lib/color'; -import { FatalException } from '../../lib/errors'; -import { prependNodeModulesBinToPath, Shell } from '../../lib/shell'; -import { createDefaultLoggerHandlers, Logger } from '../../lib/utils/logger'; - -import { CapacitorCLIConfig, CapacitorConfig, CapacitorJSONConfig } from './capacitor'; -import { LiveUpdatesCoreCommand } from './core'; - -const debug = Debug('ionic:commands:live-update:manifest'); -const CAPACITOR_CONFIG_JSON_FILE = 'capacitor.config.json'; - -interface LiveUpdatesManifestItem { - href: string; - size: number; - integrity: string; -} - -export class LiveUpdatesManifestCommand extends LiveUpdatesCoreCommand { - async getMetadata(): Promise { - // This command is set as type 'global' in order to support Capacitor apps without an ionic.config.json - return { - name: 'manifest', - type: 'global', - summary: 'Generates a manifest file for the Ionic Live Updates service from a built app directory', - groups: [MetadataGroup.PAID], - }; - } - - async run(): Promise { - const capacitorConfig = await this.getCapacitorConfig(); - if (!this.project && !capacitorConfig) { - throw new FatalException(`Cannot run ${input('ionic live-update manifest')} outside a project directory.`); - } - - let buildDir: string; - if (this.project) { - await this.requireNativeIntegration(); - buildDir = await this.project.getDistDir(); - } else { - buildDir = capacitorConfig!.webDir ? capacitorConfig!.webDir : 'www'; - } - - const manifest = await this.getFilesAndSizesAndHashesForGlobPattern(buildDir); - - const manifestPath = path.resolve(buildDir, 'pro-manifest.json'); - await writeFile(manifestPath, JSON.stringify(manifest, undefined, 2), { encoding: 'utf8' }); - this.env.log.ok(`Ionic Live Updates manifest written to ${input(prettyPath(manifestPath))}!`); - } - - private async getFilesAndSizesAndHashesForGlobPattern(buildDir: string): Promise { - const contents = await readdirp(buildDir, { filter: item => !/(css|js)\.map$/.test(item.path) }); - const stats = await map(contents, async (f): Promise<[string, fs.Stats]> => [f, await stat(f)]); - const files = stats.filter(([, s]) => !s.isDirectory()); - - const items = await Promise.all(files.map(([f, s]) => this.getFileAndSizeAndHashForFile(buildDir, f, s))); - - return items.filter(item => item.href !== 'pro-manifest.json'); - } - - private async getFileAndSizeAndHashForFile(buildDir: string, file: string, s: fs.Stats): Promise { - const buffer = await this.readFile(file); - - return { - href: path.relative(buildDir, file), - size: s.size, - integrity: this.getIntegrity(buffer), - }; - } - - private async readFile(file: string): Promise { - return new Promise((resolve, reject) => { - fs.readFile(file, (err, buffer) => { - if (err) { - return reject(err); - } - - resolve(buffer); - }); - }); - } - - private getIntegrity(data: Buffer) { - return ['sha256', 'sha384', 'sha512'] - .map(algorithm => { - const hash = crypto.createHash(algorithm); - hash.update(data); - return algorithm + '-' + hash.digest('base64'); - }) - .join(' '); - } - - private getCapacitorConfigJsonPath(): string { - return path.resolve(this.env.ctx.execPath, CAPACITOR_CONFIG_JSON_FILE); - } - - private getCapacitorCLIConfig = lodash.memoize(async (): Promise => { - // I had to create a new shell to force prependNodeModulesBinToPath. - // If ionic.config.json is not present, then this.env.shell will not implement this, and the Capacitor command will fail. - const args = ['config', '--json']; - const log = new Logger({ - level: LOGGER_LEVELS.INFO, - handlers: createDefaultLoggerHandlers(), - }); - const shell = new Shell({ log }, { alterPath: p => { return prependNodeModulesBinToPath(this.env.ctx.execPath, p) } }); - - debug('Getting config with Capacitor CLI: %O', args); - - const output = await shell.cmdinfo('capacitor', args); - - if (!output) { - debug('Could not get config from Capacitor CLI (probably old version)'); - return; - } - - try { - return JSON.parse(output); - } catch (e) { - debug('Could not get config from Capacitor CLI (probably old version)', e); - return; - } - }); - - private getCapacitorConfig = lodash.memoize(async (): Promise => { - const cli = await this.getCapacitorCLIConfig(); - - if (cli) { - debug('Loaded Capacitor config!'); - return cli.app.extConfig; - } - - // fallback to reading capacitor.config.json if it exists - const confPath = this.getCapacitorConfigJsonPath(); - - if (!(await pathExists(confPath))) { - debug('Capacitor config file does not exist at %O', confPath); - debug('Failed to load Capacitor config'); - return; - } - - const conf = new CapacitorJSONConfig(confPath); - const extConfig = conf.c; - - debug('Loaded Capacitor config!'); - - return extConfig; - }); -} diff --git a/packages/@ionic/cli/src/commands/login.ts b/packages/@ionic/cli/src/commands/login.ts deleted file mode 100644 index 46f709c107..0000000000 --- a/packages/@ionic/cli/src/commands/login.ts +++ /dev/null @@ -1,199 +0,0 @@ -import { combine, validators } from '@ionic/cli-framework'; -import chalk from 'chalk'; -import * as readline from 'readline'; - -import { CommandLineInputs, CommandLineOptions, CommandMetadata, CommandPreRun } from '../definitions'; -import { input, strong, success } from '../lib/color'; -import { Command } from '../lib/command'; -import { FatalException } from '../lib/errors'; -import { generateUUID } from '../lib/utils/uuid'; - -export class LoginCommand extends Command implements CommandPreRun { - async getMetadata(): Promise { - return { - name: 'login', - type: 'global', - summary: 'Log in to Ionic', - description: ` -Authenticate with Ionic and retrieve a user token, which is stored in the CLI config. Running ${input('ionic login')} will open a browser where you can submit your credentials. - -If the ${input('IONIC_TOKEN')} environment variable is set, the CLI will automatically authenticate you. Use the Dashboard to generate a Personal Access Token. - -If you need to create an Ionic account, use ${input('ionic signup')} or the Ionic Website[^signup]. - -If you are having issues logging in, please get in touch with our Support[^support-request]. - `, - footnotes: [ - { - id: 'signup', - url: 'https://ionicframework.com/signup', - }, - { - id: 'support-request', - url: 'https://ion.link/support-request', - }, - ], - exampleCommands: [''], - inputs: [ - { - name: 'email', - summary: 'Your email address (deprecated)', - private: true, - }, - { - name: 'password', - summary: 'Your password (deprecated)', - private: true, - }, - ], - }; - } - - async preRun(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - if (options['email'] || options['password']) { - throw new FatalException( - `${input('email')} and ${input('password')} are command arguments, not options. Please try this:\n` + - `${input('ionic login ')}\n` - ); - } - - if (options['sso']) { - this.env.log.warn( - `The ${strong('--sso')} flag is no longer necessary.\n` + - `SSO login has been upgraded to OpenID login, which is now the new default authentication flow of ${input('ionic login')}. Refresh tokens are used to automatically re-authenticate sessions.` - ); - this.env.log.nl(); - } - - if (!!inputs[0]) { - this.env.log.warn( - 'Authenticating using email and password is deprecated. ' + - (this.env.flags.interactive - ? `Please run ${input('ionic login')} without arguments to log in.` - : `Please generate a Personal Access Token and set the ${input('IONIC_TOKEN')} environment variable.`) - ); - this.env.log.nl(); - } - - // ask for password only if the user specifies an email - const validateEmail = !!inputs[0]; - const askForPassword = inputs[0] && !inputs[1]; - - if (this.env.session.isLoggedIn()) { - const email = this.env.config.get('user.email'); - - const extra = askForPassword - ? (this.env.flags.interactive ? `Prompting for new credentials.\n\nUse ${chalk.yellow('Ctrl+C')} to cancel and remain logged in.` : '') - : 'You will be logged out beforehand.'; - - if (this.env.flags.interactive) { - this.env.log.warn( - 'You will be logged out.\n' + - `You are already logged in${email ? ' as ' + strong(email) : ''}! ${extra}` - ); - - this.env.log.nl(); - } - } else { - if (this.env.flags.interactive) { - this.env.log.info( - `Log into your Ionic account!\n` + - `If you don't have one yet, create yours by running: ${input(`ionic signup`)}` - ); - - this.env.log.nl(); - } - } - - // TODO: combine with promptToLogin ? - if (validateEmail) { - const validatedEmail = validators.email(inputs[0]); - if (validatedEmail !== true) { - this.env.log.warn(`${validatedEmail}. \n Please enter a valid email address.`); - if (this.env.flags.interactive) { - const email = await this.env.prompt({ - type: 'input', - name: 'email', - message: 'Email:', - validate: v => combine(validators.required, validators.email)(v), - }); - inputs[0] = email; - } else { - throw new FatalException('Invalid email'); - } - } - } - if (askForPassword) { - if (this.env.flags.interactive) { - const password = await this.env.prompt({ - type: 'password', - name: 'password', - message: 'Password:', - mask: '*', - validate: v => validators.required(v), - }); - - inputs[1] = password; - } else { - inputs[1] = await this.getPasswordFromStdin(); - } - } - } - - getPasswordFromStdin(): Promise { - return new Promise(resolve => { - const rl = readline.createInterface({ - input: process.stdin, - terminal: false, - }); - - rl.on('line', line => { - resolve(line); - rl.close(); - }); - }); - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - const [email, password] = inputs; - - if (email && password) { - await this.logout(); - await this.env.session.login(email, password); - } else { - if (!this.env.flags.interactive && !this.env.flags.confirm) { - throw new FatalException( - 'Refusing to attempt browser login in non-interactive mode.\n' + - `If you are attempting to log in, make sure you are using a modern, interactive terminal. Otherwise, you can log in using inline username and password with ${input('ionic login ')}. See ${input('ionic login --help')} for more details.` - ); - } - - this.env.log.info(`During this process, a browser window will open to authenticate you. Please leave this process running until authentication is complete.`); - this.env.log.nl(); - - const login = await this.env.prompt({ - type: 'confirm', - name: 'continue', - message: 'Open the browser to log in to your Ionic account?', - default: true, - }); - - if (login) { - await this.logout(); - await this.env.session.webLogin(); - } else { - return; - } - - } - - this.env.log.ok(success(strong('You are logged in!'))); - } - - async logout() { - if (this.env.session.isLoggedIn()) { - await this.env.session.logout(); - this.env.config.set('tokens.telemetry', generateUUID()); - } - } -} diff --git a/packages/@ionic/cli/src/commands/logout.ts b/packages/@ionic/cli/src/commands/logout.ts deleted file mode 100644 index 6622e2eee1..0000000000 --- a/packages/@ionic/cli/src/commands/logout.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { CommandLineInputs, CommandLineOptions, CommandMetadata } from '../definitions'; -import { input } from '../lib/color'; -import { Command } from '../lib/command'; - -export class LogoutCommand extends Command { - async getMetadata(): Promise { - return { - name: 'logout', - type: 'global', - summary: 'Log out of Ionic', - description: ` -Remove the Ionic user token from the CLI config. - -Log in again with ${input('ionic login')}. - -If you need to create an Ionic account, use ${input('ionic signup')}. - `, - }; - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - if (!this.env.session.isLoggedIn()) { - this.env.log.msg('You are already logged out.'); - return; - } - - await this.env.session.logout(); - this.env.log.ok('You are logged out.'); - } -} diff --git a/packages/@ionic/cli/src/commands/monitoring/index.ts b/packages/@ionic/cli/src/commands/monitoring/index.ts deleted file mode 100644 index 3029833c92..0000000000 --- a/packages/@ionic/cli/src/commands/monitoring/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { MetadataGroup } from '@ionic/cli-framework'; - -import { CommandMap, Namespace } from '../../lib/namespace'; - -export class MonitoringNamespace extends Namespace { - async getMetadata() { - const groups: MetadataGroup[] = []; - - groups.push(MetadataGroup.HIDDEN); - - return { - name: 'monitoring', - summary: 'Commands relating to Ionic Appflow Error Monitoring', - groups, - }; - } - - async getCommands(): Promise { - return new CommandMap([ - ['syncmaps', async () => { const { MonitoringSyncSourcemapsCommand } = await import('./syncmaps'); return new MonitoringSyncSourcemapsCommand(this); }], - ]); - } -} diff --git a/packages/@ionic/cli/src/commands/monitoring/syncmaps.ts b/packages/@ionic/cli/src/commands/monitoring/syncmaps.ts deleted file mode 100644 index e27afa5a19..0000000000 --- a/packages/@ionic/cli/src/commands/monitoring/syncmaps.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { pathExists, readFile, readdirSafe } from '@ionic/utils-fs'; -import { columnar, prettyPath } from '@ionic/utils-terminal'; -import { debug as Debug } from 'debug'; -import * as path from 'path'; - -import { APIResponseSuccess, CommandLineInputs, CommandLineOptions, CommandMetadata } from '../../definitions'; -import { isSuperAgentError } from '../../guards'; -import { input, strong, weak } from '../../lib/color'; -import { Command } from '../../lib/command'; -import { FatalException } from '../../lib/errors'; - -const debug = Debug('ionic:commands:monitoring:syncmaps'); - -const SOURCEMAP_DIRECTORY = '.sourcemaps'; - -export class MonitoringSyncSourcemapsCommand extends Command { - async getMetadata(): Promise { - return { - name: 'syncmaps', - type: 'project', - summary: 'Build & upload sourcemaps to Ionic Appflow Monitoring service', - description: ` -By default, ${input('ionic monitoring syncmaps')} will upload the sourcemap files within ${strong(SOURCEMAP_DIRECTORY)}. To optionally perform a production build before uploading sourcemaps, specify the ${input('--build')} flag. - `, - inputs: [ - { - name: 'snapshot_id', - summary: `Specify a Snapshot ID to associate the uploaded sourcemaps with`, - }, - ], - options: [ - { - name: 'build', - summary: 'Invoke a production Ionic build', - type: Boolean, - }, - ], - }; - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - const { loadCordovaConfig } = await import('../../lib/integrations/cordova/config'); - - if (!this.project) { - throw new FatalException(`Cannot run ${input('ionic monitoring syncmaps')} outside a project directory.`); - } - - const token = await this.env.session.getUserToken(); - const appflowId = await this.project.requireAppflowId(); - - const [snapshotId] = inputs; - const doBuild = options.build ? true : false; - - const cordova = this.project.requireIntegration('cordova'); - const conf = await loadCordovaConfig(cordova); - const cordovaInfo = conf.getProjectInfo(); - - const appVersion = cordovaInfo.version; - const commitHash = (await this.env.shell.output('git', ['rev-parse', 'HEAD'], { cwd: this.project.directory })).trim(); - debug(`Commit hash: ${strong(commitHash)}`); - - const sourcemapsDir = path.resolve(this.project.directory, SOURCEMAP_DIRECTORY); - let sourcemapsExist = await pathExists(sourcemapsDir); - - if (doBuild || !sourcemapsExist) { - const runner = await this.project.requireBuildRunner(); - const runnerOpts = runner.createOptionsFromCommandLine([], { _: [], prod: true }); - await runner.run(runnerOpts); - } - - sourcemapsExist = await pathExists(sourcemapsDir); - - if (sourcemapsExist) { - this.env.log.msg(`Using existing sourcemaps in ${strong(prettyPath(sourcemapsDir))}`); - } else { // TODO: this is hard-coded for ionic-angular, make it work for all project types - throw new FatalException( - `Cannot find directory: ${strong(prettyPath(sourcemapsDir))}.\n` + - `Make sure you have the latest ${strong('@ionic/app-scripts')}. Then, re-run this command.` - ); - } - - let count = 0; - const tasks = this.createTaskChain(); - const syncTask = tasks.next('Syncing sourcemaps'); - - const sourcemapFiles = (await readdirSafe(sourcemapsDir)).filter(f => f.endsWith('.js.map')); - debug(`Found ${sourcemapFiles.length} sourcemap files: ${sourcemapFiles.map(f => strong(f)).join(', ')}`); - - await Promise.all(sourcemapFiles.map(async f => { - await this.syncSourcemap(path.resolve(sourcemapsDir, f), snapshotId, appVersion, commitHash, appflowId, token); - count += 1; - syncTask.msg = `Syncing sourcemaps: ${strong(`${count} / ${sourcemapFiles.length}`)}`; - })); - - syncTask.msg = `Syncing sourcemaps: ${strong(`${sourcemapFiles.length} / ${sourcemapFiles.length}`)}`; - tasks.end(); - - const details = columnar([ - ['App ID', strong(appflowId)], - ['Version', strong(appVersion)], - ['Package ID', strong(cordovaInfo.id)], - ['Snapshot ID', snapshotId ? strong(snapshotId) : weak('not set')], - ], { vsep: ':' }); - - this.env.log.ok( - `Sourcemaps synced!\n` + - details + '\n\n' + - `See the Error Monitoring docs for usage information and next steps: ${strong('https://ionicframework.com/docs/appflow/monitoring')}` - ); - } - - async syncSourcemap(file: string, snapshotId: string, appVersion: string, commitHash: string, appflowId: string, token: string): Promise { - const { req } = await this.env.client.make('POST', `/monitoring/${appflowId}/sourcemaps`); - - req - .set('Authorization', `Bearer ${token}`) - .send({ - name: path.basename(file), - version: appVersion, - commit: commitHash, - snapshot_id: snapshotId, - }); - - try { - const res = await this.env.client.do(req); - - return this.uploadSourcemap(res, file); - } catch (e: any) { - if (isSuperAgentError(e)) { - this.env.log.error(`Unable to sync map ${file}: ` + e.message); - if (e.response.status === 401) { - this.env.log.error('Try logging out and back in again.'); - } - } else { - throw e; - } - } - } - - async uploadSourcemap(sourcemap: APIResponseSuccess, file: string) { - const { createRequest } = await import('../../lib/utils/http'); - - const sm = sourcemap as any; - - const fileData = await readFile(file, { encoding: 'utf8' }); - const sourcemapPost = sm.data.sourcemap_post; - - const { req } = await createRequest('POST', sourcemapPost.url, this.env.config.getHTTPConfig()); - - req - .field(sourcemapPost.fields) - .field('file', fileData); - - const res = await req; - - if (res.status !== 204) { - throw new FatalException(`Unexpected status code from AWS: ${res.status}`); - } - } -} diff --git a/packages/@ionic/cli/src/commands/repair.ts b/packages/@ionic/cli/src/commands/repair.ts deleted file mode 100644 index 7090f77b0e..0000000000 --- a/packages/@ionic/cli/src/commands/repair.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { pathExists, remove, unlink } from '@ionic/utils-fs'; -import { prettyPath } from '@ionic/utils-terminal'; -import * as path from 'path'; - -import { CommandInstanceInfo, CommandLineInputs, CommandLineOptions, CommandMetadata, IProject, ProjectIntegration } from '../definitions'; -import { input, strong } from '../lib/color'; -import { Command } from '../lib/command'; -import { FatalException } from '../lib/errors'; -import { runCommand } from '../lib/executor'; - -export class RepairCommand extends Command { - async getMetadata(): Promise { - return { - name: 'repair', - type: 'project', - summary: 'Remove and recreate dependencies and generated files', - description: ` -This command may be useful when obscure errors or issues are encountered. It removes and recreates dependencies of your project. - -For Cordova apps, it removes and recreates the generated native project and the native dependencies of your project. -`, - options: [ - { - name: 'cordova', - summary: 'Only perform the repair steps for Cordova platforms and plugins.', - type: Boolean, - }, - ], - }; - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions, runinfo: CommandInstanceInfo): Promise { - if (!this.project) { - throw new FatalException(`Cannot run ${input('ionic repair')} outside a project directory.`); - } - - const { pkgManagerArgs } = await import('../lib/utils/npm'); - const [ installer, ...installerArgs ] = await pkgManagerArgs(this.env.config.get('npmClient'), { command: 'install' }); - - const cordovaOnly = !!options['cordova']; - const cordova = this.project.getIntegration('cordova'); - - if (cordovaOnly && !cordova) { - throw new FatalException(`${input('--cordova')} was specified, but Cordova has not been added to this project.`); - } - - if (cordova && !cordova.enabled) { - this.env.log.warn(`Cordova integration found, but is disabled--not running repair for Cordova.`); - } - - if (this.env.flags.interactive) { - const steps: string[] = []; - - if (!cordovaOnly) { - steps.push( - `- Remove ${strong('node_modules/')} and ${strong('package-lock.json')}\n` + - `- Run ${input([installer, ...installerArgs].join(' '))} to restore dependencies\n` - ); - } - - if (cordova && cordova.enabled) { - steps.push( - `- Remove ${strong('platforms/')} and ${strong('plugins/')}\n` + - `- Run ${input('cordova prepare')} to restore platforms and plugins\n` - ); - } - - if (steps.length === 0) { - this.env.log.ok(`${input('ionic repair')} has nothing to do.`); - throw new FatalException('', 0); - } - - this.env.log.info(`${input('ionic repair')} will do the following:\n\n` + steps.join('')); - } - - const confirm = await this.env.prompt({ - type: 'confirm', - name: 'confirm', - message: 'Continue?', - default: false, - }); - - if (!confirm) { - throw new FatalException(`Not running ${input('ionic repair')}.`); - } - - this.env.log.nl(); - - if (!cordovaOnly) { - await this.npmRepair(this.project); - } - - if (cordova && cordova.enabled) { - await this.cordovaRepair(cordova, runinfo); - } - } - - async npmRepair(project: IProject) { - const { pkgManagerArgs } = await import('../lib/utils/npm'); - const [ installer, ...installerArgs ] = await pkgManagerArgs(this.env.config.get('npmClient'), { command: 'install' }); - - const tasks = this.createTaskChain(); - const packageLockFile = path.resolve(project.directory, 'package-lock.json'); - const nodeModulesDir = path.resolve(project.directory, 'node_modules'); - - tasks.next(`Removing ${strong(prettyPath(packageLockFile))}`); - const packageLockFileExists = await pathExists(packageLockFile); - - if (packageLockFileExists) { - await unlink(packageLockFile); - } - - tasks.next(`Removing ${strong(prettyPath(nodeModulesDir))}`); - await remove(nodeModulesDir); - - tasks.end(); - - await this.env.shell.run(installer, installerArgs, { cwd: project.directory, stdio: 'inherit' }); - } - - async cordovaRepair(cordova: Required, runinfo: CommandInstanceInfo) { - const tasks = this.createTaskChain(); - - const platformsDir = path.resolve(cordova.root, 'platforms'); - const pluginsDir = path.resolve(cordova.root, 'plugins'); - - tasks.next(`Removing ${strong(prettyPath(platformsDir))}`); - await remove(platformsDir); - - tasks.next(`Removing ${strong(prettyPath(pluginsDir))}`); - await remove(pluginsDir); - - tasks.end(); - - await runCommand(runinfo, ['cordova', 'prepare', '--no-build']); - } -} diff --git a/packages/@ionic/cli/src/commands/serve.ts b/packages/@ionic/cli/src/commands/serve.ts deleted file mode 100644 index 1cb20a381c..0000000000 --- a/packages/@ionic/cli/src/commands/serve.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { Footnote, MetadataGroup } from '@ionic/cli-framework'; -import { sleepForever } from '@ionic/utils-process'; -import * as lodash from 'lodash'; - -import { CommandInstanceInfo, CommandLineInputs, CommandLineOptions, CommandMetadata, CommandMetadataOption, CommandPreRun } from '../definitions'; -import { input } from '../lib/color'; -import { Command } from '../lib/command'; -import { FatalException, RunnerException } from '../lib/errors'; -import { BROWSERS, COMMON_SERVE_COMMAND_OPTIONS } from '../lib/serve'; - -export class ServeCommand extends Command implements CommandPreRun { - async getMetadata(): Promise { - const groups: string[] = []; - - let options: CommandMetadataOption[] = [ - ...COMMON_SERVE_COMMAND_OPTIONS, - { - name: 'open', - summary: 'Do not open a browser window', - type: Boolean, - default: true, - // TODO: Adding 'b' to aliases here has some weird behavior with minimist. - }, - { - name: 'browser', - summary: `Specifies the browser to use (${BROWSERS.map(b => input(b)).join(', ')})`, - aliases: ['w'], - groups: [MetadataGroup.ADVANCED], - }, - { - name: 'browseroption', - summary: `Specifies a path to open to (${input('/#/tab/dash')})`, - aliases: ['o'], - groups: [MetadataGroup.ADVANCED], - spec: { value: 'path' }, - }, - ]; - - const exampleCommands = ['', '--external']; - const footnotes: Footnote[] = []; - - let description = ` -Easily spin up a development server which launches in your browser. It watches for changes in your source files and automatically reloads with the updated build. - -By default, ${input('ionic serve')} boots up a development server on ${input('localhost')}. To serve to your LAN, specify the ${input('--external')} option, which will use all network interfaces and print the external address(es) on which your app is being served.`; - - const runner = this.project && await this.project.getServeRunner(); - - if (runner) { - const libmetadata = await runner.getCommandMetadata(); - groups.push(...libmetadata.groups || []); - options = lodash.uniqWith([...libmetadata.options || [], ...options], (optionA, optionB) => optionA.name === optionB.name); - description += `\n\n${(libmetadata.description || '').trim()}`; - footnotes.push(...libmetadata.footnotes || []); - exampleCommands.push(...libmetadata.exampleCommands || []); - } - - return { - name: 'serve', - type: 'project', - summary: 'Start a local dev server for app dev/testing', - description, - footnotes, - groups, - exampleCommands, - options, - }; - } - - async preRun(inputs: CommandLineInputs, options: CommandLineOptions, { location }: CommandInstanceInfo): Promise { - if (options['nolivereload']) { - this.env.log.warn(`The ${input('--nolivereload')} option has been deprecated. Please use ${input('--no-livereload')}.`); - options['livereload'] = false; - } - - if (options['nobrowser']) { - this.env.log.warn(`The ${input('--nobrowser')} option has been deprecated. Please use ${input('--no-open')}.`); - options['open'] = false; - } - - if (options['b']) { - options['open'] = false; - } - - if (options['noproxy']) { - this.env.log.warn(`The ${input('--noproxy')} option has been deprecated. Please use ${input('--no-proxy')}.`); - options['proxy'] = false; - } - - if (options['x']) { - options['proxy'] = false; - } - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions, runinfo: CommandInstanceInfo): Promise { - if (!this.project) { - throw new FatalException(`Cannot run ${input('ionic serve')} outside a project directory.`); - } - - try { - const runner = await this.project.requireServeRunner(); - const runnerOpts = runner.createOptionsFromCommandLine(inputs, options); - - await runner.run(runnerOpts); - } catch (e: any) { - if (e instanceof RunnerException) { - throw new FatalException(e.message); - } - - throw e; - } - - await sleepForever(); - } -} diff --git a/packages/@ionic/cli/src/commands/share.ts b/packages/@ionic/cli/src/commands/share.ts deleted file mode 100644 index e925ad1387..0000000000 --- a/packages/@ionic/cli/src/commands/share.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { MetadataGroup } from '@ionic/cli-framework'; - -import { CommandMetadata } from '../definitions'; -import { input, strong } from '../lib/color'; -import { Command } from '../lib/command'; -import { FatalException } from '../lib/errors'; - -export class ShareCommand extends Command { - async getMetadata(): Promise { - return { - name: 'share', - type: 'global', - summary: '', - groups: [MetadataGroup.HIDDEN], - }; - } - - async run(): Promise { - const dashUrl = this.env.config.getDashUrl(); - - throw new FatalException( - `${input('ionic share')} has been removed.\n` + - `The functionality now exists in the Ionic Dashboard: ${strong(dashUrl)}` - ); - } -} diff --git a/packages/@ionic/cli/src/commands/signup.ts b/packages/@ionic/cli/src/commands/signup.ts deleted file mode 100644 index 220e85dd1a..0000000000 --- a/packages/@ionic/cli/src/commands/signup.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { CommandLineInputs, CommandLineOptions, CommandMetadata } from '../definitions'; -import { Command } from '../lib/command'; -import { openUrl } from '../lib/open'; - -export class SignupCommand extends Command { - async getMetadata(): Promise { - return { - name: 'signup', - type: 'global', - summary: 'Create an Ionic account', - description: ` -If you are having issues signing up, please get in touch with our Support[^support-request]. - `, - footnotes: [ - { - id: 'support-request', - url: 'https://ion.link/support-request', - }, - ], - }; - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - const dashUrl = this.env.config.getDashUrl(); - - await openUrl(`${dashUrl}/signup?source=cli`); - - this.env.log.ok('Launched signup form in your browser!'); - } -} diff --git a/packages/@ionic/cli/src/commands/ssh/add.ts b/packages/@ionic/cli/src/commands/ssh/add.ts deleted file mode 100644 index 4bbb3d44d2..0000000000 --- a/packages/@ionic/cli/src/commands/ssh/add.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { validators, MetadataGroup } from '@ionic/cli-framework'; -import { pathAccessible, pathExists } from '@ionic/utils-fs'; -import { expandPath, prettyPath } from '@ionic/utils-terminal'; -import * as fs from 'fs'; -import * as os from 'os'; -import * as path from 'path'; - -import { CommandInstanceInfo, CommandLineInputs, CommandLineOptions, CommandMetadata, CommandPreRun } from '../../definitions'; -import { isSuperAgentError } from '../../guards'; -import { input, strong } from '../../lib/color'; -import { FatalException } from '../../lib/errors'; -import { runCommand } from '../../lib/executor'; - -import { SSHBaseCommand } from './base'; - -export class SSHAddCommand extends SSHBaseCommand implements CommandPreRun { - async getMetadata(): Promise { - return { - name: 'add', - type: 'global', - summary: 'Add an SSH public key to Ionic', - inputs: [ - { - name: 'pubkey-path', - summary: 'Location of public key file to add to Ionic', - validators: [validators.required], - }, - ], - options: [ - { - name: 'use', - summary: 'Use the newly added key as your default SSH key for Ionic', - type: Boolean, - }, - ], - groups: [MetadataGroup.DEPRECATED], - }; - } - - async preRun(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - if (!inputs[0]) { - const defaultPubkeyPath = path.resolve(os.homedir(), '.ssh', 'id_rsa.pub'); - const defaultPubkeyExists = await pathAccessible(defaultPubkeyPath, fs.constants.R_OK); - - const pubkeyPath = await this.env.prompt({ - type: 'input', - name: 'pubkeyPath', - message: 'Enter the location to your public key file to upload to Ionic:', - default: defaultPubkeyExists ? prettyPath(defaultPubkeyPath) : undefined, - }); - - inputs[0] = pubkeyPath; - } - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions, runinfo: CommandInstanceInfo): Promise { - const { ERROR_SSH_INVALID_PUBKEY, SSHKeyClient, parsePublicKeyFile } = await import('../../lib/ssh'); - - const pubkeyPath = expandPath(inputs[0]); - const pubkeyName = prettyPath(pubkeyPath); - - let pubkey: string; - - try { - [ pubkey ] = await parsePublicKeyFile(pubkeyPath); - } catch (e: any) { - if (e.code === 'ENOENT') { - throw new FatalException( - `${strong(prettyPath(pubkeyPath))} does not appear to exist. Please specify a valid SSH public key.\n` + - `If you are having issues, try using ${input('ionic ssh setup')}.` - ); - } else if (e === ERROR_SSH_INVALID_PUBKEY) { - throw new FatalException( - `${strong(pubkeyName)} does not appear to be a valid SSH public key. (Not in ${strong('authorized_keys')} file format.)\n` + - `If you are having issues, try using ${input('ionic ssh setup')}.` - ); - } - - throw e; - } - - const user = this.env.session.getUser(); - const token = await this.env.session.getUserToken(); - const sshkeyClient = new SSHKeyClient({ client: this.env.client, token, user }); - - try { - const key = await sshkeyClient.create({ pubkey }); - this.env.log.ok(`Your public key (${strong(key.fingerprint)}) has been added to Ionic!`); - } catch (e: any) { - if (isSuperAgentError(e) && e.response.status === 409) { - this.env.log.msg('Pubkey already added to Ionic.'); - } else { - throw e; - } - } - - if (pubkeyPath.endsWith('.pub')) { - let confirm = options['use']; - - if (!confirm) { - confirm = await this.env.prompt({ - type: 'confirm', - name: 'confirm', - message: 'Would you like to use this key as your default for Ionic?', - }); - } - - if (confirm) { - const keyPath = pubkeyPath.substring(0, pubkeyPath.length - 4); // corresponding private key, theoretically - const keyExists = await pathExists(keyPath); - - if (keyExists) { - await runCommand(runinfo, ['ssh', 'use', prettyPath(keyPath)]); - } else { - this.env.log.error( - `SSH key does not exist: ${strong(prettyPath(keyPath))}.\n` + - `Please use ${input('ionic ssh use')} manually to use the corresponding private key.` - ); - } - } - } - } -} diff --git a/packages/@ionic/cli/src/commands/ssh/base.ts b/packages/@ionic/cli/src/commands/ssh/base.ts deleted file mode 100644 index b923da3c6f..0000000000 --- a/packages/@ionic/cli/src/commands/ssh/base.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ERROR_COMMAND_NOT_FOUND, SubprocessError } from '@ionic/utils-subprocess'; - -import { strong } from '../../lib/color'; -import { Command } from '../../lib/command'; -import { FatalException } from '../../lib/errors'; - -export abstract class SSHBaseCommand extends Command { - async checkForOpenSSH() { - try { - await this.env.shell.run('ssh', ['-V'], { stdio: 'ignore', showCommand: false, fatalOnNotFound: false }); - } catch (e: any) { - if (!(e instanceof SubprocessError && e.code === ERROR_COMMAND_NOT_FOUND)) { - throw e; - } - - this.env.log.warn('OpenSSH not found on your computer.'); // TODO: more helpful message - - throw new FatalException(`Command not found: ${strong('ssh')}`); - } - } -} diff --git a/packages/@ionic/cli/src/commands/ssh/delete.ts b/packages/@ionic/cli/src/commands/ssh/delete.ts deleted file mode 100644 index 571a2cb969..0000000000 --- a/packages/@ionic/cli/src/commands/ssh/delete.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { validators, MetadataGroup } from '@ionic/cli-framework'; - -import { CommandLineInputs, CommandLineOptions, CommandMetadata, CommandPreRun } from '../../definitions'; -import { input, strong } from '../../lib/color'; - -import { SSHBaseCommand } from './base'; - -export class SSHDeleteCommand extends SSHBaseCommand implements CommandPreRun { - async getMetadata(): Promise { - return { - name: 'delete', - type: 'global', - summary: 'Delete an SSH public key from Ionic', - inputs: [ - { - name: 'key-id', - summary: 'The ID of the public key to delete', - validators: [validators.required], - }, - ], - groups: [MetadataGroup.DEPRECATED], - }; - } - - async preRun(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - const { SSHKeyClient } = await import('../../lib/ssh'); - - if (!inputs[0]) { - const user = this.env.session.getUser(); - const token = await this.env.session.getUserToken(); - - const sshkeyClient = new SSHKeyClient({ client: this.env.client, user, token }); - const paginator = sshkeyClient.paginate(); - - const [ r ] = paginator; - const res = await r; - - if (res.data.length === 0) { - this.env.log.warn(`No SSH keys found. Use ${input('ionic ssh add')} to add keys to Ionic.`); - } - - inputs[0] = await this.env.prompt({ - type: 'list', - name: 'id', - message: 'Which SSH keys would you like to delete from Ionic?', - choices: res.data.map(key => ({ - name: `${key.fingerprint} ${key.name} ${key.annotation}`, - value: key.id, - })), - }); - } - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - const { SSHKeyClient } = await import('../../lib/ssh'); - - const [ id ] = inputs; - - const user = this.env.session.getUser(); - const token = await this.env.session.getUserToken(); - - const sshkeyClient = new SSHKeyClient({ client: this.env.client, user, token }); - await sshkeyClient.delete(id); - - this.env.log.ok(`Your public key (${strong(id)}) has been removed from Ionic.`); - } -} diff --git a/packages/@ionic/cli/src/commands/ssh/generate.ts b/packages/@ionic/cli/src/commands/ssh/generate.ts deleted file mode 100644 index 20600efd9e..0000000000 --- a/packages/@ionic/cli/src/commands/ssh/generate.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { MetadataGroup, contains, validate } from '@ionic/cli-framework'; -import { mkdirp, pathExists, unlink } from '@ionic/utils-fs'; -import { expandPath, prettyPath } from '@ionic/utils-terminal'; -import * as path from 'path'; - -import { CommandLineInputs, CommandLineOptions, CommandMetadata, CommandPreRun } from '../../definitions'; -import { input, strong } from '../../lib/color'; - -import { SSHBaseCommand } from './base'; - -const SSH_KEY_TYPES = ['ecdsa', 'ed25519', 'rsa']; - -export class SSHGenerateCommand extends SSHBaseCommand implements CommandPreRun { - async getMetadata(): Promise { - return { - name: 'generate', - type: 'global', - summary: 'Generates a private and public SSH key pair', - inputs: [ - { - name: 'key-path', - summary: 'Destination of private key file', - }, - ], - options: [ - { - name: 'type', - summary: `The type of key to generate: ${SSH_KEY_TYPES.map(v => input(v)).join(', ')}`, - default: 'rsa', - aliases: ['t'], - groups: [MetadataGroup.ADVANCED], - }, - { - name: 'bits', - summary: 'Number of bits in the key', - aliases: ['b'], - default: '2048', - groups: [MetadataGroup.ADVANCED], - }, - { - name: 'annotation', - summary: 'Annotation (comment) in public key. Your Ionic email address will be used', - aliases: ['C'], - groups: [MetadataGroup.ADVANCED], - }, - ], - groups: [MetadataGroup.DEPRECATED], - }; - } - - async preRun(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - await this.checkForOpenSSH(); - - await this.env.session.getUserToken(); - - if (!options['annotation']) { - options['annotation'] = this.env.config.get('user.email'); - } - - validate(String(options['type']), 'type', [contains(SSH_KEY_TYPES, { caseSensitive: false })]); - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - const { getGeneratedPrivateKeyPath } = await import('../../lib/ssh'); - - const { bits, annotation } = options; - - const keyPath = inputs[0] ? expandPath(String(inputs[0])) : await getGeneratedPrivateKeyPath(this.env.config.get('user.id')); - const keyPathDir = path.dirname(keyPath); - const pubkeyPath = `${keyPath}.pub`; - - if (!(await pathExists(keyPathDir))) { - await mkdirp(keyPathDir, 0o700 as any); - this.env.log.msg(`Created ${strong(prettyPath(keyPathDir))} directory for you.`); - } - - if (await pathExists(keyPath)) { - const confirm = await this.env.prompt({ - type: 'confirm', - name: 'confirm', - message: `Key ${strong(prettyPath(keyPath))} exists. Overwrite?`, - }); - - if (confirm) { - await unlink(keyPath); - } else { - this.env.log.msg(`Not overwriting ${strong(prettyPath(keyPath))}.`); - return; - } - } - - this.env.log.info( - 'Enter a passphrase for your private key.\n' + - `You will be prompted to provide a ${strong('passphrase')}, which is used to protect your private key should you lose it. (If someone has your private key, they can impersonate you!) Passphrases are recommended, but not required.\n` - ); - - await this.env.shell.run( - 'ssh-keygen', - ['-q', '-t', String(options['type']), '-b', String(bits), '-C', String(annotation), '-f', keyPath], - { stdio: 'inherit', showCommand: false, showError: false } - ); - - this.env.log.nl(); - - this.env.log.rawmsg( - `Private Key (${strong(prettyPath(keyPath))}): Keep this safe!\n` + - `Public Key (${strong(prettyPath(pubkeyPath))}): Give this to all your friends!\n\n` - ); - - this.env.log.ok('A new pair of SSH keys has been generated!'); - this.env.log.nl(); - - this.env.log.msg( - `${strong('Next steps:')}\n` + - ` * Add your public key to Ionic: ${input('ionic ssh add ' + prettyPath(pubkeyPath))}\n` + - ` * Use your private key for secure communication with Ionic: ${input('ionic ssh use ' + prettyPath(keyPath))}` - ); - } -} diff --git a/packages/@ionic/cli/src/commands/ssh/index.ts b/packages/@ionic/cli/src/commands/ssh/index.ts deleted file mode 100644 index 3e92e81832..0000000000 --- a/packages/@ionic/cli/src/commands/ssh/index.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { MetadataGroup } from '@ionic/cli-framework'; -import { input } from '../../lib/color'; -import { CommandMap, Namespace } from '../../lib/namespace'; - -export class SSHNamespace extends Namespace { - async getMetadata() { - const dashUrl = this.env.config.getDashUrl(); - - return { - name: 'ssh', - summary: 'Commands for configuring SSH keys', - description: ` -These commands help automate your SSH configuration for Ionic. As an alternative, SSH configuration can be done entirely manually by visiting your Personal Settings[^dashboard-settings-ssh-keys]. - -To begin, run ${input('ionic ssh setup')}, which lets you use existing keys or generate new ones just for Ionic. - -Deprecated. Developers should configure SSH by visiting their Personal Settings at https://dashboard.ionicframework.com/settings/ssh-keys. - `, - footnotes: [ - { - id: 'dashboard-settings-ssh-keys', - url: `${dashUrl}/settings/ssh-keys`, - }, - ], - groups: [MetadataGroup.DEPRECATED], - }; - } - - async getCommands(): Promise { - return new CommandMap([ - ['generate', async () => { const { SSHGenerateCommand } = await import('./generate'); return new SSHGenerateCommand(this); }], - ['use', async () => { const { SSHUseCommand } = await import('./use'); return new SSHUseCommand(this); }], - ['add', async () => { const { SSHAddCommand } = await import('./add'); return new SSHAddCommand(this); }], - ['delete', async () => { const { SSHDeleteCommand } = await import('./delete'); return new SSHDeleteCommand(this); }], - ['list', async () => { const { SSHListCommand } = await import('./list'); return new SSHListCommand(this); }], - ['setup', async () => { const { SSHSetupCommand } = await import('./setup'); return new SSHSetupCommand(this); }], - ['ls', 'list'], - ['remove', 'delete'], - ['rm', 'delete'], - ['g', 'generate'], - ]); - } -} diff --git a/packages/@ionic/cli/src/commands/ssh/list.ts b/packages/@ionic/cli/src/commands/ssh/list.ts deleted file mode 100644 index 29e74ece6c..0000000000 --- a/packages/@ionic/cli/src/commands/ssh/list.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { MetadataGroup } from '@ionic/cli-framework'; -import { columnar } from '@ionic/utils-terminal'; - -import { COLUMNAR_OPTIONS } from '../../constants'; -import { CommandLineInputs, CommandLineOptions, CommandMetadata, CommandPreRun } from '../../definitions'; -import { input, strong } from '../../lib/color'; - -import { SSHBaseCommand } from './base'; - -export class SSHListCommand extends SSHBaseCommand implements CommandPreRun { - async getMetadata(): Promise { - return { - name: 'list', - type: 'global', - summary: 'List your SSH public keys on Ionic', - options: [ - { - name: 'json', - summary: 'Output SSH keys in JSON', - type: Boolean, - }, - ], - groups: [MetadataGroup.DEPRECATED], - }; - } - - async preRun() { - await this.checkForOpenSSH(); - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - const { SSHKeyClient } = await import('../../lib/ssh'); - const { findHostSection, getConfigPath, isDirective, loadFromPath } = await import('../../lib/ssh-config'); - - const { json } = options; - - const user = this.env.session.getUser(); - const token = await this.env.session.getUserToken(); - - const sshkeyClient = new SSHKeyClient({ client: this.env.client, user, token }); - const paginator = sshkeyClient.paginate(); - - const [ r ] = paginator; - const res = await r; - - if (json) { - process.stdout.write(JSON.stringify(res.data)); - } else { - let activeFingerprint: string | undefined; - let foundActiveKey = false; - - const sshConfigPath = getConfigPath(); - const conf = await loadFromPath(sshConfigPath); - const section = findHostSection(conf, this.env.config.getGitHost()); - - if (section && section.config) { - const [ identityFile ] = section.config.filter(line => { - return isDirective(line) && line.param === 'IdentityFile'; - }); - - if (isDirective(identityFile)) { - const output = await this.env.shell.output('ssh-keygen', ['-E', 'sha256', '-lf', identityFile.value], { fatalOnError: false }); - activeFingerprint = output.trim().split(' ')[1]; - } - } - - if (res.data.length === 0) { - this.env.log.warn(`No SSH keys found. Use ${input('ionic ssh add')} to add keys to Ionic.`); - return; - } - - const keysMatrix = res.data.map(sshkey => { - const data = [sshkey.fingerprint, sshkey.name, sshkey.annotation]; - - if (sshkey.fingerprint === activeFingerprint) { - foundActiveKey = true; - return data.map(v => strong(v)); - } - - return data; - }); - - const table = columnar(keysMatrix, { - ...COLUMNAR_OPTIONS, - headers: ['fingerprint', 'name', 'annotation'].map(h => strong(h)), - }); - - if (foundActiveKey) { - this.env.log.nl(); - this.env.log.msg(`The row in ${strong('bold')} is the key that this computer is using. To change, use ${input('ionic ssh use')}.`); - } - - this.env.log.nl(); - this.env.log.rawmsg(table); - this.env.log.nl(); - this.env.log.ok(`Showing ${strong(String(res.data.length))} SSH key${res.data.length === 1 ? '' : 's'}.`); - this.env.log.nl(); - } - } -} diff --git a/packages/@ionic/cli/src/commands/ssh/setup.ts b/packages/@ionic/cli/src/commands/ssh/setup.ts deleted file mode 100644 index b8abd7a680..0000000000 --- a/packages/@ionic/cli/src/commands/ssh/setup.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { MetadataGroup } from '@ionic/cli-framework'; -import { pathExists } from '@ionic/utils-fs'; -import { prettyPath } from '@ionic/utils-terminal'; - -import { CommandInstanceInfo, CommandLineInputs, CommandLineOptions, CommandMetadata } from '../../definitions'; -import { input, strong } from '../../lib/color'; -import { FatalException } from '../../lib/errors'; -import { runCommand } from '../../lib/executor'; - -import { SSHBaseCommand } from './base'; - -export class SSHSetupCommand extends SSHBaseCommand { - async getMetadata(): Promise { - const dashUrl = this.env.config.getDashUrl(); - - return { - name: 'setup', - type: 'global', - summary: 'Setup your Ionic SSH keys automatically', - description: ` -This command offers a setup wizard for Ionic SSH keys using a series of prompts. For more control, see the commands available for managing SSH keys with the ${input('ionic ssh --help')} command. For an entirely manual approach, see ${strong('Personal Settings')} => ${strong('SSH Keys')} in the Dashboard[^dashboard-settings-ssh-keys]. - -If you are having issues setting up SSH keys, please get in touch with our Support[^support-request]. - `, - footnotes: [ - { - id: 'dashboard-settings-ssh-keys', - url: `${dashUrl}/settings/ssh-keys`, - }, - { - id: 'support-request', - url: 'https://ion.link/support-request', - }, - ], - groups: [MetadataGroup.DEPRECATED], - }; - } - - async preRun() { - await this.checkForOpenSSH(); - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions, runinfo: CommandInstanceInfo): Promise { - const { getGeneratedPrivateKeyPath } = await import('../../lib/ssh'); - const { getConfigPath } = await import('../../lib/ssh-config'); - const { promptToLogin } = await import('../../lib/session'); - - if (!this.env.session.isLoggedIn()) { - await promptToLogin(this.env); - } - - const CHOICE_AUTOMATIC = 'automatic'; - const CHOICE_MANUAL = 'manual'; - const CHOICE_SKIP = 'skip'; - const CHOICE_IGNORE = 'ignore'; - - if (this.env.config.get('git.setup')) { - const rerun = await this.env.prompt({ - type: 'confirm', - name: 'confirm', - message: `SSH setup wizard has run before. Would you like to run it again?`, - }); - - if (!rerun) { - return; - } - } else { - this.env.log.msg(`Looks like you haven't configured your SSH settings yet.`); - } - - // TODO: link to docs about manual git setup - - const setupChoice = await this.env.prompt({ - type: 'list', - name: 'setupChoice', - message: `How would you like to connect to Ionic?`, - choices: [ - { - name: 'Automatically setup new a SSH key pair for Ionic', - value: CHOICE_AUTOMATIC, - }, - { - name: 'Use an existing SSH key pair', - value: CHOICE_MANUAL, - }, - { - name: 'Skip for now', - value: CHOICE_SKIP, - }, - { - name: 'Ignore this prompt forever', - value: CHOICE_IGNORE, - }, - ], - }); - - if (setupChoice === CHOICE_AUTOMATIC) { - const sshconfigPath = getConfigPath(); - const keyPath = await getGeneratedPrivateKeyPath(this.env.config.get('user.id')); - const pubkeyPath = `${keyPath}.pub`; - - const [ pubkeyExists, keyExists ] = await Promise.all([pathExists(keyPath), pathExists(pubkeyPath)]); - - if (!pubkeyExists && !keyExists) { - this.env.log.info( - 'The automatic SSH setup will do the following:\n' + - `1) Generate a new SSH key pair with OpenSSH (will not overwrite any existing keys).\n` + - `2) Upload the generated SSH public key to our server, registering it on your account.\n` + - `3) Modify your SSH config (${strong(prettyPath(sshconfigPath))}) to use the generated SSH private key for our server(s).` - ); - - const confirm = await this.env.prompt({ - type: 'confirm', - name: 'confirm', - message: 'May we proceed?', - }); - - if (!confirm) { - throw new FatalException(); - } - } - - if (pubkeyExists && keyExists) { - this.env.log.msg( - `Using your previously generated key: ${strong(prettyPath(keyPath))}.\n` + - `You can generate a new one by deleting it.` - ); - } else { - await runCommand(runinfo, ['ssh', 'generate', keyPath]); - } - - await runCommand(runinfo, ['ssh', 'add', pubkeyPath, '--use']); - } else if (setupChoice === CHOICE_MANUAL) { - await runCommand(runinfo, ['ssh', 'add']); - } - - if (setupChoice === CHOICE_SKIP) { - this.env.log.warn(`Skipping for now. You can configure your SSH settings using ${input('ionic ssh setup')}.`); - } else { - if (setupChoice === CHOICE_IGNORE) { - this.env.log.ok(`We won't pester you about SSH settings anymore!`); - } else { - this.env.log.ok('SSH setup successful!'); - } - - this.env.config.set('git.setup', true); - } - } -} diff --git a/packages/@ionic/cli/src/commands/ssh/use.ts b/packages/@ionic/cli/src/commands/ssh/use.ts deleted file mode 100644 index 5e30aed4c2..0000000000 --- a/packages/@ionic/cli/src/commands/ssh/use.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { validators, MetadataGroup } from '@ionic/cli-framework'; -import { fileToString, writeFile } from '@ionic/utils-fs'; -import { expandPath, prettyPath } from '@ionic/utils-terminal'; - -import { CommandLineInputs, CommandLineOptions, CommandMetadata } from '../../definitions'; -import { input, strong } from '../../lib/color'; -import { FatalException } from '../../lib/errors'; - -import { SSHBaseCommand } from './base'; - -export class SSHUseCommand extends SSHBaseCommand { - async getMetadata(): Promise { - return { - name: 'use', - type: 'global', - summary: 'Set your active Ionic SSH key', - description: ` -This command modifies the SSH configuration file (${strong('~/.ssh/config')}) to set an active private key for the ${strong('git.ionicjs.com')} host. Read more about SSH configuration by running the ${input('man ssh_config')} command or by visiting online man pages[^ssh-config-docs]. - -Before making changes, ${input('ionic ssh use')} will print a diff and ask for permission to write the file. - `, - footnotes: [ - { - id: 'ssh-config-docs', - url: 'https://linux.die.net/man/5/ssh_config', - }, - ], - inputs: [ - { - name: 'key-path', - summary: 'Location of private key file to use', - validators: [validators.required], - }, - ], - groups: [MetadataGroup.DEPRECATED], - }; - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - const { ERROR_SSH_INVALID_PRIVKEY, ERROR_SSH_MISSING_PRIVKEY, validatePrivateKey } = await import('../../lib/ssh'); - const { ensureHostAndKeyPath, getConfigPath } = await import('../../lib/ssh-config'); - - const keyPath = expandPath(inputs[0]); - - try { - await validatePrivateKey(keyPath); - } catch (e: any) { - if (e === ERROR_SSH_MISSING_PRIVKEY) { - throw new FatalException( - `${strong(prettyPath(keyPath))} does not appear to exist. Please specify a valid SSH private key.\n` + - `If you are having issues, try using ${input('ionic ssh setup')}.` - ); - } else if (e === ERROR_SSH_INVALID_PRIVKEY) { - throw new FatalException( - `${strong(prettyPath(keyPath))} does not appear to be a valid SSH private key. (Missing '-----BEGIN RSA PRIVATE KEY-----' header.)\n` + - `If you are having issues, try using ${input('ionic ssh setup')}.` - ); - } else { - throw e; - } - } - - const { SSHConfig } = await import('../../lib/ssh-config'); - const sshConfigPath = getConfigPath(); - const text1 = await fileToString(sshConfigPath); - const conf = SSHConfig.parse(text1); - ensureHostAndKeyPath(conf, { host: this.env.config.getGitHost(), port: this.env.config.getGitPort() }, keyPath); - const text2 = SSHConfig.stringify(conf); - - if (text1 === text2) { - this.env.log.msg(`${strong(prettyPath(keyPath))} is already your active SSH key.`); - return; - } else { - const { diffPatch } = await import('../../lib/diff'); - const diff = await diffPatch(sshConfigPath, text1, text2); - - this.env.log.rawmsg(diff); - - const confirm = await this.env.prompt({ - type: 'confirm', - name: 'confirm', - message: `May we make the above change(s) to '${prettyPath(sshConfigPath)}'?`, - }); - - if (!confirm) { - // TODO: link to docs about manual git setup - throw new FatalException(); - } - } - - await writeFile(sshConfigPath, text2, { encoding: 'utf8', mode: 0o600 }); - - this.env.log.ok(`Your active Ionic SSH key has been set to ${strong(keyPath)}!`); - } -} diff --git a/packages/@ionic/cli/src/commands/ssl/base.ts b/packages/@ionic/cli/src/commands/ssl/base.ts deleted file mode 100644 index d449ff13d1..0000000000 --- a/packages/@ionic/cli/src/commands/ssl/base.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ERROR_COMMAND_NOT_FOUND, SubprocessError } from '@ionic/utils-subprocess'; - -import { input } from '../../lib/color'; -import { Command } from '../../lib/command'; -import { FatalException } from '../../lib/errors'; - -export abstract class SSLBaseCommand extends Command { - async checkForOpenSSL() { - try { - await this.env.shell.run('openssl', ['version'], { stdio: 'ignore', showCommand: false, fatalOnNotFound: false }); - } catch (e: any) { - if (!(e instanceof SubprocessError && e.code === ERROR_COMMAND_NOT_FOUND)) { - throw e; - } - - this.env.log.warn('OpenSSL not found on your computer.'); // TODO: more helpful message - - throw new FatalException(`Command not found: ${input('openssl')}`); - } - } -} diff --git a/packages/@ionic/cli/src/commands/ssl/generate.ts b/packages/@ionic/cli/src/commands/ssl/generate.ts deleted file mode 100644 index acb1a3e595..0000000000 --- a/packages/@ionic/cli/src/commands/ssl/generate.ts +++ /dev/null @@ -1,225 +0,0 @@ -import { MetadataGroup } from '@ionic/cli-framework'; -import { mkdirp, pathExists, tmpfilepath, unlink, writeFile } from '@ionic/utils-fs'; -import { prettyPath } from '@ionic/utils-terminal'; -import * as lodash from 'lodash'; -import * as path from 'path'; - -import { CommandLineInputs, CommandLineOptions, CommandMetadata, CommandPreRun } from '../../definitions'; -import { input, strong } from '../../lib/color'; -import { FatalException } from '../../lib/errors'; - -import { SSLBaseCommand } from './base'; - -const DEFAULT_BITS = '2048'; -const DEFAULT_COUNTRY_NAME = 'US'; -const DEFAULT_STATE_OR_PROVINCE_NAME = 'Wisconsin'; -const DEFAULT_LOCALITY_NAME = 'Madison'; -const DEFAULT_ORGANIZATION_NAME = 'Ionic'; -const DEFAULT_COMMON_NAME = 'localhost'; - -interface OpenSSLConfig { - bits: string; - countryName: string; - stateOrProvinceName: string; - localityName: string; - organizationName: string; - commonName: string; -} - -const DEFAULT_KEY_FILE = '.ionic/ssl/key.pem'; -const DEFAULT_CERT_FILE = '.ionic/ssl/cert.pem'; - -export class SSLGenerateCommand extends SSLBaseCommand implements CommandPreRun { - getDefaultKeyPath() { - return path.resolve(this.project ? this.project.directory : '', DEFAULT_KEY_FILE); - } - - getDefaultCertPath() { - return path.resolve(this.project ? this.project.directory : '', DEFAULT_CERT_FILE); - } - - async getMetadata(): Promise { - const defaultKeyPath = prettyPath(this.getDefaultKeyPath()); - const defaultCertPath = prettyPath(this.getDefaultCertPath()); - - return { - name: 'generate', - type: 'project', - summary: 'Generates an SSL key & certificate', - // TODO: document how to add trusted certs - description: ` -Uses OpenSSL to create a self-signed certificate for ${strong('localhost')} (by default). - -After the certificate is generated, you will still need to add it to your system or browser as a trusted certificate. - -The default directory for ${input('--key-path')} and ${input('--cert-path')} is ${input('.ionic/ssl/')}. - -Deprecated. Developers should generate an SSL certificate locally and then configure it using their project tooling such as Vite or Angular CLI. - `, - options: [ - { - name: 'key-path', - summary: 'Destination of private key file', - default: defaultKeyPath, - spec: { value: 'path' }, - }, - { - name: 'cert-path', - summary: 'Destination of certificate file', - default: defaultCertPath, - spec: { value: 'path' }, - }, - { - name: 'country-name', - summary: 'The country name (C) of the SSL certificate', - default: DEFAULT_COUNTRY_NAME, - groups: [MetadataGroup.ADVANCED], - spec: { value: 'C' }, - }, - { - name: 'state-or-province-name', - summary: 'The state or province name (ST) of the SSL certificate', - default: DEFAULT_STATE_OR_PROVINCE_NAME, - groups: [MetadataGroup.ADVANCED], - spec: { value: 'ST' }, - }, - { - name: 'locality-name', - summary: 'The locality name (L) of the SSL certificate', - default: DEFAULT_LOCALITY_NAME, - groups: [MetadataGroup.ADVANCED], - spec: { value: 'L' }, - }, - { - name: 'organization-name', - summary: 'The organization name (O) of the SSL certificate', - default: DEFAULT_ORGANIZATION_NAME, - groups: [MetadataGroup.ADVANCED], - spec: { value: 'O' }, - }, - { - name: 'common-name', - summary: 'The common name (CN) of the SSL certificate', - default: DEFAULT_COMMON_NAME, - groups: [MetadataGroup.ADVANCED], - spec: { value: 'CN' }, - }, - { - name: 'bits', - summary: 'Number of bits in the key', - aliases: ['b'], - default: DEFAULT_BITS, - groups: [MetadataGroup.ADVANCED], - }, - ], - groups: [MetadataGroup.DEPRECATED], - }; - } - - async preRun(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - await this.checkForOpenSSL(); - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - if (!this.project) { - throw new FatalException(`Cannot run ${input('ionic ssl generate')} outside a project directory.`); - } - - const keyPath = path.resolve(options['key-path'] ? String(options['key-path']) : this.getDefaultKeyPath()); - const keyPathDir = path.dirname(keyPath); - const certPath = path.resolve(options['cert-path'] ? String(options['cert-path']) : this.getDefaultCertPath()); - const certPathDir = path.dirname(certPath); - - const bits = options['bits'] ? String(options['bits']) : DEFAULT_BITS; - const countryName = options['country-name'] ? String(options['country-name']) : DEFAULT_COUNTRY_NAME; - const stateOrProvinceName = options['state-or-province-name'] ? String(options['state-or-province-name']) : DEFAULT_STATE_OR_PROVINCE_NAME; - const localityName = options['locality-name'] ? String(options['locality-name']) : DEFAULT_LOCALITY_NAME; - const organizationName = options['organization-name'] ? String(options['organization-name']) : DEFAULT_ORGANIZATION_NAME; - const commonName = options['common-name'] ? String(options['common-name']) : DEFAULT_COMMON_NAME; - - await this.ensureDirectory(keyPathDir); - await this.ensureDirectory(certPathDir); - - const overwriteKeyPath = await this.checkExistingFile(keyPath); - const overwriteCertPath = await this.checkExistingFile(certPath); - - if (overwriteKeyPath) { - await unlink(keyPath); - } - - if (overwriteCertPath) { - await unlink(certPath); - } - - const cnf = { bits, countryName, stateOrProvinceName, localityName, organizationName, commonName }; - const cnfPath = await this.writeConfig(cnf); - - await this.env.shell.run('openssl', ['req', '-x509', '-newkey', `rsa:${bits}`, '-nodes', '-subj', this.formatSubj(cnf), '-reqexts', 'SAN', '-extensions', 'SAN', '-config', cnfPath, '-days', '365', '-keyout', keyPath, '-out', certPath], {}); - - this.env.log.nl(); - - this.env.log.rawmsg( - `Key: ${strong(prettyPath(keyPath))}\n` + - `Cert: ${strong(prettyPath(certPath))}\n\n` - ); - - this.env.log.ok('Generated key & certificate!'); - } - - private formatSubj(cnf: OpenSSLConfig) { - const subjNames = new Map([ - ['countryName', 'C'], - ['stateOrProvinceName', 'ST'], - ['localityName', 'L'], - ['organizationName', 'O'], - ['commonName', 'CN'], - ]); - - return '/' + lodash.toPairs(cnf).filter(([k]) => subjNames.has(k)).map(([k, v]) => `${subjNames.get(k)}=${v}`).join('/'); - } - - private async ensureDirectory(p: string) { - if (!(await pathExists(p))) { - await mkdirp(p, 0o700 as any); - this.env.log.msg(`Created ${strong(prettyPath(p))} directory for you.`); - } - } - - private async checkExistingFile(p: string): Promise { - if (await pathExists(p)) { - const confirm = await this.env.prompt({ - type: 'confirm', - name: 'confirm', - message: `Key ${strong(prettyPath(p))} exists. Overwrite?`, - }); - - if (confirm) { - return true; - } else { - throw new FatalException(`Not overwriting ${strong(prettyPath(p))}.`); - } - } - } - - private async writeConfig({ bits, countryName, stateOrProvinceName, localityName, organizationName, commonName }: OpenSSLConfig): Promise { - const cnf = ` -[req] -default_bits = ${bits} -distinguished_name = req_distinguished_name - -[req_distinguished_name] -countryName = ${countryName} -stateOrProvinceName = ${stateOrProvinceName} -localityName = ${localityName} -organizationName = ${organizationName} -commonName = ${commonName} - -[SAN] -subjectAltName=DNS:${commonName} -`.trim(); - - const p = tmpfilepath('ionic-ssl'); - await writeFile(p, cnf, { encoding: 'utf8' }); - return p; - } -} diff --git a/packages/@ionic/cli/src/commands/ssl/index.ts b/packages/@ionic/cli/src/commands/ssl/index.ts deleted file mode 100644 index 21988e0309..0000000000 --- a/packages/@ionic/cli/src/commands/ssl/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { MetadataGroup } from '@ionic/cli-framework'; - -import { input } from '../../lib/color'; -import { CommandMap, Namespace } from '../../lib/namespace'; - -export class SSLNamespace extends Namespace { - async getMetadata() { - return { - name: 'ssl', - summary: 'Commands for managing SSL keys & certificates', - groups: [MetadataGroup.DEPRECATED], - description: ` -These commands make it easy to manage SSL certificates for using HTTPS with ${input('ionic serve')}. - `, - }; - } - - async getCommands(): Promise { - return new CommandMap([ - ['generate', async () => { const { SSLGenerateCommand } = await import('./generate'); return new SSLGenerateCommand(this); }], - ['g', 'generate'], - ]); - } -} diff --git a/packages/@ionic/cli/src/commands/start.ts b/packages/@ionic/cli/src/commands/start.ts deleted file mode 100644 index 2beb5db709..0000000000 --- a/packages/@ionic/cli/src/commands/start.ts +++ /dev/null @@ -1,1193 +0,0 @@ -import { MetadataGroup, validators } from '@ionic/cli-framework'; -import { isValidURL, slugify } from '@ionic/cli-framework/utils/string'; -import { mkdir, pathExists, remove, unlink } from '@ionic/utils-fs'; -import { columnar, prettyPath } from '@ionic/utils-terminal'; -import chalk from 'chalk'; -import { debug as Debug } from 'debug'; -import * as path from 'path'; - -import { COLUMNAR_OPTIONS, PROJECT_FILE, ANGULAR_STANDALONE } from '../constants'; -import { - CommandInstanceInfo, - CommandLineInputs, - CommandLineOptions, - CommandMetadata, - CommandPreRun, - IProject, - IShellRunOptions, - ProjectType, - ResolvedStarterTemplate, - StarterManifest, -} from '../definitions'; -import { failure, input, strong } from '../lib/color'; -import { Command } from '../lib/command'; -import { FatalException } from '../lib/errors'; -import { runCommand } from '../lib/executor'; -import { - createProjectFromDetails, - createProjectFromDirectory, - isValidProjectId, -} from '../lib/project'; -import { promptToSignup } from '../lib/session'; -import { prependNodeModulesBinToPath } from '../lib/shell'; -import { - AppSchema, - STARTER_BASE_URL, - STARTER_TEMPLATES, - SUPPORTED_FRAMEWORKS, - getAdvertisement, - getStarterList, - getStarterProjectTypes, - readStarterManifest, - verifyOptions, -} from '../lib/start'; -import { emoji } from '../lib/utils/emoji'; -import { createRequest } from '../lib/utils/http'; - -const debug = Debug('ionic:commands:start'); - -interface StartWizardApp { - type: ProjectType; - name: string; - appId: string; - template: string; - 'package-id': string; - tid: string; - email: string; - theme: string; - ip: string; - appIcon: string; - appSplash: string; - utm: { [key: string]: string }; -} - -export class StartCommand extends Command implements CommandPreRun { - private canRemoveExisting = false; - - private schema?: AppSchema; - - async getMetadata(): Promise { - return { - name: 'start', - type: 'global', - summary: 'Create a new project', - description: ` -This command creates a working Ionic app. It installs dependencies for you and sets up your project. - -Running ${input( - 'ionic start' - )} without any arguments will prompt you for information about your new project. - -The first argument is your app's ${input( - 'name' - )}. Don't worry--you can always change this later. The ${input( - '--project-id' - )} is generated from ${input('name')} unless explicitly specified. - -The second argument is the ${input( - 'template' - )} from which to generate your app. You can list all templates with the ${input( - '--list' - )} option. You can also specify a git repository URL for ${input( - 'template' - )}, in which case the existing project will be cloned. - -Use the ${input( - '--type' - )} option to start projects using a different JavaScript Framework. Use ${input( - '--list' - )} to see all project types and templates. - `, - exampleCommands: [ - '', - '--list', - 'myApp', - 'myApp blank', - 'myApp tabs --capacitor', - 'myApp list --type=vue', - '"My App" blank', - '"Conference App" https://github.com/ionic-team/ionic-conference-app', - ], - inputs: [ - { - name: 'name', - summary: `The name of your new project (e.g. ${input( - 'myApp' - )}, ${input('"My App"')})`, - validators: [validators.required], - }, - { - name: 'template', - summary: `The starter template to use (e.g. ${['blank', 'tabs'] - .map((t) => input(t)) - .join(', ')}; use ${input('--list')} to see all)`, - validators: [validators.required], - }, - ], - options: [ - { - name: 'list', - summary: 'List available starter templates', - type: Boolean, - aliases: ['l'], - }, - { - name: 'type', - summary: `Type of project to start (e.g. ${getStarterProjectTypes() - .map((type) => input(type)) - .join(', ')})`, - type: String, - }, - { - name: 'cordova', - summary: 'Include Cordova integration', - type: Boolean, - groups: [MetadataGroup.DEPRECATED], - }, - { - name: 'capacitor', - summary: 'Include Capacitor integration', - type: Boolean, - }, - { - name: 'deps', - summary: 'Do not install npm/yarn dependencies', - type: Boolean, - default: true, - groups: [MetadataGroup.ADVANCED], - }, - { - name: 'git', - summary: 'Do not initialize a git repo', - type: Boolean, - default: true, - groups: [MetadataGroup.ADVANCED], - }, - { - name: 'link', - summary: 'Connect your new app to Ionic', - type: Boolean, - groups: [MetadataGroup.ADVANCED], - }, - { - name: 'id', - summary: 'Specify an Ionic App ID to link', - }, - { - name: 'project-id', - summary: - 'Specify a slug for your app (used for the directory name and package name)', - groups: [MetadataGroup.ADVANCED], - spec: { value: 'slug' }, - }, - { - name: 'package-id', - summary: - 'Specify the bundle ID/application ID for your app (reverse-DNS notation)', - groups: [MetadataGroup.ADVANCED], - spec: { value: 'id' }, - }, - { - name: 'start-id', - summary: - 'Used by the Ionic app start experience to generate an associated app locally', - groups: [MetadataGroup.HIDDEN], - spec: { value: 'id' }, - }, - { - name: 'tag', - summary: `Specify a tag to use for the starters (e.g. ${[ - 'latest', - 'testing', - 'next', - ] - .map((t) => input(t)) - .join(', ')})`, - default: 'latest', - groups: [MetadataGroup.HIDDEN], - }, - ], - }; - } - - async startIdStart(inputs: CommandLineInputs, options: CommandLineOptions) { - const startId = options['start-id']; - - const wizardApiUrl = - process.env.START_WIZARD_URL_BASE || `https://ionicframework.com`; - - const { req } = await createRequest( - 'GET', - `${wizardApiUrl}/api/v1/wizard/app/${startId}`, - this.env.config.getHTTPConfig() - ); - - const error = (e?: Error) => { - this.env.log.error( - `No such app ${chalk.bold( - startId - )}. This app configuration may have expired. Please retry at https://ionicframework.com/start` - ); - if (e) { - throw e; - } - }; - - let data: StartWizardApp; - try { - const ret = await req; - if (ret.status !== 200) { - return error(); - } - - data = (await req).body as StartWizardApp; - - if (!data) { - return error(); - } - } catch (e: any) { - return error(e); - } - - let projectDir = slugify(data.name); - if (inputs.length === 1) { - projectDir = inputs[0]; - } - - await this.checkForExisting(projectDir); - - inputs.push(data.name); - inputs.push(data.template); - - await this.startIdConvert(startId as string); - - const appIconBuffer = data.appIcon - ? Buffer.from( - data.appIcon.replace(/^data:image\/\w+;base64,/, ''), - 'base64' - ) - : undefined; - - const splashBuffer = data.appSplash - ? Buffer.from( - data.appSplash.replace(/^data:image\/\w+;base64,/, ''), - 'base64' - ) - : undefined; - - this.schema = { - cloned: false, - name: data.name, - type: data.type, - template: data.template, - projectId: slugify(data.name), - projectDir, - packageId: data['package-id'], - appflowId: undefined, - appIcon: appIconBuffer, - splash: splashBuffer, - themeColor: data.theme, - }; - } - - async startIdConvert(id: string) { - const wizardApiUrl = - process.env.START_WIZARD_URL_BASE || `https://ionicframework.com`; - - if (!wizardApiUrl) { - return; - } - - const { req } = await createRequest( - 'POST', - `${wizardApiUrl}/api/v1/wizard/app/${id}/start`, - this.env.config.getHTTPConfig() - ); - - try { - await req; - } catch (e: any) { - this.env.log.warn(`Unable to set app flag on server: ${e.message}`); - } - } - - /** - * Check if we should use the wizard for the start command. - * We should use if they ran `ionic start` or `ionic start --capacitor` - * and they are in an interactive environment. - */ - async shouldUseStartWizard( - inputs: CommandLineInputs, - options: CommandLineOptions - ) { - const flagsToTestFor = [ - 'list', - 'l', - 'cordova', - 'link', - 'help', - 'h', - 'type', - 'id', - 'project-id', - 'package-id', - 'start-id', - ]; - - let didUseFlags = false; - - for (const key of flagsToTestFor) { - if (options[key] !== null) { - didUseFlags = true; - break; - } - } - - return ( - inputs.length === 0 && - options['interactive'] && - options['deps'] && - options['git'] && - !didUseFlags - ); - } - - async preRun( - inputs: CommandLineInputs, - options: CommandLineOptions - ): Promise { - const { promptToLogin } = await import('../lib/session'); - - verifyOptions(options, this.env); - - if (await this.shouldUseStartWizard(inputs, options)) { - const confirm = await this.env.prompt({ - type: 'confirm', - name: 'confirm', - message: 'Use the app creation wizard?', - default: true, - }); - - if (confirm) { - const startId = await this.env.session.wizardLogin(); - if (!startId) { - this.env.log.error( - 'There was an issue using the web wizard. Falling back to CLI wizard.' - ); - } else { - options['start-id'] = startId; - } - } - } - - const appflowId = options['id'] ? String(options['id']) : undefined; - - if (appflowId) { - if (!this.env.session.isLoggedIn()) { - await promptToLogin(this.env); - } - } - - // The start wizard pre-populates all arguments for the CLI - if (options['start-id']) { - await this.startIdStart(inputs, options); - return; - } - - let projectType = isValidURL(inputs[1]) - ? 'custom' - : options['type'] - ? String(options['type']) - : await this.getProjectType(); - - if (options['cordova']) { - const { checkForUnsupportedProject } = await import( - '../lib/integrations/cordova/utils' - ); - - try { - await checkForUnsupportedProject(projectType as ProjectType); - } catch (e: any) { - this.env.log.error(e.message); - options['cordova'] = false; - } - } - - if (!inputs[0]) { - if (appflowId) { - const { AppClient } = await import('../lib/app'); - const token = await this.env.session.getUserToken(); - const appClient = new AppClient(token, this.env); - const tasks = this.createTaskChain(); - tasks.next(`Looking up app ${input(appflowId)}`); - const app = await appClient.load(appflowId); - // TODO: can ask to clone via repo_url - tasks.end(); - this.env.log.info( - `Using ${strong(app.name)} for ${input('name')} and ${strong( - app.slug - )} for ${input('--project-id')}.` - ); - inputs[0] = app.name; - options['project-id'] = app.slug; - } else { - if (this.env.flags.interactive) { - this.env.log.nl(); - this.env.log.msg( - `${strong(`Every great app needs a name! ${emoji('😍', '')}`)}\n` + - `Please enter the full name of your app. You can change this at any time. To bypass this prompt next time, supply ${input( - 'name' - )}, the first argument to ${input('ionic start')}.\n\n` - ); - } - - const name = await this.env.prompt({ - type: 'input', - name: 'name', - message: 'Project name:', - validate: (v) => validators.required(v), - }); - - inputs[0] = name; - } - } - - if (!inputs[1]) { - if (this.env.flags.interactive) { - this.env.log.nl(); - this.env.log.msg( - `${strong( - `Let's pick the perfect starter template! ${emoji('💪', '')}` - )}\n` + - `Starter templates are ready-to-go Ionic apps that come packed with everything you need to build your app. To bypass this prompt next time, supply ${input( - 'template' - )}, the second argument to ${input('ionic start')}.\n\n` - ); - } - - const template = await this.env.prompt({ - type: 'list', - name: 'template', - message: 'Starter template:', - choices: () => { - const starterTemplateList = STARTER_TEMPLATES.filter( - (st) => st.projectType === projectType - ); - const cols = columnar( - starterTemplateList.map(({ name, description }) => [ - input(name), - description || '', - ]), - COLUMNAR_OPTIONS - ).split('\n'); - - if (starterTemplateList.length === 0) { - throw new FatalException( - `No starter templates found for project type: ${input( - projectType - )}.` - ); - } - - return starterTemplateList.map((starter, i) => { - return { - name: cols[i], - short: starter.name, - value: starter.name, - }; - }); - }, - }); - - inputs[1] = template; - } - - - let starterTemplate = STARTER_TEMPLATES.find( - (t) => t.name === inputs[1] && t.projectType === projectType - ); - - - if (projectType === 'angular') { - const angularMode = await this.env.prompt({ - type: 'list', - name: 'standalone', - message: 'Would you like to build your app with Standalone Components or NgModules? \n Standalone components are the default way to build with Angular that simplifies the way you build your app. \n To learn more, visit the Angular docs:\n https://angular.dev/guide/components\n\n', - choices: () => [ - { - name: 'Standalone', - short: 'Standalone', - value: 'standalone', - }, - { - name: 'NgModules', - short: 'NgModules', - value: 'ngModules', - } - ], - }); - - /** - * If the developer wants to use standalone - * components then we need to get the correct starter. - */ - if (angularMode === 'standalone') { - /** - * Attempt to find the same type of starter - * but with standalone components. - */ - const standaloneStarter = STARTER_TEMPLATES.find((t) => t.name === inputs[1] && t.projectType === ANGULAR_STANDALONE); - - /** - * If found, update the projectType and - * starterTemplate vars to use the new project. - * If no project is found it will continue - * to use the NgModule version. - */ - if (standaloneStarter !== undefined) { - projectType = ANGULAR_STANDALONE; - starterTemplate = standaloneStarter; - } - } - } - - if (starterTemplate && starterTemplate.type === 'repo') { - inputs[1] = starterTemplate.repo; - } - - const cloned = isValidURL(inputs[1]); - - if (this.project && this.project.details.context === 'app') { - const confirm = await this.env.prompt({ - type: 'confirm', - name: 'confirm', - message: - 'You are already in an Ionic project directory. Do you really want to start another project here?', - default: false, - }); - - if (!confirm) { - this.env.log.info('Not starting project within existing project.'); - throw new FatalException(); - } - } - - await this.validateProjectType(projectType); - - if (cloned) { - if (!options['git']) { - this.env.log.warn( - `The ${input( - '--no-git' - )} option has no effect when cloning apps. Git must be used.` - ); - } - - options['git'] = true; - } - - if (options['v1'] || options['v2']) { - throw new FatalException( - `The ${input('--v1')} and ${input('--v2')} flags have been removed.\n` + - `Use the ${input('--type')} option. (see ${input( - 'ionic start --help' - )})` - ); - } - - if (options['app-name']) { - this.env.log.warn( - `The ${input('--app-name')} option has been removed. Use the ${input( - 'name' - )} argument with double quotes: e.g. ${input('ionic start "My App"')}` - ); - } - - if (options['display-name']) { - this.env.log.warn( - `The ${input( - '--display-name' - )} option has been removed. Use the ${input( - 'name' - )} argument with double quotes: e.g. ${input('ionic start "My App"')}` - ); - } - - if (options['bundle-id']) { - this.env.log.warn( - `The ${input( - '--bundle-id' - )} option has been deprecated. Please use ${input('--package-id')}.` - ); - options['package-id'] = options['bundle-id']; - } - - let projectId = options['project-id'] - ? String(options['project-id']) - : undefined; - - if (projectId) { - await this.validateProjectId(projectId); - } else { - projectId = options['project-id'] = isValidProjectId(inputs[0]) - ? inputs[0] - : slugify(inputs[0]); - } - - const projectDir = path.resolve(projectId); - const packageId = options['package-id'] - ? String(options['package-id']) - : undefined; - - if (projectId) { - await this.checkForExisting(projectDir); - } - - if (cloned) { - this.schema = { - cloned: true, - url: inputs[1], - projectId, - projectDir, - }; - } else { - this.schema = { - cloned: false, - name: inputs[0], - type: projectType as ProjectType, - template: inputs[1], - projectId, - projectDir, - packageId, - appflowId, - themeColor: undefined, - }; - } - } - - async getProjectType() { - if (this.env.flags.interactive) { - this.env.log.nl(); - this.env.log.msg( - `${strong(`Pick a framework! ${emoji('😁', '')}`)}\n\n` + - `Please select the JavaScript framework to use for your new app. To bypass this prompt next time, supply a value for the ${input( - '--type' - )} option.\n\n` - ); - } - - const frameworkChoice = await this.env.prompt({ - type: 'list', - name: 'frameworks', - message: 'Framework:', - default: 'angular', - choices: () => { - const cols = columnar( - SUPPORTED_FRAMEWORKS.map(({ name, description }) => [ - input(name), - description, - ]), - COLUMNAR_OPTIONS - ).split('\n'); - return SUPPORTED_FRAMEWORKS.map((starterTemplate, i) => { - return { - name: cols[i], - short: starterTemplate.name, - value: starterTemplate.type, - }; - }); - }, - }); - - return frameworkChoice; - } - - async run( - inputs: CommandLineInputs, - options: CommandLineOptions, - runinfo: CommandInstanceInfo - ): Promise { - const { pkgManagerArgs } = await import('../lib/utils/npm'); - const { getTopLevel, isGitInstalled } = await import('../lib/git'); - - if (!this.schema) { - throw new FatalException(`Invalid start schema: cannot start app.`); - } - - const { projectId, projectDir, packageId, appflowId } = this.schema; - - const tag = options['tag'] ? String(options['tag']) : 'latest'; - let linkConfirmed = typeof appflowId === 'string'; - - const gitDesired = options['git'] ? true : false; - const gitInstalled = await isGitInstalled(this.env); - const gitTopLevel = await getTopLevel(this.env); - - let gitIntegration = - gitDesired && gitInstalled && !gitTopLevel ? true : false; - - if (!gitInstalled) { - const installationDocs = `See installation docs for git: ${strong( - 'https://git-scm.com/book/en/v2/Getting-Started-Installing-Git' - )}`; - - if (appflowId) { - throw new FatalException( - `Git CLI not found on your PATH.\n` + - `Git must be installed to connect this app to Ionic. ${installationDocs}` - ); - } - - if (this.schema.cloned) { - throw new FatalException( - `Git CLI not found on your PATH.\n` + - `Git must be installed to clone apps with ${input( - 'ionic start' - )}. ${installationDocs}` - ); - } - } - - if (gitTopLevel && !this.schema.cloned) { - this.env.log.info( - `Existing git project found (${strong( - gitTopLevel - )}). Git operations are disabled.` - ); - } - - const tasks = this.createTaskChain(); - tasks.next(`Preparing directory ${input(prettyPath(projectDir))}`); - - if (this.canRemoveExisting) { - await remove(projectDir); - } - - await mkdir(projectDir); - - tasks.end(); - - if (this.schema.cloned) { - await this.env.shell.run( - 'git', - ['clone', this.schema.url, projectDir, '--progress'], - { stdio: 'inherit' } - ); - } else { - const starterTemplate = await this.findStarterTemplate( - this.schema.template, - this.schema.type, - tag - ); - await this.downloadStarterTemplate(projectDir, starterTemplate); - } - - let project: IProject | undefined; - - if ( - this.project && - this.project.details.context === 'multiapp' && - !this.schema.cloned - ) { - // We're in a multi-app setup, so the new config file isn't wanted. - await unlink(path.resolve(projectDir, PROJECT_FILE)); - - project = await createProjectFromDetails( - { - context: 'multiapp', - configPath: path.resolve(this.project.rootDirectory, PROJECT_FILE), - id: projectId, - type: this.schema.type, - errors: [], - }, - this.env - ); - project.config.set('type', this.schema.type); - project.config.set( - 'root', - path.relative(this.project.rootDirectory, projectDir) - ); - } else { - project = await createProjectFromDirectory( - projectDir, - { _: [] }, - this.env, - { logErrors: false } - ); - } - - // start is weird, once the project directory is created, it becomes a - // "project" command and so we replace the `Project` instance that was - // autogenerated when the CLI booted up. This has worked thus far? - this.namespace.root.project = project; - - if (!this.project) { - throw new FatalException('Error while loading project.'); - } - - this.env.shell.alterPath = (p) => - prependNodeModulesBinToPath(projectDir, p); - - if (!this.schema.cloned) { - if (this.schema.type === 'react' || this.schema.type === 'vue') { - options['capacitor'] = true; - } - - if ( - (this.schema.type === 'angular' || this.schema.type === 'angular-standalone') - && options['cordova'] === null - ) { - options['capacitor'] = true; - } - - if (options['cordova']) { - const { confirmCordovaUsage } = await import( - '../lib/integrations/cordova/utils' - ); - const confirm = await confirmCordovaUsage(this.env); - - if (confirm) { - await runCommand(runinfo, [ - 'integrations', - 'enable', - 'cordova', - '--quiet', - ]); - } else { - options['cordova'] = false; - } - } - - if (options['capacitor'] === null && !options['cordova']) { - const confirm = await this.env.prompt({ - type: 'confirm', - name: 'confirm', - message: - 'Integrate your new app with Capacitor to target native iOS and Android?', - default: false, - }); - - if (confirm) { - options['capacitor'] = true; - } - } - - if (options['capacitor']) { - await runCommand(runinfo, [ - 'integrations', - 'enable', - 'capacitor', - '--quiet', - '--', - this.schema.name, - packageId ? packageId : 'io.ionic.starter', - ]); - } - - await this.project.personalize({ - name: this.schema.name, - projectId, - packageId, - appIcon: this.schema.appIcon, - splash: this.schema.splash, - themeColor: this.schema.themeColor, - }); - - this.env.log.nl(); - } - - const shellOptions: IShellRunOptions = { - cwd: projectDir, - stdio: 'inherit', - }; - - if (options['deps']) { - this.env.log.msg('Installing dependencies may take several minutes.'); - this.env.log.rawmsg(getAdvertisement()); - - const [installer, ...installerArgs] = await pkgManagerArgs( - this.env.config.get('npmClient'), - { command: 'install' } - ); - await this.env.shell.run(installer, installerArgs, shellOptions); - - if (options['cordova']) { - try { - await this.env.shell.run( - 'ng', - ['add', '@ionic/cordova-builders', '--skip-confirmation'], - { cwd: this.project.rootDirectory } - ); - } catch (e: any) { - debug('Error while adding @ionic/cordova-builders: %O', e); - } - } - } else { - // --no-deps flag was used so skip installing dependencies, this also results in the package.json being out sync with the package.json so warn the user - this.env.log.warn( - 'Using the --no-deps flag results in an out of date package lock file. The lock file can be updated by performing an `install` with your package manager.' - ); - if (options['cordova']) { - this.env.log.warn( - "@ionic/cordova-builders couldn't be added, make sure you run `ng add @ionic/cordova-builders` after performing an `install` with your package manager." - ); - } - } - - if (!this.schema.cloned) { - if (gitIntegration) { - try { - await this.env.shell.run('git', ['init'], shellOptions); // TODO: use initializeRepo()? - } catch (e: any) { - this.env.log.warn( - 'Error encountered during repo initialization. Disabling further git operations.' - ); - gitIntegration = false; - } - } - - // Prompt to create account - if (!this.env.session.isLoggedIn()) { - await promptToSignup(this.env); - } - - if (options['link']) { - const cmdArgs = ['link']; - - if (appflowId) { - cmdArgs.push(appflowId); - } - - cmdArgs.push('--name', this.schema.name); - - await runCommand(runinfo, cmdArgs); - linkConfirmed = true; - } - - const manifestPath = path.resolve(projectDir, 'ionic.starter.json'); - const manifest = await this.loadManifest(manifestPath); - - if (manifest) { - await unlink(manifestPath); - } - - if (gitIntegration) { - try { - await this.env.shell.run('git', ['add', '-A'], shellOptions); - await this.env.shell.run( - 'git', - ['commit', '-m', 'Initial commit', '--no-gpg-sign'], - shellOptions - ); - } catch (e: any) { - this.env.log.warn( - 'Error encountered during commit. Disabling further git operations.' - ); - gitIntegration = false; - } - } - - if (manifest) { - await this.performManifestOps(manifest); - } - } - - this.env.log.nl(); - - await this.showNextSteps( - projectDir, - this.schema.cloned, - linkConfirmed, - !options['cordova'] - ); - } - - async checkForExisting(projectDir: string) { - const projectExists = await pathExists(projectDir); - - if (projectExists) { - const confirm = await this.env.prompt({ - type: 'confirm', - name: 'confirm', - message: `${input(prettyPath(projectDir))} exists. ${failure( - 'Overwrite?' - )}`, - default: false, - }); - - if (!confirm) { - this.env.log.msg( - `Not erasing existing project in ${input(prettyPath(projectDir))}.` - ); - throw new FatalException(); - } - - this.canRemoveExisting = confirm; - } - } - - async findStarterTemplate( - template: string, - type: string, - tag: string - ): Promise { - const starterTemplate = STARTER_TEMPLATES.find( - (t) => t.projectType === type && t.name === template - ); - - if (starterTemplate && starterTemplate.type === 'managed') { - return { - ...starterTemplate, - archive: `${STARTER_BASE_URL}/${tag === 'latest' ? '' : `${tag}/`}${starterTemplate.id - }.tar.gz`, - }; - } - - const tasks = this.createTaskChain(); - tasks.next('Looking up starter'); - const starterList = await getStarterList(this.env.config, tag); - - const starter = starterList.starters.find( - (t) => t.type === type && t.name === template - ); - - if (starter) { - tasks.end(); - - return { - name: starter.name, - projectType: starter.type, - archive: `${STARTER_BASE_URL}/${tag === 'latest' ? '' : `${tag}/`}${starter.id - }.tar.gz`, - }; - } else { - throw new FatalException( - `Unable to find starter template for ${input(template)}\n` + - `If this is not a typo, please make sure it is a valid starter template within the starters repo: ${strong( - 'https://github.com/ionic-team/starters' - )}` - ); - } - } - - async validateProjectType(type: string) { - const projectTypes = getStarterProjectTypes(); - - if (!['custom', ...projectTypes].includes(type)) { - throw new FatalException( - `${input(type)} is not a valid project type.\n` + - `Please choose a different ${input('--type')}. Use ${input( - 'ionic start --list' - )} to list all available starter templates.` - ); - } - } - - async validateProjectId(projectId: string) { - if (!isValidProjectId(projectId)) { - throw new FatalException( - `${input(projectId)} is not a valid package or directory name.\n` + - `Please choose a different ${input( - '--project-id' - )}. Alphanumeric characters are always safe.` - ); - } - } - - async loadManifest( - manifestPath: string - ): Promise { - try { - return await readStarterManifest(manifestPath); - } catch (e: any) { - debug( - `Error with manifest file ${strong(prettyPath(manifestPath))}: ${e}` - ); - } - } - - async performManifestOps(manifest: StarterManifest) { - if (manifest.welcome) { - this.env.log.nl(); - this.env.log.msg(`${strong('Starter Welcome')}:`); - this.env.log.msg(manifest.welcome); - } - } - - async downloadStarterTemplate( - projectDir: string, - starterTemplate: ResolvedStarterTemplate - ) { - const { createRequest, download } = await import('../lib/utils/http'); - const { tar } = await import('../lib/utils/archive'); - - const tasks = this.createTaskChain(); - const task = tasks.next( - `Downloading and extracting ${input( - starterTemplate.name.toString() - )} starter` - ); - debug('Tar extraction created for %s', projectDir); - const ws = tar.extract({ cwd: projectDir }); - - const { req } = await createRequest( - 'GET', - starterTemplate.archive, - this.env.config.getHTTPConfig() - ); - await download(req, ws, { - progress: (loaded, total) => task.progress(loaded, total), - }); - - tasks.end(); - } - - async showNextSteps( - projectDir: string, - cloned: boolean, - linkConfirmed: boolean, - isCapacitor: boolean - ) { - const cordovaResCommand = isCapacitor - ? 'cordova-res --skip-config --copy' - : 'cordova-res'; - const steps = [ - `Go to your ${cloned ? 'cloned' : 'new'} project: ${input( - `cd ${prettyPath(projectDir)}` - )}`, - `Run ${input( - 'ionic serve' - )} within the app directory to see your app in the browser`, - isCapacitor - ? `Run ${input( - 'ionic capacitor add' - )} to add a native iOS or Android project using Capacitor` - : `Run ${input( - 'ionic cordova platform add' - )} to add a native iOS or Android project using Cordova`, - `Generate your app icon and splash screens using ${input( - cordovaResCommand - )}`, - `Explore the Ionic docs for components, tutorials, and more: ${strong( - 'https://ion.link/docs' - )}`, - `Building an enterprise app? Ionic has Enterprise Support and Features: ${strong( - 'https://ion.link/enterprise-edition' - )}`, - ]; - - if (linkConfirmed) { - steps.push( - `Push your code to Ionic Appflow to perform real-time updates, and more: ${input( - 'git push ionic master' - )}` - ); - } - - this.env.log.msg( - `${strong('Your Ionic app is ready! Follow these next steps')}:\n${steps - .map((s) => ` - ${s}`) - .join('\n')}` - ); - } -} diff --git a/packages/@ionic/cli/src/commands/state.ts b/packages/@ionic/cli/src/commands/state.ts deleted file mode 100644 index 7ec982b1c2..0000000000 --- a/packages/@ionic/cli/src/commands/state.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { MetadataGroup } from '@ionic/cli-framework'; -import { columnar, indent } from '@ionic/utils-terminal'; - -import { COLUMNAR_OPTIONS } from '../constants'; -import { CommandMetadata } from '../definitions'; -import { input, strong } from '../lib/color'; -import { Command } from '../lib/command'; -import { FatalException } from '../lib/errors'; - -export class StateCommand extends Command { - async getMetadata(): Promise { - return { - name: 'state', - type: 'global', - summary: '', - groups: [MetadataGroup.HIDDEN], - }; - } - - async run(): Promise { - const data = [ - [`${indent(4)}${input('ionic cordova platform save')}`, `save existing installed platforms to ${strong('config.xml')}`], - [`${indent(4)}${input('ionic cordova plugin save')}`, `save existing installed plugins to ${strong('config.xml')}`], - [`${indent(4)}${input('ionic cordova platform --help')}`, `view help page for managing Cordova platforms`], - [`${indent(4)}${input('ionic cordova plugin --help')}`, `view help page for managing Cordova plugins`], - [`${indent(4)}${input('ionic cordova prepare')}`, `install platforms and plugins listed in ${strong('config.xml')}`], - ]; - - throw new FatalException( - `${input('ionic state')} has been removed.\n\n` + - `We recommend using Cordova directly to manage Cordova plugins and platforms.\n` + - `The following commands fulfill the old ${input('ionic state')} functionality:\n\n` + - `${columnar(data, COLUMNAR_OPTIONS)}\n\n` + - `See ${strong('https://cordova.apache.org/docs/en/latest/platform_plugin_versioning_ref/')} for detailed information.\n` - ); - } -} diff --git a/packages/@ionic/cli/src/commands/telemetry.ts b/packages/@ionic/cli/src/commands/telemetry.ts deleted file mode 100644 index 7feae60b26..0000000000 --- a/packages/@ionic/cli/src/commands/telemetry.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { MetadataGroup } from '@ionic/cli-framework'; - -import { CommandLineInputs, CommandLineOptions, CommandMetadata } from '../definitions'; -import { input } from '../lib/color'; -import { Command } from '../lib/command'; -import { FatalException } from '../lib/errors'; - -export class TelemetryCommand extends Command { - async getMetadata(): Promise { - return { - name: 'telemetry', - type: 'global', - summary: 'Opt in and out of telemetry', - groups: [MetadataGroup.HIDDEN], - inputs: [ - { - name: 'status', - summary: `${input('on')} or ${input('off')}`, - }, - ], - }; - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - throw new FatalException( - `${input('ionic telemetry')} has been removed.\n` + - `Please use ${input('ionic config')} directly. Examples:\n\n` + - ` ${input('ionic config get -g telemetry')}\n` + - ` ${input('ionic config set -g telemetry true')}\n` + - ` ${input('ionic config set -g telemetry false')}` - ); - } -} diff --git a/packages/@ionic/cli/src/commands/version.ts b/packages/@ionic/cli/src/commands/version.ts deleted file mode 100644 index 268dc60a97..0000000000 --- a/packages/@ionic/cli/src/commands/version.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { MetadataGroup } from '@ionic/cli-framework'; - -import { CommandLineInputs, CommandLineOptions, CommandMetadata } from '../definitions'; -import { Command } from '../lib/command'; - -export class VersionCommand extends Command { - async getMetadata(): Promise { - return { - name: 'version', - type: 'global', - summary: 'Returns the current CLI version', - groups: [MetadataGroup.HIDDEN], - }; - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - // can't use logger--see https://github.com/ionic-team/ionic-cli/issues/2507 - process.stdout.write(this.env.ctx.version + '\n'); - } -} diff --git a/packages/@ionic/cli/src/constants.ts b/packages/@ionic/cli/src/constants.ts deleted file mode 100644 index 1d31eb7bed..0000000000 --- a/packages/@ionic/cli/src/constants.ts +++ /dev/null @@ -1,15 +0,0 @@ -import chalk from 'chalk'; -import * as lodash from 'lodash'; -import * as path from 'path'; - -import { ProjectType } from './definitions'; - -export const ASSETS_DIRECTORY = path.resolve(__dirname, 'assets'); -export const ANGULAR_STANDALONE = 'angular-standalone'; - -export const PROJECT_FILE = process.env['IONIC_CONFIG_FILE'] ?? 'ionic.config.json'; -export const PROJECT_TYPES: ProjectType[] = ['angular', ANGULAR_STANDALONE, 'react', 'vue', 'custom', 'vue-vite', 'react-vite']; -export const LEGACY_PROJECT_TYPES: ProjectType[] = []; -export const MODERN_PROJECT_TYPES: ProjectType[] = lodash.difference(PROJECT_TYPES, LEGACY_PROJECT_TYPES); - -export const COLUMNAR_OPTIONS = { hsep: chalk.dim('-'), vsep: chalk.dim('|') }; diff --git a/packages/@ionic/cli/src/definitions.ts b/packages/@ionic/cli/src/definitions.ts deleted file mode 100644 index 4f17d688e9..0000000000 --- a/packages/@ionic/cli/src/definitions.ts +++ /dev/null @@ -1,839 +0,0 @@ -import { - BaseConfig, - CommandInstanceInfo as FrameworkCommandInstanceInfo, - CommandLineInputs, - CommandLineOptions, - CommandMetadata as FrameworkCommandMetadata, - CommandMetadataInput as FrameworkCommandMetadataInput, - CommandMetadataOption as FrameworkCommandMetadataOption, - HydratedCommandMetadata as FrameworkHydratedCommandMetadata, - ICommand as FrameworkCommand, - INamespace as FrameworkNamespace, - NamespaceLocateResult as FrameworkNamespaceLocateResult, - PackageJson, -} from '@ionic/cli-framework'; -import { Logger } from '@ionic/cli-framework-output'; -import { PromptModule } from '@ionic/cli-framework-prompts'; -import { NetworkInterface } from '@ionic/utils-network'; -import { Subprocess, SubprocessOptions, WhichOptions } from '@ionic/utils-subprocess'; -import { ChildProcess, SpawnOptions } from 'child_process'; -import * as fs from 'fs'; - -import type { Integration as CapacitorIntegration } from './lib/integrations/capacitor'; -import type { CapacitorConfig } from './lib/integrations/capacitor/config'; -import type { Integration as CordovaIntegration } from './lib/integrations/cordova'; -import type { Integration as EnterpriseIntegration } from './lib/integrations/enterprise'; - -export { - CommandLineInputs, - CommandLineOptions, - NamespaceMetadata, -} from '@ionic/cli-framework'; - -export type SuperAgentRequest = import('superagent').SuperAgentRequest; - -export interface SuperAgentError extends Error { - response: import('superagent').Response; -} - -export type LogFn = (msg: string) => void; - -export interface ILogger extends Logger { - ok: LogFn; - rawmsg: LogFn; -} - -export interface StarterManifest { - name: string; - baseref: string; - welcome?: string; -} - -export interface CordovaPackageJson extends PackageJson { - cordova: { - platforms: string[]; - plugins: { - [key: string]: unknown; - }; - }; -} - -export interface LegacyAndroidBuildOutputEntry { - outputType: { - type: string; - }; - path: string; -} - -export interface AndroidBuildOutput { - artifactType: { - type: string; - }; - elements: { - outputFile: string; - }[]; -} - -export interface Runner { - run(options: T): Promise; -} - -export type ProjectType = 'angular' | 'angular-standalone' | 'custom' | 'bare' | 'react' | 'vue' | 'react-vite' | 'vue-vite'; -export type HookName = 'build:before' | 'build:after' | 'serve:before' | 'serve:after' | 'capacitor:run:before' | 'capacitor:build:before' | 'capacitor:sync:after'; - -export type CapacitorRunHookName = 'capacitor:run:before'; -export type CapacitorBuildHookName = 'capacitor:build:before'; -export type CapacitorSyncHookName = 'capacitor:sync:after'; - -export interface BaseHookContext { - project: { - type: ProjectType; - dir: string; - srcDir: string; - }; - argv: string[]; - env: NodeJS.ProcessEnv; -} - -export type AnyServeOptions = ReactServeOptions | AngularServeOptions; -export type AnyBuildOptions = ReactBuildOptions | AngularBuildOptions; - -export interface CapacitorSyncHookInput { - readonly name: CapacitorSyncHookName; - readonly build?: AnyBuildOptions; - readonly capacitor: IonicCapacitorOptions; -} -export interface CapacitorRunHookInput { - readonly name: CapacitorRunHookName; - readonly serve?: AnyServeOptions; - readonly build?: AnyBuildOptions; - readonly capacitor: IonicCapacitorOptions; -} - -export interface CapacitorBuildHookInput { - readonly name: CapacitorBuildHookName; - readonly build: AnyBuildOptions; - readonly capacitor: IonicCapacitorOptions; -} - -export interface BuildHookInput { - readonly name: 'build:before' | 'build:after'; - readonly build: AngularBuildOptions; -} - -export interface ServeBeforeHookInput { - readonly name: 'serve:before'; - readonly serve: AngularServeOptions; -} - -export interface ServeAfterHookInput { - readonly name: 'serve:after'; - readonly serve: (AngularServeOptions) & ServeDetails; -} - -export type HookInput = BuildHookInput | ServeBeforeHookInput | ServeAfterHookInput | CapacitorRunHookInput | CapacitorBuildHookInput | CapacitorSyncHookInput; -export type HookContext = BaseHookContext & HookInput; - -export type HookFn = (ctx: HookContext) => Promise; - -export type IntegrationName = 'capacitor' | 'cordova' | 'enterprise'; - -export interface ProjectIntegration { - enabled?: boolean; - root?: string; -} - -export interface EnterpriseProjectIntegration extends ProjectIntegration { - productKey?: string; - registries?: string[]; - appId?: string; - orgId?: string; - keyId?: number; -} - -export interface ProjectIntegrations { - cordova?: ProjectIntegration; - capacitor?: ProjectIntegration; - enterprise?: EnterpriseProjectIntegration; -} - -export interface Response extends APIResponseSuccess { - data: T; -} - -export interface ResourceClientLoad { - load(id: string | number, modifiers: ResourceClientRequestModifiers): Promise; -} - -export interface ResourceClientDelete { - delete(id: string | number): Promise; -} - -export interface ResourceClientCreate { - create(details: U): Promise; -} - -export interface ResourceClientPaginate { - paginate(args?: Partial>>): IPaginator, PaginatorState>; -} - -export interface ResourceClientRequestModifiers { - fields?: string[]; -} - -export interface Org { - name: string; -} - -export interface GithubRepo { - full_name: string; - id: number; -} - -export interface GithubBranch { - name: string; -} - -export interface AppAssociation { - repository: RepoAssociation; -} - -export interface RepoAssociationBase { - html_url: string; - clone_url: string; - full_name: string; -} - -export interface GithubRepoAssociation extends RepoAssociationBase { - type: 'github'; - id: number; -} - -export interface GitlabRepoAssociation extends RepoAssociationBase { - type: 'gitlab'; - id: number; -} - -export interface GitlabEnterpriseRepoAssociation extends RepoAssociationBase { - type: 'gitlab_enterprise'; - id: number; -} - -export interface BitbucketCloudRepoAssociation extends RepoAssociationBase { - type: 'bitbucket_cloud'; - id: string; -} - -export interface BitbucketServerRepoAssociation extends RepoAssociationBase { - type: 'bitbucket_server'; - id: number; -} - -export interface AzureDevopsRepoAssociation extends RepoAssociationBase { - type: 'azure_devops'; - id: string; -} - -export type RepoAssociation = GithubRepoAssociation | BitbucketCloudRepoAssociation | BitbucketServerRepoAssociation; - -export type AssociationType = 'github' | 'bitbucket_cloud' | 'bitbucket_server'; - -export interface App { - id: string; - name: string; - slug: string; - org: null | Org; - repo_url?: string; - association?: null | AppAssociation; -} - -export interface Login { - user: User; - token: string; -} - -export interface User { - id: number; - email: string; - oauth_identities?: OAuthIdentity; -} - -export type OAuthIdentity = { - [A in AssociationType]?: OAuthIdentityDetails; -}; - -export interface OAuthIdentityDetails { - username: string; - name: string; - html_url: string; -} - -export interface OAuthServerConfig { - authorizationUrl: string; - tokenUrl: string; - clientId: string; - apiAudience: string; -} - -export interface OpenIdToken { - access_token: string; - expires_in: number; - id_token?: string; - refresh_token?: string; - scope: 'openid profile email offline_access'; - token_type: 'Bearer'; - state?: string; -} - -export interface Snapshot { - id: string; - sha: string; - ref: string; - state: string; - created: string; - note: string; -} - -export interface SSHKey { - id: string; - pubkey: string; - fingerprint: string; - annotation: string; - name: string; - created: string; - updated: string; -} - -export interface SecurityProfile { - name: string; - tag: string; - type: 'development' | 'production'; - created: string; - credentials: { - android?: object; - ios?: object; - }; -} - -export interface IConfig extends BaseConfig { - getAPIUrl(): string; - getDashUrl(): string; - getGitHost(): string; - getGitPort(): number; - getHTTPConfig(): CreateRequestOptions; - getOpenIDOAuthConfig(): OAuthServerConfig; -} - -export interface ProjectPersonalizationDetails { - name: string; - projectId: string; - packageId?: string; - version?: string; - description?: string; - themeColor?: string; - appIcon?: Buffer; - splash?: Buffer; -} - -export interface IProjectConfig { - name: string; - type?: ProjectType; - id?: string; - root?: string; - - readonly integrations: ProjectIntegrations; - readonly hooks?: Record; -} - -export interface IMultiProjectConfig { - defaultProject?: string; - projects: { - [key: string]: IProjectConfig | undefined; - }; -} - -export type ProjectFile = IProjectConfig | IMultiProjectConfig; - -export interface IProject { - readonly rootDirectory: string; - readonly directory: string; - readonly filePath: string; - readonly pathPrefix: readonly string[]; - readonly type: ProjectType; - readonly config: BaseConfig; - readonly details: import('./lib/project').ProjectDetailsResult; - - getSourceDir(sourceRoot?: string): Promise; - getDefaultDistDir(): Promise; - getDistDir(): Promise; - getInfo(): Promise; - detected(): Promise; - createIntegration(name: 'capacitor'): Promise; - createIntegration(name: 'cordova'): Promise; - createIntegration(name: 'enterprise'): Promise; - createIntegration(name: IntegrationName): Promise>; - getIntegration(name: IntegrationName): Required | undefined; - requireIntegration(name: IntegrationName): Required; - requireAppflowId(): Promise; - getPackageJson(pkgName?: string, options?: { logErrors?: boolean }): Promise<[PackageJson | undefined, string | undefined]>; - requirePackageJson(pkgName?: string): Promise; - personalize(details: ProjectPersonalizationDetails): Promise; - getBuildRunner(): Promise | undefined>; - getServeRunner(): Promise | undefined>; - getGenerateRunner(): Promise | undefined>; - requireBuildRunner(): Promise>; - requireServeRunner(): Promise>; - requireGenerateRunner(): Promise>; -} - -export interface IntegrationAddDetails { - quiet?: boolean; - root: string; - enableArgs?: string[]; -} - -export interface IntegrationAddHandlers { - conflictHandler?: (f: string, stats: fs.Stats) => Promise; - onFileCreate?: (f: string) => void; -} - -export interface IIntegration { - readonly name: IntegrationName; - readonly summary: string; - readonly archiveUrl?: string; - readonly config: BaseConfig; - - add(details: IntegrationAddDetails): Promise; - isAdded(): boolean; - enable(config?: T): Promise; - isEnabled(): boolean; - disable(): Promise; - getInfo(): Promise; - personalize(details: ProjectPersonalizationDetails): Promise; -} - -export interface PackageVersions { - [key: string]: string; -} - -export interface CommandMetadataInput extends FrameworkCommandMetadataInput { - private?: boolean; -} - -export interface CommandMetadataOption extends FrameworkCommandMetadataOption { - private?: boolean; - hint?: string; -} - -export interface ExitCodeException extends Error { - exitCode: number; -} - -export interface CommandMetadata extends FrameworkCommandMetadata { - type: 'global' | 'project'; -} - -export type HydratedCommandMetadata = CommandMetadata & FrameworkHydratedCommandMetadata; -export type CommandInstanceInfo = FrameworkCommandInstanceInfo; -export type NamespaceLocateResult = FrameworkNamespaceLocateResult; - -export interface ISession { - login(email: string, password: string): Promise; - ssoLogin(email: string): Promise; - tokenLogin(token: string): Promise; - webLogin(): Promise; - wizardLogin(): Promise; - logout(): Promise; - - isLoggedIn(): boolean; - getUser(): { id: number; }; - getUserToken(): Promise; -} - -export interface IShellSpawnOptions extends SpawnOptions { - showCommand?: boolean; -} - -export interface IShellOutputOptions extends IShellSpawnOptions { - fatalOnError?: boolean; - fatalOnNotFound?: boolean; - showError?: boolean; -} - -export interface IShellRunOptions extends IShellOutputOptions { - stream?: NodeJS.WritableStream; - killOnExit?: boolean; - truncateErrorOutput?: number; -} - -export interface IShell { - alterPath: (path: string) => string; - - run(command: string, args: readonly string[], options: IShellRunOptions): Promise; - output(command: string, args: readonly string[], options: IShellOutputOptions): Promise; - spawn(command: string, args: readonly string[], options: IShellSpawnOptions): Promise; - cmdinfo(cmd: string, args?: readonly string[], options?: SubprocessOptions): Promise; - which(command: string, options?: WhichOptions): Promise; - createSubprocess(command: string, args: readonly string[], options?: SubprocessOptions): Promise; -} - -export interface ITelemetry { - sendCommand(command: string, args: string[]): Promise; -} - -export type NpmClient = 'yarn' | 'npm' | 'pnpm'; - -export type FeatureId = 'ssl-commands'; - -export interface ConfigFile { - 'version': string; - 'telemetry': boolean; - 'npmClient': NpmClient; - 'interactive'?: boolean; - - // HTTP configuration - 'proxy'?: string; - 'ssl.cafile'?: string | string[]; - 'ssl.certfile'?: string | string[]; - 'ssl.keyfile'?: string | string[]; - - // Ionic API - 'urls.api'?: string; - 'urls.dash'?: string; - 'git.host'?: string; - 'git.port'?: number; - 'git.setup'?: boolean; - 'org.id'?: string; - 'user.id'?: number; - 'user.email'?: string; - 'tokens.user'?: string; - 'tokens.telemetry'?: string; - 'tokens.refresh'?: string; - 'tokens.issuedOn'?: string; - 'tokens.expiresInSeconds'?: number; - 'tokens.flowName'?: string; - - // oauth configs - 'oauth.openid.authorization_url'?: string; - 'oauth.openid.token_url'?: string; - 'oauth.openid.client_id'?: string; - 'oauth.openid.api_audience'?: string; - - // Features - 'features.ssl-commands'?: boolean; -} - -export interface SSLConfig { - cafile?: string | string[]; - certfile?: string | string[]; - keyfile?: string | string[]; -} - -export interface CreateRequestOptions { - userAgent: string; - ssl?: SSLConfig; - proxy?: string; -} - -export type APIResponse = APIResponseSuccess | APIResponseError; - -export interface APIResponseMeta { - status: number; - version: string; - request_id: string; -} - -export type APIResponseData = object | object[] | string; - -export interface APIResponseErrorDetails { - error_type: string; - parameter: string; - errors: string[]; -} - -export interface APIResponseError { - error: APIResponseErrorError; - meta: APIResponseMeta; -} - -export interface APIResponseErrorError { - message: string; - link: string | null; - type: string; - details?: APIResponseErrorDetails[]; -} - -export interface APIResponseSuccess { - data: APIResponseData; - meta: APIResponseMeta; -} - -export interface APIResponsePageTokenMeta extends APIResponseMeta { - prev_page_token?: string; - next_page_token?: string; -} - -export type HttpMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE' | 'PURGE' | 'HEAD' | 'OPTIONS'; - -export const enum ContentType { - JSON = 'application/json', - FORM_URLENCODED = 'application/x-www-form-urlencoded', - HTML = 'text/html', -} - -export interface IClient { - config: IConfig; - - make(method: HttpMethod, path: string, contentType?: ContentType): Promise<{ req: SuperAgentRequest; }>; - do(req: SuperAgentRequest): Promise; - paginate>(args: PaginateArgs): IPaginator; -} - -export type PaginateArgs> = Pick, 'reqgen' | 'guard' | 'state' | 'max'>; - -export interface IPaginator, S = PaginatorState> extends IterableIterator> { - readonly state: S; -} - -export type PaginatorRequestGenerator = () => Promise<{ req: SuperAgentRequest; }>; -export type PaginatorGuard> = (res: APIResponseSuccess) => res is T; - -export interface PaginatorState { - done: boolean; - loaded: number; -} - -export interface PagePaginatorState extends PaginatorState { - page: number; - page_size?: number; -} - -export interface TokenPaginatorState extends PaginatorState { - page_token?: string; -} - -export interface PaginatorDeps, S = PaginatorState> { - readonly client: IClient; - readonly reqgen: PaginatorRequestGenerator; - readonly guard: PaginatorGuard; - readonly state?: Partial; - readonly max?: number; -} - -export type InfoItemGroup = 'ionic' | 'capacitor' | 'cordova' | 'utility' | 'system' | 'environment'; - -export interface InfoItem { - group: InfoItemGroup; - name: string; - value: string; - key?: string; - flair?: string; - path?: string; - hidden?: boolean; -} - -export interface BaseBuildOptions { - engine: string; // browser, cordova, etc. - platform?: string; // android, ios, etc. - project?: string; - verbose?: boolean; - '--': string[]; -} - -export interface BuildOptions extends BaseBuildOptions { - type: T; -} - -export interface AngularBuildOptions extends BuildOptions<'angular'> { - /** - * The Angular architect configuration to use for builds. - * - * The `--prod` command line flag is a shortcut which translates to the - * 'production' configuration. - */ - configuration?: string; - sourcemaps?: boolean; - cordovaAssets?: boolean; - watch?: boolean; -} - -export interface ReactBuildOptions extends BuildOptions<'react'> { - publicUrl?: string; - ci?: boolean; - sourceMap?: boolean; - inlineRuntimeChunk?: boolean; -} - -export interface VueBuildOptions extends BuildOptions<'vue'> { - configuration?: string; - sourcemaps?: boolean; -} - -export interface IonicCapacitorOptions extends CapacitorConfig { - '--': string[]; - verbose?: boolean; -} - -export interface CustomBuildOptions extends BuildOptions<'custom'> {} - -export interface GenerateOptions { - name: string; -} - -export interface AngularGenerateOptions extends GenerateOptions { - [key: string]: any; // TODO - schematic: string; -} - -export interface IonicAngularGenerateOptions extends GenerateOptions { - type: string; - module: boolean; - constants: boolean; -} - -export interface ServeOptions { - // Command Options - host: string; - port: number; - publicHost?: string; - livereload: boolean; - proxy: boolean; - open: boolean; - browser?: string; - browserOption?: string; - platform?: string; // android, ios, etc. - project?: string; - verbose?: boolean; - '--': string[]; - - // Additional Options - externalAddressRequired?: boolean; - engine: string; // browser, cordova, etc. -} - -export interface AngularServeOptions extends ServeOptions { - consolelogs?: boolean; - consolelogsPort?: number; - ssl?: boolean; - configuration?: string; - sourcemaps?: boolean; -} - -export interface ReactServeOptions extends ServeOptions { - https?: boolean; - ci?: boolean; - reactEditor?: string; -} - -export interface VueServeOptions extends ServeOptions { - https: boolean; - mode: string; - configuration?: string; - sourcemaps?: boolean; -} - -export interface CustomServeOptions extends ServeOptions {} - -export interface LabServeDetails { - projectType: ProjectType; - host: string; - port: number; -} - -export interface ServeDetails { - custom: boolean; - protocol: string; - localAddress: string; - externalAddress: string; - port: number; - externalNetworkInterfaces: NetworkInterface[]; - externallyAccessible: boolean; -} - -export interface IonicContext { - readonly binPath: string; - readonly libPath: string; - readonly execPath: string; - readonly version: string; -} - -export interface IonicEnvironment { - readonly flags: IonicEnvironmentFlags; - readonly client: IClient; - readonly config: IConfig; // CLI global config (~/.ionic/config.json) - readonly log: ILogger; - readonly prompt: PromptModule; - readonly ctx: IonicContext; - readonly session: ISession; - readonly shell: IShell; - - getInfo(): Promise; -} - -export interface IonicEnvironmentFlags { - readonly interactive: boolean; - readonly confirm: boolean; -} - -export type DistTag = 'testing' | 'canary' | 'latest'; - -export interface ICommand extends FrameworkCommand { - readonly env: IonicEnvironment; - readonly project?: IProject; - - execute(inputs: CommandLineInputs, options: CommandLineOptions, metadata: CommandInstanceInfo): Promise; -} - -export interface CommandPreRun extends ICommand { - preRun(inputs: CommandLineInputs, options: CommandLineOptions, metadata: CommandInstanceInfo): Promise; -} - -export interface INamespace extends FrameworkNamespace { - env: IonicEnvironment; - project?: IProject; -} - -export interface StarterList { - starters: { - name: string; - id: string; - type: ProjectType; - }[]; - integrations: { - name: IntegrationName; - id: string; - }[]; -} - -export interface BaseStarterTemplate { - name: string; - projectType: ProjectType; - description?: string; -} - -export interface RepoStarterTemplate extends BaseStarterTemplate { - type: 'repo'; - repo: string; -} - -export interface ManagedStarterTemplate extends BaseStarterTemplate { - type: 'managed'; - id: string; -} - -export type StarterTemplate = RepoStarterTemplate | ManagedStarterTemplate; - -export interface ResolvedStarterTemplate extends BaseStarterTemplate { - archive: string; -} - -export interface TelemetryIPCMessage { - type: 'telemetry'; - data: { command: string; args: string[]; }; -} - -export interface UpdateCheckIPCMessage { - type: 'update-check'; -} - -export type IPCMessage = TelemetryIPCMessage | UpdateCheckIPCMessage; diff --git a/packages/@ionic/cli/src/guards.ts b/packages/@ionic/cli/src/guards.ts deleted file mode 100644 index 15948b4c4f..0000000000 --- a/packages/@ionic/cli/src/guards.ts +++ /dev/null @@ -1,358 +0,0 @@ -import { - APIResponse, - APIResponseError, - APIResponseSuccess, - AndroidBuildOutput, - App, - AppAssociation, - AzureDevopsRepoAssociation, - BitbucketCloudRepoAssociation, - BitbucketServerRepoAssociation, - CommandPreRun, - CordovaPackageJson, - ExitCodeException, - GithubBranch, - GithubRepo, - GithubRepoAssociation, - GitlabEnterpriseRepoAssociation, - GitlabRepoAssociation, - ICommand, - IMultiProjectConfig, - IProjectConfig, - IntegrationName, - LegacyAndroidBuildOutputEntry, - Login, - OpenIdToken, - Org, - Response, - SSHKey, - SecurityProfile, - Snapshot, - StarterManifest, - SuperAgentError, - User -} from './definitions'; -import { AuthConnection } from './lib/oauth/auth'; - -export const INTEGRATION_NAMES: IntegrationName[] = ['capacitor', 'cordova', 'enterprise']; - -export function isCommand(cmd: any): cmd is ICommand { - return cmd && typeof cmd.run === 'function'; -} - -export function isCommandPreRun(cmd: any): cmd is CommandPreRun { - return cmd && typeof cmd.preRun === 'function'; -} - -export function isStarterManifest(obj: any): obj is StarterManifest { - return obj && - typeof obj.name === 'string' && - typeof obj.baseref === 'string'; -} - -export function isCordovaPackageJson(obj: any): obj is CordovaPackageJson { - return obj && - typeof obj.name === 'string' && - typeof obj.cordova === 'object' && - Array.isArray(obj.cordova.platforms) && - typeof obj.cordova.plugins === 'object'; -} - -export function isLegacyAndroidBuildOutputFile(obj: any): obj is LegacyAndroidBuildOutputEntry[] { - if (!Array.isArray(obj)) { - return false; - } - - if (obj.length === 0) { - return true; - } - - return obj[0] - && typeof obj[0].path === 'string' - && typeof obj[0].outputType === 'object' - && typeof obj[0].outputType.type === 'string'; -} - -export function isAndroidBuildOutputFile(obj: any): obj is AndroidBuildOutput { - return obj && - typeof obj.artifactType === 'object' && - typeof obj.artifactType.type === 'string' && - Array.isArray(obj.elements); -} - -export function isExitCodeException(err: any): err is ExitCodeException { - return err && typeof err.exitCode === 'number' && err.exitCode >= 0 && err.exitCode <= 255; -} - -export function isSuperAgentError(err: any): err is SuperAgentError { - return err && err.response && typeof err.response === 'object'; -} - -export function isAPIResponseSuccess(res: any): res is APIResponseSuccess { - return res && (typeof res.data === 'object' || typeof res.data === 'string'); -} - -export function isAPIResponseError(res: any): res is APIResponseError { - return res && typeof res.error === 'object'; -} - -export function isOrg(org: any): org is Org { - return org && typeof org.name === 'string'; -} - -export function isGithubRepo(repo: any): repo is GithubRepo { - return repo - && typeof repo.full_name === 'string' - && typeof repo.id === 'number'; -} - -export function isGithubBranch(branch: any): branch is GithubBranch { - return branch && typeof branch.name === 'string'; -} - -export function isGithubRepoListResponse(res: any): res is Response { - if (!isAPIResponseSuccess(res) || !Array.isArray(res.data)) { - return false; - } - - if (res.data.length === 0) { - return true; - } - - return isGithubRepo(res.data[0]); -} - -export function isGithubBranchListResponse(res: any): res is Response { - if (!isAPIResponseSuccess(res) || !Array.isArray(res.data)) { - return false; - } - - if (res.data.length === 0) { - return true; - } - - return isGithubBranch(res.data[0]); -} - -export function isAppAssociation(association: any): association is AppAssociation { - return ( - association && - typeof association.repository === 'object' && - typeof association.repository.html_url === 'string' && - ( - isGithubRepoAssociation(association.repository) || - isBitbucketCloudRepoAssociation(association.repository) || - isBitbucketServerRepoAssociation(association.repository) || - isGitlabRepoAssociation(association.repository) || - isGitlabEnterpriseRepoAssociation(association.repository) || - isAzureDevopsRepoAssociation(association.repository) - ) - ); -} - -export function isAppAssociationResponse(res: APIResponse): res is Response { - return isAPIResponseSuccess(res) - && typeof res.data === 'object' - && isAppAssociation(res.data); -} - -export function isGithubRepoAssociation(association: any): association is GithubRepoAssociation { - return association - && association.type === 'github' - && typeof association.id === 'number'; -} - -export function isGitlabRepoAssociation(association: any): association is GitlabRepoAssociation { - return association - && association.type === 'gitlab' - && typeof association.id === 'number'; -} - -export function isGitlabEnterpriseRepoAssociation(association: any): association is GitlabEnterpriseRepoAssociation { - return association - && association.type === 'gitlab_enterprise' - && typeof association.id === 'number'; -} - -export function isBitbucketCloudRepoAssociation(association: any): association is BitbucketCloudRepoAssociation { - return association - && association.type === 'bitbucket_cloud' - && typeof association.id === 'string'; -} - -export function isBitbucketServerRepoAssociation(association: any): association is BitbucketServerRepoAssociation { - return association - && association.type === 'bitbucket_server' - && typeof association.id === 'number'; -} - -export function isAzureDevopsRepoAssociation(association: any): association is AzureDevopsRepoAssociation { - return association - && association.type === 'azure_devops' - && typeof association.id === 'string'; -} - -export function isApp(app: any): app is App { - return app - && typeof app === 'object' - && typeof app.id === 'string' - && typeof app.name === 'string' - && typeof app.slug === 'string' - && (!app.org || isOrg(app.org)) - && (!app.association || isAppAssociation(app.association)); -} - -export function isAppResponse(res: APIResponse): res is Response { - return isAPIResponseSuccess(res) - && typeof res.data === 'object' - && isApp(res.data); -} - -export function isAppsResponse(res: APIResponse): res is Response { - if (!isAPIResponseSuccess(res) || !Array.isArray(res.data)) { - return false; - } - - if (res.data.length === 0) { - return true; - } - - return isApp(res.data[0]); -} - -export interface OAuthLogin { - redirect_url: string; -} - -export function isOAuthLogin(login: any): login is OAuthLogin { - return login && typeof login.redirect_url === 'string'; -} - -export function isOAuthLoginResponse(res: any): res is Response { - return isAPIResponseSuccess(res) && isOAuthLogin(res.data); -} - -export function isOpenIDToken(tokenObj: any): tokenObj is OpenIdToken { - return tokenObj - && typeof tokenObj.access_token === 'string' - && typeof tokenObj.expires_in === 'number' - && (tokenObj.id_token ? typeof tokenObj.id_token === 'string' : true) - && (tokenObj.refresh_token ? typeof tokenObj.refresh_token === 'string' : true) - && tokenObj.scope === 'openid profile email offline_access' - && tokenObj.token_type === 'Bearer'; -} - -export function isOpenIDTokenExchangeResponse(res: any): res is Response { - return res && typeof res.body === 'object' && isOpenIDToken(res.body); -} - -export function isSnapshot(snapshot: any): snapshot is Snapshot { - return snapshot - && typeof snapshot.id === 'string' - && typeof snapshot.sha === 'string' - && typeof snapshot.ref === 'string' - && typeof snapshot.state === 'string' - && typeof snapshot.created === 'string' - && typeof snapshot.note === 'string'; -} - -export function isSnapshotResponse(res: APIResponse): res is Response { - return isAPIResponseSuccess(res) && isSnapshot(res.data); -} - -export function isSnapshotListResponse(res: APIResponse): res is Response { - if (!isAPIResponseSuccess(res) || !Array.isArray(res.data)) { - return false; - } - - if (res.data.length === 0) { - return true; - } - - return isSnapshot(res.data[0]); -} - -export function isLogin(login: any): login is Login { - return login - && isUser(login.user) - && typeof login.token === 'string'; -} - -export function isLoginResponse(res: APIResponse): res is Response { - return isAPIResponseSuccess(res) && isLogin(res.data); -} - -export function isAuthConnection(connection: any): connection is AuthConnection { - return connection && typeof connection.uuid === 'string'; -} - -export function isAuthConnectionResponse(res: APIResponse): res is Response { - return isAPIResponseSuccess(res) && isAuthConnection(res.data); -} - -export function isUser(user: any): user is User { - return user - && typeof user.id === 'number' - && typeof user.email === 'string'; -} - -export function isUserResponse(res: APIResponse): res is Response { - return isAPIResponseSuccess(res) && isUser(res.data); -} - -export function isSSHKey(key: any): key is SSHKey { - return key - && typeof key.id === 'string' - && typeof key.pubkey === 'string' - && typeof key.fingerprint === 'string' - && typeof key.annotation === 'string' - && typeof key.name === 'string' - && typeof key.created === 'string' - && typeof key.updated === 'string'; -} - -export function isSSHKeyListResponse(res: APIResponse): res is Response { - if (!isAPIResponseSuccess(res) || !Array.isArray(res.data)) { - return false; - } - - if (res.data.length === 0) { - return true; - } - - return isSSHKey(res.data[0]); -} - -export function isSSHKeyResponse(res: APIResponse): res is Response { - return isAPIResponseSuccess(res) && isSSHKey(res.data); -} - -export function isSecurityProfile(obj: any): obj is SecurityProfile { - return obj - && typeof obj.name === 'string' - && typeof obj.tag === 'string' - && typeof obj.type === 'string' - && typeof obj.created === 'string' - && typeof obj.credentials === 'object'; -} - -export function isSecurityProfileResponse(r: APIResponse): r is Response { - const res = r as Response; - return isAPIResponseSuccess(res) && isSecurityProfile(res.data); -} - -export function isIntegrationName(name: any): name is IntegrationName { - return INTEGRATION_NAMES.includes(name); -} - -export function isProjectConfig(configFile: any): configFile is IProjectConfig { - return configFile - && typeof configFile.name === 'string' - && typeof configFile.projects === 'undefined'; -} - -export function isMultiProjectConfig(configFile: any): configFile is IMultiProjectConfig { - return configFile - && typeof configFile.name === 'undefined' - && typeof configFile.projects === 'object'; -} diff --git a/packages/@ionic/cli/src/index.ts b/packages/@ionic/cli/src/index.ts deleted file mode 100644 index 949d9057fc..0000000000 --- a/packages/@ionic/cli/src/index.ts +++ /dev/null @@ -1,202 +0,0 @@ -import { BaseError, InputValidationError, PackageJson } from '@ionic/cli-framework'; -import { readPackageJsonFile } from '@ionic/cli-framework/utils/node'; -import { processExit } from '@ionic/utils-process'; -import { debug as Debug } from 'debug'; -import * as path from 'path'; -import * as util from 'util'; - -import { IonicNamespace } from './commands'; -import { IPCMessage, IonicContext, IonicEnvironment } from './definitions'; -import { isExitCodeException, isSuperAgentError } from './guards'; -import { generateIonicEnvironment } from './lib'; -import { failure, input, strong } from './lib/color'; -import { Executor } from './lib/executor'; - -export * from './constants'; -export * from './guards'; -export * from './definitions'; - -const debug = Debug('ionic'); - -const PACKAGE_ROOT_PATH = __dirname; -const PACKAGE_JSON_PATH = path.resolve(PACKAGE_ROOT_PATH, 'package.json'); - -let _pkg: PackageJson | undefined; -let _executor: Executor | undefined; - -async function loadPackageJson(): Promise { - if (!_pkg) { - _pkg = await readPackageJsonFile(PACKAGE_JSON_PATH); - } - - return _pkg; -} - -export async function generateContext(): Promise { - const pkg = await loadPackageJson(); - - if (!pkg.bin || !pkg.bin.ionic) { - throw new Error(`Missing "${strong('bin.ionic')}" in Ionic CLI package.json`); - } - - if (!pkg.main) { - throw new Error(`Missing "${strong('main')}" in Ionic CLI package.json`); - } - - return { - binPath: path.resolve(PACKAGE_ROOT_PATH, pkg.bin.ionic), - libPath: PACKAGE_ROOT_PATH, - execPath: process.cwd(), - version: pkg.version, - }; -} - -export async function loadExecutor(ctx: IonicContext, pargv: string[]): Promise { - if (!_executor) { - const deps = await generateIonicEnvironment(ctx, pargv); - const namespace = new IonicNamespace(deps); - _executor = new Executor({ namespace }); - } - - return _executor; -} - -async function authenticateFromEnvironment(ienv: IonicEnvironment) { - const token = process.env['IONIC_TOKEN']; - const email = process.env['IONIC_EMAIL']; - const password = process.env['IONIC_PASSWORD']; - - if (token) { - const wasLoggedIn = ienv.session.isLoggedIn(); - debug(`${strong('IONIC_TOKEN')} environment variable detected`); - - if (ienv.config.get('tokens.user') !== token) { - debug(`${strong('IONIC_TOKEN')} mismatch with current session--attempting login`); - await ienv.session.tokenLogin(token); - - if (wasLoggedIn) { - ienv.log.info(`You have been logged out--using ${strong('IONIC_TOKEN')} environment variable`); - } - } - } else if (email && password) { - debug(`${strong('IONIC_EMAIL')} / ${strong('IONIC_PASSWORD')} environment variables detected`); - - if (ienv.config.get('user.email') !== email) { - debug(`${strong('IONIC_EMAIL')} mismatch with current session--attempting login`); - - ienv.log.warn( - 'Authenticating using email and password is deprecated. Please generate a Personal Access Token and set the ' + - strong('IONIC_TOKEN') + - ' environment variable.' - ); - ienv.log.nl(); - - try { - await ienv.session.login(email, password); - } catch (e: any) { - ienv.log.error(`Error occurred during automatic login via ${strong('IONIC_EMAIL')} / ${strong('IONIC_PASSWORD')} environment variables.`); - throw e; - } - } - } -} - -export async function run(pargv: string[]): Promise { - let err: any; - let executor: Executor; - - try { - executor = await loadExecutor(await generateContext(), pargv); - } catch (e: any) { - process.stderr.write(`${e.message ? e.message : (e.stack ? e.stack : e)}\n`); - process.exitCode = 1; - return; - } - - const ienv = executor.namespace.env; - - if (pargv[0] === '_') { - return; - } - - try { - debug('Context: %o', ienv.ctx); - - ienv.config.set('version', ienv.ctx.version); - - const location = await executor.locate(pargv); - - const [, [cmd = ''] = []] = location.path; - - if (!['config', 'completion', 'help', 'login', 'logout', 'version'].includes(cmd)) { - await authenticateFromEnvironment(ienv); - } - - await executor.execute(location, process.env); - } catch (e: any) { - err = e; - } finally { - if (ienv.flags.interactive) { - const { runUpdateNotify } = await import('./lib/updates'); - await runUpdateNotify(ienv, await loadPackageJson()); - } - } - - if (err) { - process.exitCode = 1; - - if (err instanceof InputValidationError) { - for (const e of err.errors) { - ienv.log.error(e.message); - } - ienv.log.msg(`Use the ${input('--help')} flag for more details.`); - } else if (isSuperAgentError(err)) { - const { formatSuperAgentError } = await import('./lib/http'); - ienv.log.rawmsg(formatSuperAgentError(err)); - } else if (err.code && err.code === 'ENOTFOUND' || err.code === 'ECONNREFUSED') { - ienv.log.error( - `Network connectivity error occurred, are you offline?\n` + - `If you are behind a firewall and need to configure proxy settings, see: ${strong('https://ion.link/cli-proxy-docs')}\n\n` + - failure(String(err.stack ? err.stack : err)) - ); - } else if (isExitCodeException(err)) { - if (err.message) { - if (err.exitCode > 0) { - ienv.log.error(err.message); - } else { - ienv.log.msg(err.message); - } - } - - await processExit(err.exitCode); - } else if (err instanceof BaseError) { - ienv.log.error(err.message); - } else { - ienv.log.rawmsg(failure(util.inspect(err))); - } - } -} - -export async function receive(msg: IPCMessage) { - if (!_executor) { - throw new Error('Executor not initialized.'); - } - - const { env, project } = _executor.namespace; - - if (msg.type === 'telemetry') { - const { sendCommand } = await import('./lib/telemetry'); - - await sendCommand({ - getInfo: env.getInfo, - client: env.client, - config: env.config, - ctx: env.ctx, - project, - session: env.session, - }, msg.data.command, msg.data.args); - } else if (msg.type === 'update-check') { - const { runUpdateCheck } = await import('./lib/updates'); - await runUpdateCheck(env); - } -} diff --git a/packages/@ionic/cli/src/lib/__tests__/command.ts b/packages/@ionic/cli/src/lib/__tests__/command.ts deleted file mode 100644 index 3bec45d40e..0000000000 --- a/packages/@ionic/cli/src/lib/__tests__/command.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { CommandMetadata, NamespaceMetadata } from '../../definitions'; -import { Command } from '../command'; -import { Namespace } from '../namespace'; - -class MyNamespace extends Namespace { - async getMetadata(): Promise { - return { - name: 'my', - summary: '', - }; - } -} - -class FooCommand extends Command { - async getMetadata(): Promise { - return { - name: 'foo', - type: 'global', - summary: '', - }; - } - - async run() {} -} - -class BarCommand extends Command { - async getMetadata(): Promise { - return { - name: 'bar', - type: 'global', - summary: '', - inputs: [ - { - name: 'arg1', - summary: '', - }, - { - name: 'arg2', - summary: '', - }, - { - name: 'arg3', - summary: '', - private: true, - }, - { - name: 'arg4', - summary: '', - }, - ], - options: [ - { - name: 'opt1', - summary: '', - type: Boolean, - }, - { - name: 'opt2', - summary: '', - }, - { - name: 'opt3', - summary: '', - default: 'default', - }, - { - name: 'opt4', - summary: '', - private: true, - }, - { - name: 'opt5', - summary: '', - aliases: ['o'], - }, - ], - }; - } - - async run() {} -} - -describe('@ionic/cli', () => { - - describe('Command', () => { - - describe('getCleanInputsForTelemetry', () => { - - const myns = new MyNamespace(undefined); - - // TODO: aliases can be intelligently removed - - it('should be empty with no inputs', async () => { - const foo = new FooCommand(myns); - const inputs: string[] = []; - const results = await foo.getCleanInputsForTelemetry(inputs, { _: inputs }); - expect(results).toEqual([]); - }); - - it('should include additional, unknown arguments', async () => { - const foo = new FooCommand(myns); - const inputs = ['a', 'b', 'c']; - const results = await foo.getCleanInputsForTelemetry(inputs, { _: inputs }); - expect(results).toEqual(['a', 'b', 'c']); - }); - - it('should include additional, unknown options', async () => { - const foo = new FooCommand(myns); - const inputs: string[] = []; - const results = await foo.getCleanInputsForTelemetry(inputs, { _: inputs, opt1: true, opt2: 'cow' }); - expect(results).toEqual(['--opt1', '--opt2=cow']); - }); - - it('should include known arguments and options', async () => { - const bar = new BarCommand(myns); - const inputs = ['a', 'b']; - const results = await bar.getCleanInputsForTelemetry(inputs, { _: inputs, opt1: true, opt2: 'cow', opt3: 'not default' }); - expect(results).toEqual(['a', 'b', '--opt1', '--opt2=cow', '--opt3="not default"']); - }); - - it('should exclude options with default values', async () => { - const bar = new BarCommand(myns); - const inputs: string[] = []; - const results = await bar.getCleanInputsForTelemetry(inputs, { _: inputs, opt3: 'default' }); - expect(results).toEqual([]); - }); - - it('should exclude private arguments and options', async () => { - const bar = new BarCommand(myns); - const inputs = ['a', 'b', 'c', 'd']; - const results = await bar.getCleanInputsForTelemetry(inputs, { _: inputs, opt1: true, opt4: 'private!' }); - expect(results).toEqual(['a', 'b', '*****', 'd', '--opt1']); - }); - - it('should exclude aliases', async () => { - const bar = new BarCommand(myns); - const inputs: string[] = []; - const results = await bar.getCleanInputsForTelemetry(inputs, { _: inputs, o: 'wow', opt5: 'wow' }); - expect(results).toEqual(['--opt5=wow']); - }); - - it('should include separated args', async () => { - const bar = new BarCommand(myns); - const inputs = ['a']; - const results = await bar.getCleanInputsForTelemetry(inputs, { _: inputs, '--': ['other', 'args'], opt1: true }); - expect(results).toEqual(['a', '--opt1', '--', 'other', 'args']); - }); - - }); - - }); - -}); diff --git a/packages/@ionic/cli/src/lib/__tests__/executor.ts b/packages/@ionic/cli/src/lib/__tests__/executor.ts deleted file mode 100644 index 370fa38324..0000000000 --- a/packages/@ionic/cli/src/lib/__tests__/executor.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { CommandMetadata } from '../../definitions'; -import { metadataToCmdOptsEnv } from '../executor'; - -describe('@ionic/cli', () => { - - describe('lib/executor', () => { - - describe('metadataToCmdOptsEnv', () => { - - const metadata: CommandMetadata = { - name: '', - summary: '', - type: 'global', - options: [ - { - name: 'baz', - summary: '', - }, - { - name: 'opt-with-dashes', - summary: '', - }, - ], - }; - - it('should return empty array for command with no options', () => { - const result = metadataToCmdOptsEnv({ name: '', summary: '', type: 'global', options: [] }, ['cmd']); - expect(result.size).toEqual(0); - }); - - it('should return schema for options', () => { - const result = metadataToCmdOptsEnv(metadata, ['foo', 'bar']); - const envvars = [...result.values()]; - expect(envvars.length).toEqual(2); - expect(envvars).toContain('IONIC_CMDOPTS_FOO_BAR_BAZ'); - expect(envvars).toContain('IONIC_CMDOPTS_FOO_BAR_OPT_WITH_DASHES'); - }); - - }); - - }); - -}); diff --git a/packages/@ionic/cli/src/lib/__tests__/fixtures/ssh-config/config1 b/packages/@ionic/cli/src/lib/__tests__/fixtures/ssh-config/config1 deleted file mode 100644 index 887ee944da..0000000000 --- a/packages/@ionic/cli/src/lib/__tests__/fixtures/ssh-config/config1 +++ /dev/null @@ -1,3 +0,0 @@ -Host foo - HostName 10.0.0.1 - User dan diff --git a/packages/@ionic/cli/src/lib/__tests__/fixtures/ssh-config/config2 b/packages/@ionic/cli/src/lib/__tests__/fixtures/ssh-config/config2 deleted file mode 100644 index 2812af16f6..0000000000 --- a/packages/@ionic/cli/src/lib/__tests__/fixtures/ssh-config/config2 +++ /dev/null @@ -1,4 +0,0 @@ - -Host foo - HostName 10.0.0.1 - User dan \ No newline at end of file diff --git a/packages/@ionic/cli/src/lib/__tests__/fixtures/ssh-config/config3 b/packages/@ionic/cli/src/lib/__tests__/fixtures/ssh-config/config3 deleted file mode 100644 index d040a6aa5e..0000000000 --- a/packages/@ionic/cli/src/lib/__tests__/fixtures/ssh-config/config3 +++ /dev/null @@ -1,6 +0,0 @@ -Host foo - HostName 10.0.0.1 - User dan - -Host bar - IdentityFile /id_rsa diff --git a/packages/@ionic/cli/src/lib/__tests__/fixtures/ssh-config/config4 b/packages/@ionic/cli/src/lib/__tests__/fixtures/ssh-config/config4 deleted file mode 100644 index da39f05bbb..0000000000 --- a/packages/@ionic/cli/src/lib/__tests__/fixtures/ssh-config/config4 +++ /dev/null @@ -1,6 +0,0 @@ -Host foo - HostName 10.0.0.1 - User dan - -Host bar - IdentityFile /id_rsa_bad diff --git a/packages/@ionic/cli/src/lib/__tests__/fixtures/ssh-config/config5 b/packages/@ionic/cli/src/lib/__tests__/fixtures/ssh-config/config5 deleted file mode 100644 index 56459995d2..0000000000 --- a/packages/@ionic/cli/src/lib/__tests__/fixtures/ssh-config/config5 +++ /dev/null @@ -1,6 +0,0 @@ -Host bar - IdentityFile /id_rsa - -Host foo - HostName 10.0.0.1 - User dan diff --git a/packages/@ionic/cli/src/lib/__tests__/fixtures/ssh-config/config6 b/packages/@ionic/cli/src/lib/__tests__/fixtures/ssh-config/config6 deleted file mode 100644 index 07ff3a5e14..0000000000 --- a/packages/@ionic/cli/src/lib/__tests__/fixtures/ssh-config/config6 +++ /dev/null @@ -1 +0,0 @@ -AddKeysToAgent yes diff --git a/packages/@ionic/cli/src/lib/__tests__/hooks.ts b/packages/@ionic/cli/src/lib/__tests__/hooks.ts deleted file mode 100644 index 28853a5a65..0000000000 --- a/packages/@ionic/cli/src/lib/__tests__/hooks.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { addHook, removeHook, locateHook } from '../hooks'; - -describe('@ionic/cli', () => { - - describe('lib/hooks', () => { - - describe('addHook', () => { - - it('should return array with hook added if given undefined', () => { - const hooks = addHook('/base', undefined, 'hook.js'); - expect(hooks).toEqual(['hook.js']); - }); - - it('should return array with hook added if given a single string', () => { - const hooks = addHook('/base', 'other-hook.js', 'hook.js'); - expect(hooks).toEqual(['other-hook.js', 'hook.js']); - }); - - it('should return array with hook added if given an empty array', () => { - const hooks = addHook('/base', [], 'hook.js'); - expect(hooks).toEqual(['hook.js']); - }); - - it('should return array with hook added if given an array', () => { - const hooks = addHook('/base', ['other-hook.js'], 'hook.js'); - expect(hooks).toEqual(['other-hook.js', 'hook.js']); - }); - - it('should respect relative paths', () => { - const hooks = addHook('/base/dir/wow', undefined, '../path/to/hook.js'); - expect(hooks).toEqual(['../path/to/hook.js']); - }); - - it('should respect absolute paths', () => { - const hooks = addHook('/base', undefined, '/path/to/hook.js'); - expect(hooks).toEqual(['/path/to/hook.js']); - }); - - it('should not add if it already exists', () => { - const hooks = addHook('/base', ['hook.js'], 'hook.js'); - expect(hooks).toEqual(['hook.js']); - }); - - it('should not add if it already exists as an absolute path', () => { - const hooks = addHook('/base', ['hook.js'], '/base/hook.js'); - expect(hooks).toEqual(['hook.js']); - }); - - }); - - describe('removeHook', () => { - - it('should return empty array if given undefined', () => { - const hooks = removeHook('/base', undefined, 'hook.js'); - expect(hooks).toEqual([]); - }); - - it('should return empty array if given empty array', () => { - const hooks = removeHook('/base', [], 'hook.js'); - expect(hooks).toEqual([]); - }); - - it('should return array if given a single string', () => { - const hooks = removeHook('/base', 'other-hook.js', 'hook.js'); - expect(hooks).toEqual(['other-hook.js']); - }); - - it('should return empty array if given same hook', () => { - const hooks = removeHook('/base', 'hook.js', 'hook.js'); - expect(hooks).toEqual([]); - }); - - it('should return array with hook removed if given an array', () => { - const hooks = removeHook('/base', ['other-hook.js', 'hook.js'], 'hook.js'); - expect(hooks).toEqual(['other-hook.js']); - }); - - it('should respect relative paths', () => { - const hooks = removeHook('/base/dir/wow', ['../path/to/hook.js'], '../path/to/hook.js'); - expect(hooks).toEqual([]); - }); - - it('should respect absolute paths', () => { - const hooks = removeHook('/base', ['/path/to/hook.js'], '/path/to/hook.js'); - expect(hooks).toEqual([]); - }); - - it('should not remove anything if it does not exist', () => { - const hooks = removeHook('/base', ['other-hook.js'], 'hook.js'); - expect(hooks).toEqual(['other-hook.js']); - }); - - it('should remove it if it exists as a relative path', () => { - const hooks = removeHook('/base', ['hook.js'], '/base/hook.js'); - expect(hooks).toEqual([]); - }); - - }); - - describe('locateHook', () => { - - it('should not locate hook in empty array', () => { - const i = locateHook('/base', [], 'hook.js'); - expect(i).toEqual(-1); - }); - - it('should not locate hook in array without it', () => { - const i = locateHook('/base', ['other-hook.js'], 'hook.js'); - expect(i).toEqual(-1); - }); - - it('should locate hook at correct index', () => { - const i = locateHook('/base', ['hook.js'], 'hook.js'); - expect(i).toEqual(0); - }); - - it('should locate relative hook at correct index', () => { - const i = locateHook('/base', ['other-hook.js', 'hook.js'], '/base/hook.js'); - expect(i).toEqual(1); - }); - - it('should locate absolute hook at correct index', () => { - const i = locateHook('/base', ['other-hook.js', '/base/hook.js'], 'hook.js'); - expect(i).toEqual(1); - }); - - }); - - }); - -}); diff --git a/packages/@ionic/cli/src/lib/__tests__/serve.ts b/packages/@ionic/cli/src/lib/__tests__/serve.ts deleted file mode 100644 index 48c25bb61c..0000000000 --- a/packages/@ionic/cli/src/lib/__tests__/serve.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { ServeRunner } from '../serve'; - -class MyServeRunner extends ServeRunner { - constructor(protected readonly e: any) { - super(); - } - - async getCommandMetadata(): Promise { } - modifyOpenUrl(): any { } - async serveProject(): Promise { } -} - -describe('@ionic/cli', () => { - - describe('lib/serve', () => { - - describe('ServeRunner', () => { - - describe('createOptionsFromCommandLine', () => { - - const defaults = { - '--': [], - publicHost: undefined, - host: 'localhost', - browser: undefined, - browserOption: undefined, - engine: 'browser', - externalAddressRequired: false, - livereload: true, - open: false, - port: 8100, - proxy: true, - project: undefined, - verbose: false, - }; - - it('should provide defaults with no options', () => { - const runner = new MyServeRunner({}); - const result = runner.createOptionsFromCommandLine([], { _: [] }); - expect(result).toEqual(defaults); - }); - - it('should provide options from negations of cli flag defaults', () => { - const runner = new MyServeRunner({}); - const result = runner.createOptionsFromCommandLine([], { _: [], livereload: false, proxy: false, open: true, externalAddressRequired: true }); - expect(result).toEqual({ ...defaults, livereload: false, proxy: false, open: true, externalAddressRequired: true }); - }); - - it('should allow overrides of default values', () => { - const runner = new MyServeRunner({}); - const result = runner.createOptionsFromCommandLine([], { _: [], host: '0.0.0.0', port: '1111', 'livereload-port': '2222', 'dev-logger-port': '3333' }); - expect(result).toEqual({ ...defaults, host: '0.0.0.0', port: 1111 }); - }); - - it('should respect --external flag', () => { - const runner = new MyServeRunner({}); - const result = runner.createOptionsFromCommandLine([], { _: [], host: 'localhost', external: true }); - expect(result).toEqual({ ...defaults, host: '0.0.0.0' }); - }); - - it('should respect --project flag', () => { - const runner = new MyServeRunner({}); - const result = runner.createOptionsFromCommandLine([], { _: [], project: 'app' }); - expect(result).toEqual({ ...defaults, project: 'app' }); - }); - - it('should pass on separated args', () => { - const runner = new MyServeRunner({}); - const result = runner.createOptionsFromCommandLine([], { _: [], '--': ['foo', '--bar'] }); - expect(result).toEqual({ ...defaults, '--': ['foo', '--bar'] }); - }); - - }); - - }); - - }); - -}); diff --git a/packages/@ionic/cli/src/lib/__tests__/ssh-config.ts b/packages/@ionic/cli/src/lib/__tests__/ssh-config.ts deleted file mode 100644 index 479ed17679..0000000000 --- a/packages/@ionic/cli/src/lib/__tests__/ssh-config.ts +++ /dev/null @@ -1,70 +0,0 @@ -import * as path from 'path'; - -import SSHConfig from 'ssh-config'; -import { readFile } from '@ionic/utils-fs'; - -import { ensureHostAndKeyPath } from '../ssh-config'; - -describe('@ionic/cli', () => { - - describe('lib/ssh-config', () => { - - describe('ensureHostAndKeyPath', () => { - - const expected = 'Host bar\n IdentityFile /id_rsa\n'; - - it('should stringify with empty file', async () => { - const conf = SSHConfig.parse(''); - ensureHostAndKeyPath(conf, { host: 'bar' }, '/id_rsa'); - expect(SSHConfig.stringify(conf)).toEqual(expected); - }); - - it('should stringify with config1 file', async () => { - const config1 = await readFile(path.resolve(__dirname, 'fixtures/ssh-config/config1'), { encoding: 'utf8' }); - const conf = SSHConfig.parse(config1); - ensureHostAndKeyPath(conf, { host: 'bar' }, '/id_rsa'); - expect(SSHConfig.stringify(conf)).toEqual(`${config1}\n${expected}`); - }); - - it('should stringify with config2 file', async () => { - const config2 = await readFile(path.resolve(__dirname, 'fixtures/ssh-config/config2'), { encoding: 'utf8' }); - const conf = SSHConfig.parse(config2); - ensureHostAndKeyPath(conf, { host: 'bar' }, '/id_rsa'); - expect(SSHConfig.stringify(conf)).toEqual(`${config2}\n\n${expected}`); - }); - - it('should stringify with config3 file', async () => { - const config3 = await readFile(path.resolve(__dirname, 'fixtures/ssh-config/config3'), { encoding: 'utf8' }); - const conf = SSHConfig.parse(config3); - ensureHostAndKeyPath(conf, { host: 'bar' }, '/id_rsa'); - expect(SSHConfig.stringify(conf)).toEqual(config3); - }); - - it('should stringify with config4 file', async () => { - const config4 = await readFile(path.resolve(__dirname, 'fixtures/ssh-config/config4'), { encoding: 'utf8' }); - const conf = SSHConfig.parse(config4); - ensureHostAndKeyPath(conf, { host: 'bar' }, '/id_rsa'); - const s = config4.split('\n'); - s[s.length - 2] = ' IdentityFile /id_rsa'; - expect(SSHConfig.stringify(conf)).toEqual(s.join('\n')); - }); - - it('should stringify with config5 file', async () => { - const config5 = await readFile(path.resolve(__dirname, 'fixtures/ssh-config/config5'), { encoding: 'utf8' }); - const conf = SSHConfig.parse(config5); - ensureHostAndKeyPath(conf, { host: 'bar' }, '/id_rsa'); - expect(SSHConfig.stringify(conf)).toEqual(config5); - }); - - it('should stringify with config6 file', async () => { - const config6 = await readFile(path.resolve(__dirname, 'fixtures/ssh-config/config6'), { encoding: 'utf8' }); - const conf = SSHConfig.parse(config6); - ensureHostAndKeyPath(conf, { host: 'bar' }, '/id_rsa'); - expect(SSHConfig.stringify(conf)).toEqual(`${config6}\n${expected}`); - }); - - }); - - }); - -}); diff --git a/packages/@ionic/cli/src/lib/__tests__/ssh.ts b/packages/@ionic/cli/src/lib/__tests__/ssh.ts deleted file mode 100644 index 01324036c4..0000000000 --- a/packages/@ionic/cli/src/lib/__tests__/ssh.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { ERROR_SSH_INVALID_PUBKEY, parsePublicKey } from '../ssh'; - -describe('@ionic/cli', () => { - - describe('lib/ssh', () => { - - describe('parsePublicKey', () => { - - const valids = [ - 'AAAAB3NzaC1yc2EAAAABJQAAAQEAiDDC3iewOnn2kXbfOu2/LdHEiTFQoJufcSBzZ0COogvehg3L72biGeQD19nNvF3Ou05II5UGxVbpQG5yYOwH42AVVZf7fqUTjQEm7ig1SLMwJ9QsGw9CVSgpgVDnm3t7mgcVyOT+sp2lYxqPLzYakVE2OSBjzBRyau210FYIxbA4s4pJlFOcfJUmrnYwibUe6JqpXrSF0H1IGRE8ZR+Ck8GVAwyiMeOK5ea1gQ50khy+KuvRG+6+lYaAsUXDGCOaZIbHJwnMuVp50sObne1doX9rhYwYWXtcjsr5U+/HGEHWi2z12vE2Cd4wirEHP3ROzsWn51hmqJj4nDvYt4MtZQ==', - 'AAAAB3NzaC1yc2EAAAADAQABAAABAQDqQmHXeWG737iR40rMO5enlhHMTGTmTi4Kehql9TxheN1PPujeBlQk6Kl0ylYDjPqqUBrSxq/tdxHyyF8h5QD+L5uO9PHyEVLoNCE0gYRhvSupf6K4EhZNz91KVYchRNZcldt6esf6KQfZIF20fNKLnIBJ5rkGO3vJJJkDZIBMH3IzMI7DUyZIA3RBE7wzfbSaZC3X87GWxS3IYM31IdFHXlKwBNFQpv4PKZ3RAeLRHpSuiyXlIuBWE7KEJCxcbDSFSTc3oFNUkwQACSfDze9Gt2T3zCowVB+G5k1pTpBunmMb6uXdzcqAzR41M2cpbbqRCpRmqzbW8psUbcsyTMM5', - ]; - - const fmt = (bytes: string, annotation?: string) => `ssh-rsa ${bytes}${annotation ? ' ' + annotation : ''}`; - - it('should parse valid public keys', async () => { - for (let valid of valids) { - const formatted = fmt(valid, 'super'); - const results = parsePublicKey(formatted); - expect(results).toEqual([formatted, 'ssh-rsa', valid, 'super']); - } - }); - - it('should parse valid public keys with no annotation', async () => { - for (let valid of valids) { - const formatted = fmt(valid); - const results = parsePublicKey(formatted); - expect(results).toEqual([formatted, 'ssh-rsa', valid, '']); - } - }); - - it('should throw error for invalid public keys', async () => { - expect(() => parsePublicKey('blah')).toThrow(ERROR_SSH_INVALID_PUBKEY); - }); - - }); - - }); - -}); diff --git a/packages/@ionic/cli/src/lib/app.ts b/packages/@ionic/cli/src/lib/app.ts deleted file mode 100644 index 6b836de485..0000000000 --- a/packages/@ionic/cli/src/lib/app.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { App, AppAssociation, AssociationType, IClient, IPaginator, PaginateArgs, PaginatorState, ResourceClientCreate, ResourceClientLoad, ResourceClientPaginate, Response } from '../definitions'; -import { isAppAssociationResponse, isAppResponse, isAppsResponse } from '../guards'; - -import { weak } from './color'; -import { ResourceClient, createFatalAPIFormat } from './http'; - -export function formatName(app: Pick) { - if (app.org) { - return `${weak(`${app.org.name} / `)}${app.name}`; - } - - return app.name; -} - -export interface AppClientDeps { - readonly client: IClient; -} - -export interface AppCreateDetails { - readonly name: string; - readonly org_id?: string; -} - -export class AppClient extends ResourceClient implements ResourceClientLoad, ResourceClientCreate, ResourceClientPaginate { - constructor(readonly token: string, readonly e: AppClientDeps) { - super(); - } - - async load(id: string): Promise { - const { req } = await this.e.client.make('GET', `/apps/${id}`); - this.applyAuthentication(req, this.token); - const res = await this.e.client.do(req); - - if (!isAppResponse(res)) { - throw createFatalAPIFormat(req, res); - } - - return res.data; - } - - async create(details: AppCreateDetails): Promise { - const { req } = await this.e.client.make('POST', '/apps'); - this.applyAuthentication(req, this.token); - req.send(details); - const res = await this.e.client.do(req); - - if (!isAppResponse(res)) { - throw createFatalAPIFormat(req, res); - } - - return res.data; - } - - paginate(args: Partial>> = {}, orgId?: string): IPaginator, PaginatorState> { - return this.e.client.paginate({ - reqgen: async () => { - const { req } = await this.e.client.make('GET', '/apps'); - this.applyAuthentication(req, this.token); - if (orgId) { - req.send({ org_id: orgId }); - } - return { req }; - }, - guard: isAppsResponse, - ...args, - }); - } - - async createAssociation(id: string, association: { repoId: number; type: AssociationType; branches: string[] }): Promise { - const { req } = await this.e.client.make('POST', `/apps/${id}/repository`); - - req - .set('Authorization', `Bearer ${this.token}`) - .send({ - repository_id: association.repoId, - type: association.type, - branches: association.branches, - }); - - const res = await this.e.client.do(req); - - if (!isAppAssociationResponse(res)) { - throw createFatalAPIFormat(req, res); - } - - return res.data; - } - - async deleteAssociation(id: string): Promise { - const { req } = await this.e.client.make('DELETE', `/apps/${id}/repository`); - - req - .set('Authorization', `Bearer ${this.token}`) - .send({}); - - await req; - } -} diff --git a/packages/@ionic/cli/src/lib/build.ts b/packages/@ionic/cli/src/lib/build.ts deleted file mode 100644 index dc07de3a3b..0000000000 --- a/packages/@ionic/cli/src/lib/build.ts +++ /dev/null @@ -1,316 +0,0 @@ -import { BaseError, MetadataGroup } from '@ionic/cli-framework'; -import { PromptModule } from '@ionic/cli-framework-prompts'; -import { createProcessEnv } from '@ionic/utils-process'; -import { ERROR_COMMAND_NOT_FOUND, SubprocessError } from '@ionic/utils-subprocess'; -import { debug as Debug } from 'debug'; - -import { BaseBuildOptions, BuildOptions, CommandLineInputs, CommandLineOptions, CommandMetadata, CommandMetadataOption, IConfig, ILogger, IProject, IShell, NpmClient, Runner } from '../definitions'; - -import { ancillary, input, strong } from './color'; -import { BuildCLIProgramNotFoundException, FatalException } from './errors'; -import { Hook } from './hooks'; - -const debug = Debug('ionic:lib:build'); - -export const BUILD_SCRIPT = 'ionic:build'; - -export const COMMON_BUILD_COMMAND_OPTIONS: readonly CommandMetadataOption[] = [ - { - name: 'engine', - summary: `Target engine (e.g. ${['browser', 'cordova'].map(e => input(e)).join(', ')})`, - groups: [MetadataGroup.ADVANCED], - }, - { - name: 'platform', - summary: `Target platform on chosen engine (e.g. ${['ios', 'android'].map(e => input(e)).join(', ')})`, - groups: [MetadataGroup.ADVANCED], - }, -]; - -export interface BuildRunnerDeps { - readonly config: IConfig; - readonly log: ILogger; - readonly project: IProject; - readonly prompt: PromptModule; - readonly shell: IShell; -} - -export abstract class BuildRunner> implements Runner { - - protected abstract readonly e: BuildRunnerDeps; - - abstract getCommandMetadata(): Promise>; - abstract createOptionsFromCommandLine(inputs: CommandLineInputs, options: CommandLineOptions): T; - abstract buildProject(options: T): Promise; - - getPkgManagerBuildCLI(): PkgManagerBuildCLI { - const pkgManagerCLIs = { - npm: NpmBuildCLI, - pnpm: PnpmBuildCLI, - yarn: YarnBuildCLI, - }; - - const client = this.e.config.get('npmClient'); - const CLI = pkgManagerCLIs[client]; - - if (CLI) { - return new CLI(this.e); - } - - throw new BuildCLIProgramNotFoundException('Unknown CLI client: ' + client); - } - - createBaseOptionsFromCommandLine(inputs: CommandLineInputs, options: CommandLineOptions): BaseBuildOptions { - const separatedArgs = options['--']; - const [platform] = options['platform'] ? [String(options['platform'])] : inputs; - const engine = this.determineEngineFromCommandLine(options); - const project = options['project'] ? String(options['project']) : undefined; - const verbose = !!options['verbose']; - - return { '--': separatedArgs ? separatedArgs : [], engine, platform, project, verbose }; - } - - determineEngineFromCommandLine(options: CommandLineOptions): string { - if (options['engine']) { - return String(options['engine']); - } - - if (options['cordova']) { - return 'cordova'; - } - - return 'browser'; - } - - async beforeBuild(options: T): Promise { - const hook = new BuildBeforeHook(this.e); - - try { - await hook.run({ name: hook.name, build: options }); - } catch (e: any) { - if (e instanceof BaseError) { - throw new FatalException(e.message); - } - - throw e; - } - } - - async run(options: T): Promise { - debug('build options: %O', options); - - if (options.engine === 'cordova' && !options.platform) { - this.e.log.warn(`Cordova engine chosen without a target platform. This could cause issues. Please use the ${input('--platform')} option.`); - } - - await this.beforeBuild(options); - await this.buildProject(options); - await this.afterBuild(options); - } - - async afterBuild(options: T): Promise { - const hook = new BuildAfterHook(this.e); - - try { - await hook.run({ name: hook.name, build: options }); - } catch (e: any) { - if (e instanceof BaseError) { - throw new FatalException(e.message); - } - - throw e; - } - } -} - -export abstract class BuildCLI { - - /** - * The pretty name of this Build CLI. - */ - abstract readonly name: string; - - /** - * The npm package of this Build CLI. - */ - abstract readonly pkg: string; - - /** - * The bin program to use for this Build CLI. - */ - abstract readonly program: string; - - /** - * If specified, `package.json` is inspected for this script to use instead - * of `program`. - */ - abstract readonly script?: string; - - /** - * If true, the Build CLI will not prompt to be installed. - */ - readonly global: boolean = false; - - private _resolvedProgram?: string; - - constructor(protected readonly e: BuildRunnerDeps) { } - - get resolvedProgram() { - if (this._resolvedProgram) { - return this._resolvedProgram; - } - - return this.program; - } - - /** - * Build the arguments for starting this Build CLI. Called by `this.run()`. - */ - protected abstract buildArgs(options: T): Promise; - - /** - * Build the environment variables for this Build CLI. Called by `this.run()`. - */ - protected async buildEnvVars(options: T): Promise { - return process.env; - } - - async resolveScript(): Promise { - if (typeof this.script === 'undefined') { - return; - } - - const pkg = await this.e.project.requirePackageJson(); - - return pkg.scripts && pkg.scripts[this.script]; - } - - async build(options: T): Promise { - this._resolvedProgram = await this.resolveProgram(); - - await this.runWrapper(options); - } - - protected async runWrapper(options: T): Promise { - try { - return await this.run(options); - } catch (e: any) { - if (!(e instanceof BuildCLIProgramNotFoundException)) { - throw e; - } - - if (this.global) { - this.e.log.nl(); - throw new FatalException(`${input(this.pkg)} is required for this command to work properly.`); - } - - this.e.log.nl(); - this.e.log.info( - `Looks like ${input(this.pkg)} isn't installed in this project.\n` + - `This package is required for this command to work properly.` - ); - - const installed = await this.promptToInstall(); - - if (!installed) { - this.e.log.nl(); - throw new FatalException(`${input(this.pkg)} is required for this command to work properly.`); - } - - return this.run(options); - } - } - - protected async run(options: T): Promise { - const args = await this.buildArgs(options); - const env = await this.buildEnvVars(options); - - try { - await this.e.shell.run(this.resolvedProgram, args, { stdio: 'inherit', cwd: this.e.project.directory, fatalOnNotFound: false, env: createProcessEnv(env) }); - } catch (e: any) { - if (e instanceof SubprocessError && e.code === ERROR_COMMAND_NOT_FOUND) { - throw new BuildCLIProgramNotFoundException(`${strong(this.resolvedProgram)} command not found.`); - } - - throw e; - } - } - - protected async resolveProgram(): Promise { - if (typeof this.script !== 'undefined') { - debug(`Looking for ${ancillary(this.script)} npm script.`); - - if (await this.resolveScript()) { - debug(`Using ${ancillary(this.script)} npm script.`); - return this.e.config.get('npmClient'); - } - } - - return this.program; - } - - protected async promptToInstall(): Promise { - const { pkgManagerArgs } = await import('./utils/npm'); - const [manager, ...managerArgs] = await pkgManagerArgs(this.e.config.get('npmClient'), { command: 'install', pkg: this.pkg, saveDev: true, saveExact: true }); - - this.e.log.nl(); - - const confirm = await this.e.prompt({ - name: 'confirm', - message: `Install ${input(this.pkg)}?`, - type: 'confirm', - }); - - if (!confirm) { - this.e.log.warn(`Not installing--here's how to install manually: ${input(`${manager} ${managerArgs.join(' ')}`)}`); - return false; - } - - await this.e.shell.run(manager, managerArgs, { cwd: this.e.project.directory }); - - return true; - } -} - -abstract class PkgManagerBuildCLI extends BuildCLI { - readonly abstract program: NpmClient; - readonly global = true; - readonly script = BUILD_SCRIPT; - - protected async resolveProgram(): Promise { - return this.program; - } - - protected async buildArgs(options: BaseBuildOptions): Promise { - const { pkgManagerArgs } = await import('./utils/npm'); - const [, ...pkgArgs] = await pkgManagerArgs(this.program, { command: 'run', script: this.script, scriptArgs: [...options['--'] || []] }); - - return pkgArgs; - } -} - -export class NpmBuildCLI extends PkgManagerBuildCLI { - readonly name = 'npm CLI'; - readonly pkg = 'npm'; - readonly program = 'npm'; -} - -export class PnpmBuildCLI extends PkgManagerBuildCLI { - readonly name = 'pnpm CLI'; - readonly pkg = 'pnpm'; - readonly program = 'pnpm'; -} - -export class YarnBuildCLI extends PkgManagerBuildCLI { - readonly name = 'Yarn'; - readonly pkg = 'yarn'; - readonly program = 'yarn'; -} - -class BuildBeforeHook extends Hook { - readonly name = 'build:before'; -} - -class BuildAfterHook extends Hook { - readonly name = 'build:after'; -} diff --git a/packages/@ionic/cli/src/lib/color.ts b/packages/@ionic/cli/src/lib/color.ts deleted file mode 100644 index 789ff736d6..0000000000 --- a/packages/@ionic/cli/src/lib/color.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Colors, DEFAULT_COLORS, HelpColors } from '@ionic/cli-framework'; -import chalk from 'chalk'; - -const HELP_COLORS: Partial = { - title: chalk.bold, -}; - -export const COLORS: Colors = { ...DEFAULT_COLORS, help: { ...DEFAULT_COLORS.help, ...HELP_COLORS } }; - -export const { strong, weak, input, success, failure, ancillary, help: { title }, log: { WARN } } = COLORS; diff --git a/packages/@ionic/cli/src/lib/command.ts b/packages/@ionic/cli/src/lib/command.ts deleted file mode 100644 index 92c03fae9d..0000000000 --- a/packages/@ionic/cli/src/lib/command.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { BaseCommand, DEFAULT_COLORS, generateCommandPath, unparseArgs } from '@ionic/cli-framework'; -import { LOGGER_LEVELS, OutputStrategy, StreamHandler, StreamOutputStrategy, TTYOutputStrategy, TaskChain } from '@ionic/cli-framework-output'; -import { TERMINAL_INFO } from '@ionic/utils-terminal'; - -import { CommandInstanceInfo, CommandLineInputs, CommandLineOptions, CommandMetadata, CommandMetadataInput, CommandMetadataOption, ICommand, INamespace, IProject, IonicEnvironment } from '../definitions'; -import { isCommandPreRun } from '../guards'; - -import { input } from './color'; -import { createDefaultLoggerHandlers, createFormatter } from './utils/logger'; - -export abstract class Command extends BaseCommand implements ICommand { - protected readonly taskChains: TaskChain[] = []; - - constructor(public namespace: INamespace) { - super(namespace); - } - - get env(): IonicEnvironment { - return this.namespace.root.env; - } - - get project(): IProject | undefined { - return this.namespace.root.project; - } - - createTaskChain(): TaskChain { - let output: OutputStrategy; - - const formatter = createFormatter(); - - if (this.env.flags.interactive) { - output = new TTYOutputStrategy({ stream: process.stdout, colors: DEFAULT_COLORS }); - this.env.log.handlers = new Set([new StreamHandler({ stream: output.stream, formatter })]); - } else { - this.env.log.handlers = createDefaultLoggerHandlers(); - output = new StreamOutputStrategy({ stream: this.env.log.createWriteStream(LOGGER_LEVELS.INFO, false), colors: DEFAULT_COLORS }); - } - - const chain = output.createTaskChain(); - this.taskChains.push(chain); - - chain.on('end', () => { - this.env.log.handlers = createDefaultLoggerHandlers(); - }); - - return chain; - } - - async execute(inputs: CommandLineInputs, options: CommandLineOptions, runinfo: CommandInstanceInfo): Promise { - if (isCommandPreRun(this)) { - await this.preRun(inputs, options, runinfo); - } - - try { - await this.validate(inputs); - } catch (e: any) { - if (!this.env.flags.interactive) { - this.env.log.warn(`Command ran non-interactively due to ${input('--no-interactive')} flag, CI being detected, non-TTY, or a config setting.`); - } - - throw e; - } - - const runPromise = this.run(inputs, options, runinfo); - - const telemetryPromise = (async () => { - if (this.env.config.get('telemetry') !== false && !TERMINAL_INFO.ci && TERMINAL_INFO.tty) { - const { Telemetry } = await import('./telemetry'); - - let cmdInputs: CommandLineInputs = []; - const metadata = await this.getMetadata(); - - if (metadata.name === 'login' || metadata.name === 'logout') { - // This is a hack to wait until the selected commands complete before - // sending telemetry data. These commands update `this.env` in some - // way, which is used in the `Telemetry` instance. - await runPromise; - } else if (metadata.name === 'completion') { - // Ignore telemetry for these commands. - return; - } else if (metadata.name === 'help') { - cmdInputs = inputs; - } else { - cmdInputs = await this.getCleanInputsForTelemetry(inputs, options); - } - - const cmd: ICommand = this; - const path = await generateCommandPath(cmd); - const telemetry = new Telemetry({ client: this.env.client, config: this.env.config, getInfo: this.env.getInfo, ctx: this.env.ctx, project: this.project, session: this.env.session }); - - await telemetry.sendCommand(path.map(([p]) => p).join(' '), cmdInputs); - } - })(); - - await Promise.all([runPromise, telemetryPromise]); - } - - async getCleanInputsForTelemetry(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - const initialOptions: CommandLineOptions = { _: [] }; - - const metadata = await this.getMetadata(); - const filteredInputs = inputs.map((input, i) => metadata.inputs && (metadata.inputs[i] && metadata.inputs[i].private) ? '*****' : input); - const filteredOptions = Object.keys(options) - .filter(optionName => { - if (optionName === '_') { - return false; - } - - const metadataOption = metadata.options && metadata.options.find(o => { - return o.name === optionName || (typeof o.aliases !== 'undefined' && o.aliases.includes(optionName)); - }); - - if (metadataOption && metadataOption.aliases && metadataOption.aliases.includes(optionName)) { - return false; // exclude aliases - } - - if (!metadataOption) { - return true; // include unknown options - } - - if (metadataOption.private) { - return false; // exclude private options - } - - if (typeof metadataOption.default !== 'undefined' && metadataOption.default === options[optionName]) { - return false; // exclude options that match their default value (means it wasn't supplied by user) - } - - return true; - }) - .reduce((allOptions, optionName) => { - allOptions[optionName] = options[optionName]; - return allOptions; - }, initialOptions); - - const optionInputs = unparseArgs(filteredOptions, { useDoubleQuotes: true }); - return filteredInputs.concat(optionInputs); - } -} diff --git a/packages/@ionic/cli/src/lib/config.ts b/packages/@ionic/cli/src/lib/config.ts deleted file mode 100644 index 0e8464bf27..0000000000 --- a/packages/@ionic/cli/src/lib/config.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { BaseConfig, BaseConfigOptions, MetadataGroup, ParsedArgs, metadataOptionsToParseArgsOptions, parseArgs } from '@ionic/cli-framework'; -import * as os from 'os'; -import * as path from 'path'; - -import { CommandMetadataOption, ConfigFile, CreateRequestOptions, IConfig, OAuthServerConfig } from '../definitions'; - -export const GLOBAL_OPTIONS: readonly CommandMetadataOption[] = [ - { - name: 'help', - summary: 'Display help for commands', - aliases: ['h'], - type: Boolean, - groups: [MetadataGroup.HIDDEN], - }, - { - name: 'verbose', - summary: 'Print debug log messages', - type: Boolean, - }, - { - name: 'quiet', - summary: 'Only print warning and error log messages', - type: Boolean, - }, - { - name: 'interactive', - summary: 'Disable interactivity such as progress indicators and prompts', - type: Boolean, - default: true, - }, - { - name: 'color', - summary: 'Disable colors in stdout', - type: Boolean, - default: true, - }, - { - name: 'confirm', - summary: 'Automatically answer YES to confirmation prompts', - type: Boolean, - }, - { - name: 'project', - summary: 'The project ID to use in a multi-app configuration setup', - groups: [MetadataGroup.HIDDEN], - }, - { - name: 'json', - summary: 'Use JSON when operating with stdout, if possible', - type: Boolean, - groups: [MetadataGroup.HIDDEN], - }, -]; - -export const CONFIG_FILE = 'config.json'; -export const DEFAULT_CONFIG_DIRECTORY = path.resolve(os.homedir(), '.ionic'); - -export class Config extends BaseConfig implements IConfig { - constructor(p: string, options?: BaseConfigOptions) { - super(p, options); - - const c = this.c as any; - - // <4.0.0 config migration - if (c.state) { - // start fresh - this.c = { - 'version': '4.0.0', - 'telemetry': c.telemetry, - 'npmClient': c.npmClient, - 'interactive': c.interactive, - 'user.id': c.user && c.user.id, - 'user.email': c.user && c.user.email, - 'git.setup': c.git && c.git.setup, - 'tokens.user': c.tokens && c.tokens.user, - 'tokens.telemetry': c.tokens && c.tokens.telemetry, - 'features.ssl-commands': c.features && c.features['ssl-commands'], - }; - } - } - - provideDefaults(config: Partial): ConfigFile { - return { - 'version': '4.0.0', - 'telemetry': true, - 'npmClient': 'npm', - }; - } - - getAPIUrl(): string { - return this.get('urls.api', 'https://api.ionicjs.com'); - } - - getDashUrl(): string { - return this.get('urls.dash', 'https://dashboard.ionicframework.com'); - } - - getGitHost(): string { - return this.get('git.host', 'git.ionicjs.com'); - } - - getGitPort(): number { - return this.get('git.port', 22); - } - - getHTTPConfig(): CreateRequestOptions { - const { c } = this; - - return { - userAgent: `node/superagent/Ionic CLI ${c.version}`, - ssl: { - cafile: c['ssl.cafile'], - certfile: c['ssl.certfile'], - keyfile: c['ssl.keyfile'], - }, - proxy: c['proxy'], - }; - } - - getOpenIDOAuthConfig(): OAuthServerConfig { - return { - authorizationUrl: this.get('oauth.openid.authorization_url', 'https://ionicframework.com/oauth/authorize'), - tokenUrl: this.get('oauth.openid.token_url', 'https://api.ionicjs.com/oauth/token'), - clientId: this.get('oauth.openid.client_id', 'cli'), - apiAudience: this.get('oauth.openid.api_audience', 'https://api.ionicjs.com'), - }; - } -} - -export function parseGlobalOptions(pargv: string[]): ParsedArgs { - return parseArgs(pargv, metadataOptionsToParseArgsOptions(GLOBAL_OPTIONS)); -} diff --git a/packages/@ionic/cli/src/lib/cordova-res.ts b/packages/@ionic/cli/src/lib/cordova-res.ts deleted file mode 100644 index f898117c01..0000000000 --- a/packages/@ionic/cli/src/lib/cordova-res.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { ERROR_COMMAND_NOT_FOUND, SubprocessError, which } from '@ionic/utils-subprocess'; - -import { CommandLineOptions, IConfig, ILogger, IShell, IShellRunOptions, NpmClient } from '../definitions'; - -import { input, weak } from './color'; -import { FatalException } from './errors'; -import { createPrefixedWriteStream } from './utils/logger'; -import { pkgManagerArgs } from './utils/npm'; - -export const SUPPORTED_PLATFORMS: readonly string[] = ['ios', 'android']; - -export interface CordovaResSchema { - platform?: string; -} - -export function createCordovaResArgs({ platform }: CordovaResSchema, options: CommandLineOptions): string[] { - const args: string[] = []; - - if (platform) { - args.push(platform); - } - - if (options['icon']) { - args.push('--type', 'icon'); - } else if (options['splash']) { - args.push('--type', 'splash'); - } - - if (options['verbose']) { - args.push('--verbose'); - } - - return args; -} - -export interface RunCordovaResDeps { - readonly config: IConfig; - readonly log: ILogger; - readonly shell: IShell; -} - -export async function runCordovaRes({ config, log, shell }: RunCordovaResDeps, args: readonly string[], options: IShellRunOptions = {}): Promise { - const stream = createPrefixedWriteStream(log, weak(`[cordova-res]`)); - - try { - await shell.run('cordova-res', args, { showCommand: true, fatalOnNotFound: false, stream, ...options }); - } catch (e: any) { - if (e instanceof SubprocessError && e.code === ERROR_COMMAND_NOT_FOUND) { - throw await createCordovaResNotFoundError(config.get('npmClient')); - } - - throw e; - } -} - -export interface CheckCordovaResDeps { - readonly config: IConfig; -} - -export async function checkCordovaRes({ config }: CheckCordovaResDeps): Promise { - const p = await findCordovaRes(); - - if (!p) { - throw await createCordovaResNotFoundError(config.get('npmClient')); - } -} - -export async function findCordovaRes(): Promise { - try { - return await which('cordova-res'); - } catch (e: any) { - if (e.code !== 'ENOENT') { - throw e; - } - } -} - -export async function createCordovaResNotFoundError(npmClient: NpmClient): Promise { - return new FatalException(await createCordovaResNotFoundMessage(npmClient)); -} - -export async function createCordovaResNotFoundMessage(npmClient: NpmClient): Promise { - const installArgs = await pkgManagerArgs(npmClient, { command: 'install', pkg: 'cordova-res', global: true }); - - return ( - `${input('cordova-res')} was not found on your PATH. Please install it globally:\n\n` + - `${input(installArgs.join(' '))}\n` - ); -} diff --git a/packages/@ionic/cli/src/lib/diff.ts b/packages/@ionic/cli/src/lib/diff.ts deleted file mode 100644 index 57b9d6b104..0000000000 --- a/packages/@ionic/cli/src/lib/diff.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { failure, input, strong } from './color'; - -export async function diffPatch(filename: string, text1: string, text2: string): Promise { - const JsDiff = await import('diff'); - - return JsDiff.createPatch(filename, text1, text2, '', '').split('\n').map(line => { - if (line.indexOf('-') === 0 && line.indexOf('---') !== 0) { - line = strong(failure(line)); - } else if (line.indexOf('+') === 0 && line.indexOf('+++') !== 0) { - line = strong(input(line)); - } - - return line; - }).slice(2).join('\n'); -} diff --git a/packages/@ionic/cli/src/lib/environment.ts b/packages/@ionic/cli/src/lib/environment.ts deleted file mode 100644 index 923fe0502e..0000000000 --- a/packages/@ionic/cli/src/lib/environment.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { PromptModule } from '@ionic/cli-framework-prompts'; - -import { IClient, IConfig, ILogger, ISession, IShell, InfoItem, IonicContext, IonicEnvironment, IonicEnvironmentFlags } from '../definitions'; - -export interface EnvironmentDeps { - readonly client: IClient; - readonly config: IConfig; // CLI global config (~/.ionic/config.json) - readonly flags: IonicEnvironmentFlags; - readonly getInfo: () => Promise; - readonly log: ILogger; - readonly ctx: IonicContext; - readonly prompt: PromptModule; - readonly session: ISession; - readonly shell: IShell; -} - -export class Environment implements IonicEnvironment { - readonly flags: IonicEnvironmentFlags; - readonly client: IClient; - readonly config: IConfig; // CLI global config (~/.ionic/config.json) - getInfo: () => Promise; - readonly log: ILogger; - readonly prompt: PromptModule; - session: ISession; - readonly shell: IShell; - readonly ctx: IonicContext; - - constructor({ client, config, flags, getInfo, log, ctx, prompt, session, shell }: EnvironmentDeps) { - this.client = client; - this.config = config; - this.flags = flags; - this.getInfo = getInfo; - this.log = log; - this.ctx = ctx; - this.prompt = prompt; - this.session = session; - this.shell = shell; - } -} diff --git a/packages/@ionic/cli/src/lib/errors.ts b/packages/@ionic/cli/src/lib/errors.ts deleted file mode 100644 index 20adb39388..0000000000 --- a/packages/@ionic/cli/src/lib/errors.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { BaseError } from '@ionic/cli-framework'; - -export class BaseException extends BaseError { - readonly name = 'Exception'; -} - -export class FatalException extends BaseException { - fatal = true; - - constructor(public message = '', public exitCode = 1) { - super(message); - } -} - -export class BuildCLIProgramNotFoundException extends BaseException {} - -export class ServeCLIProgramNotFoundException extends BaseException {} - -export class SessionException extends BaseException {} - -export class RunnerException extends BaseException {} - -export class RunnerNotFoundException extends RunnerException {} - -export class IntegrationException extends BaseException {} - -export class IntegrationNotFoundException extends IntegrationException {} - -export class HookException extends BaseException {} diff --git a/packages/@ionic/cli/src/lib/events.ts b/packages/@ionic/cli/src/lib/events.ts deleted file mode 100644 index 3c52b85650..0000000000 --- a/packages/@ionic/cli/src/lib/events.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { debug as Debug } from 'debug'; - -import { ServeDetails } from '../definitions'; - -const debug = Debug('ionic:lib:events'); - -export interface IPCEvent { - type: 'event'; - event: E; - data: D; -} - -export function emit(event: 'serve:ready', data: ServeDetails): boolean; -export function emit(event: E, data: D): boolean { - if (!process.send) { - debug('No process.send, not emitting event %s', event); - return false; - } - - const msg: IPCEvent = { type: 'event', event, data }; - - process.send(msg); - debug('Sent event %s as IPC message to parent process', event); - - return true; -} diff --git a/packages/@ionic/cli/src/lib/executor.ts b/packages/@ionic/cli/src/lib/executor.ts deleted file mode 100644 index a813dd9351..0000000000 --- a/packages/@ionic/cli/src/lib/executor.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { BaseExecutor, metadataOptionsToParseArgsOptions, parseArgs, stripOptions } from '@ionic/cli-framework'; -import * as lodash from 'lodash'; - -import { CommandInstanceInfo, CommandMetadata, CommandMetadataInput, CommandMetadataOption, ICommand, INamespace, NamespaceLocateResult } from '../definitions'; -import { isCommand } from '../guards'; - -import { input } from './color'; -import { GLOBAL_OPTIONS } from './config'; -import { FatalException } from './errors'; - -export const VERSION_FLAGS: readonly string[] = ['--version', '-v']; -export const HELP_FLAGS: readonly string[] = ['--help', '-?', '-h']; - -export interface ExecutorDeps { - readonly namespace: INamespace; -} - -export class Executor extends BaseExecutor { - async locate(argv: readonly string[]): Promise { - const pargs = stripOptions(argv, {}); - const location = await this.namespace.locate(pargs); - const args = lodash.drop(argv, location.path.length - 1); - - if (lodash.intersection(VERSION_FLAGS, argv).length > 0) { - return this.locate(['version', ...pargs]); - } else if (lodash.intersection(HELP_FLAGS, argv).length > 0 || !isCommand(location.obj)) { - return this.locate(['help', ...pargs]); - } - - return { ...location, args }; - } - - async run(command: ICommand, cmdargs: string[], { location, env, executor }: CommandInstanceInfo): Promise { - const metadata = await command.getMetadata(); - const parts = getFullCommandParts(location); - - if (metadata.options) { - const optMap = metadataToCmdOptsEnv(metadata, parts.slice(1)); - - // TODO: changes opt by reference, which is probably bad - for (const [ opt, envvar ] of optMap.entries()) { - const envdefault = env[envvar]; - - if (typeof envdefault !== 'undefined') { - opt.default = opt.type === Boolean ? (envdefault && envdefault !== '0' ? true : false) : envdefault; - } - } - } - - const metadataOpts = [...metadata.options ? metadata.options : [], ...GLOBAL_OPTIONS]; - const minimistOpts = metadataOptionsToParseArgsOptions(metadataOpts); - const cmdoptions = parseArgs(cmdargs, minimistOpts); - const cmdinputs = cmdoptions._; - - const { project } = this.namespace; - - if (project) { - if (project.details.context === 'multiapp') { - cmdoptions['project'] = project.details.id; - } - } else { - if (metadata.type === 'project') { - throw new FatalException( - `Sorry! ${input(parts.join(' '))} can only be run in an Ionic project directory.\n` + - `If this is a project you'd like to integrate with Ionic, run ${input('ionic init')}.` - ); - } - } - - await command.execute(cmdinputs, cmdoptions, { location, env, executor }); - } -} - -export async function runCommand(runinfo: CommandInstanceInfo, argv: string[]) { - const { env, executor } = runinfo; - - const metadata = await executor.namespace.getMetadata(); - executor.namespace.env.log.msg(`> ${input([metadata.name, ...argv].map(a => a.includes(' ') ? `"${a}"` : a).join(' '))}`); - - await executor.execute(argv, env); -} - -export function metadataToCmdOptsEnv(metadata: CommandMetadata, cmdNameParts: string[]): Map { - const optMap = new Map(); - - if (!metadata.options) { - return optMap; - } - - const prefix = `IONIC_CMDOPTS_${cmdNameParts.map(s => s.toUpperCase()).join('_')}`; - - for (const option of metadata.options) { - optMap.set(option, `${prefix}_${option.name.toUpperCase().split('-').join('_')}`); - } - - return optMap; -} - -export function getFullCommandParts(location: NamespaceLocateResult): string[] { - return location.path.map(([p]) => p); -} diff --git a/packages/@ionic/cli/src/lib/generate.ts b/packages/@ionic/cli/src/lib/generate.ts deleted file mode 100644 index f453ff44e1..0000000000 --- a/packages/@ionic/cli/src/lib/generate.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { PromptModule } from '@ionic/cli-framework-prompts'; - -import { CommandLineInputs, CommandLineOptions, CommandMetadata, GenerateOptions, IConfig, ILogger, IProject, IShell, IonicContext, Runner } from '../definitions'; - -export interface GenerateRunnerDeps { - readonly config: IConfig; - readonly ctx: IonicContext; - readonly log: ILogger; - readonly project: IProject; - readonly prompt: PromptModule; - readonly shell: IShell; -} - -export abstract class GenerateRunner implements Runner { - protected abstract readonly e: GenerateRunnerDeps; - - createOptionsFromCommandLine(inputs: CommandLineInputs, options: CommandLineOptions): GenerateOptions { - const [ name ] = inputs; - return { name }; - } - - async ensureCommandLine(inputs: CommandLineInputs, options: CommandLineOptions): Promise { /* overwritten in subclasses */ } - abstract getCommandMetadata(): Promise>; - abstract run(options: T): Promise; -} diff --git a/packages/@ionic/cli/src/lib/git.ts b/packages/@ionic/cli/src/lib/git.ts deleted file mode 100644 index 6cd670085a..0000000000 --- a/packages/@ionic/cli/src/lib/git.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { pathExists } from '@ionic/utils-fs'; -import * as path from 'path'; - -import { IShell } from '../definitions'; - -export interface GitUtilDeps { - shell: IShell; -} - -export async function isGitInstalled({ shell }: GitUtilDeps): Promise { - return Boolean(await shell.cmdinfo('git', ['--version'])); -} - -export async function getTopLevel({ shell }: GitUtilDeps): Promise { - return shell.cmdinfo('git', ['rev-parse', '--show-toplevel']); -} - -export async function isRepoInitialized(dir: string): Promise { - return pathExists(path.join(dir, '.git')); -} - -export async function initializeRepo({ shell }: GitUtilDeps, dir: string) { - await shell.run('git', ['init'], { cwd: dir }); -} - -export async function getIonicRemote({ shell }: GitUtilDeps, dir: string): Promise { - const regex = /ionic\t(.+) \(\w+\)/; - - // would like to use get-url, but not available in git 2.0.0 - const remotes = await shell.output('git', ['remote', '-v'], { cwd: dir }); - - for (const line of remotes.split('\n')) { - const match = regex.exec(line.trim()); - - if (match) { - return match[1]; - } - } -} - -export async function addIonicRemote({ shell }: GitUtilDeps, dir: string, url: string) { - await shell.run('git', ['remote', 'add', 'ionic', url], { cwd: dir }); -} - -export async function setIonicRemote({ shell }: GitUtilDeps, dir: string, url: string) { - await shell.run('git', ['remote', 'set-url', 'ionic', url], { cwd: dir }); -} diff --git a/packages/@ionic/cli/src/lib/help.ts b/packages/@ionic/cli/src/lib/help.ts deleted file mode 100644 index f3590ecc1a..0000000000 --- a/packages/@ionic/cli/src/lib/help.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { CommandHelpFormatterDeps as BaseCommandHelpFormatterDeps, CommandHelpSchema as BaseCommandHelpSchema, CommandSchemaHelpFormatter as BaseCommandSchemaHelpFormatter, CommandStringHelpFormatter as BaseCommandStringHelpFormatter, MetadataGroup, NO_COLORS, NamespaceHelpFormatterDeps as BaseNamespaceHelpFormatterDeps, NamespaceSchemaHelpFormatter as BaseNamespaceSchemaHelpFormatter, NamespaceStringHelpFormatter as BaseNamespaceStringHelpFormatter, formatOptionName, isOptionVisible } from '@ionic/cli-framework'; -import { filter } from '@ionic/utils-array'; - -import { CommandMetadata, CommandMetadataInput, CommandMetadataOption, HydratedCommandMetadata, ICommand, INamespace } from '../definitions'; - -import { COLORS } from './color'; -import { GLOBAL_OPTIONS } from './config'; - -const IONIC_LOGO = String.raw` - _ _ - (_) ___ _ __ (_) ___ - | |/ _ \| '_ \| |/ __| - | | (_) | | | | | (__ - |_|\___/|_| |_|_|\___|`; - -export interface NamespaceHelpFormatterDeps extends BaseNamespaceHelpFormatterDeps { - readonly inProject: boolean; - readonly version: string; -} - -export interface CommandHelpFormatterDeps extends BaseCommandHelpFormatterDeps {} - -export class NamespaceStringHelpFormatter extends BaseNamespaceStringHelpFormatter { - protected readonly inProject: boolean; - protected readonly version: string; - - constructor({ version, inProject, ...rest }: NamespaceHelpFormatterDeps) { - super({ ...rest, colors: COLORS }); - this.inProject = inProject; - this.version = version; - } - - async formatHeader(): Promise { - return this.namespace.parent ? super.formatHeader() : this.formatIonicHeader(); - } - - async formatIonicHeader(): Promise { - const { strong } = this.colors; - return `\n${IONIC_LOGO} ${strong(`CLI ${this.version}`)}\n\n`; - } - - async getGlobalOptions(): Promise { - const visibleOptions = await filter(GLOBAL_OPTIONS, async opt => isOptionVisible(opt)); - return visibleOptions.map(opt => formatOptionName(opt, { colors: NO_COLORS, showAliases: false })); - } - - async formatCommands() { - const { strong } = this.colors; - const commands = await this.getCommandMetadataList(); - const globalCmds = commands.filter(cmd => cmd.type === 'global'); - const projectCmds = commands.filter(cmd => cmd.type === 'project'); - - return ( - (await this.formatCommandGroup('Global Commands', globalCmds)) + - (this.inProject ? await this.formatCommandGroup('Project Commands', projectCmds) : `\n ${strong('Project Commands')}:\n\n You are not in a project directory.\n`) - ); - } -} - -export class CommandStringHelpFormatter extends BaseCommandStringHelpFormatter { - constructor(options: CommandHelpFormatterDeps) { - super({ ...options, colors: COLORS }); - } - - async formatOptions(): Promise { - const metadata = await this.getCommandMetadata(); - const options = metadata.options ? metadata.options : []; - - const basicOptions = options.filter(o => !o.groups || !o.groups.includes(MetadataGroup.ADVANCED)); - const advancedOptions = options.filter(o => o.groups && o.groups.includes(MetadataGroup.ADVANCED)); - - return ( - (await this.formatOptionsGroup('Options', basicOptions)) + - (await this.formatOptionsGroup('Advanced Options', advancedOptions)) - ); - } - - async formatBeforeOptionSummary(opt: CommandMetadataOption): Promise { - return (opt.hint ? `${opt.hint} ` : '') + await super.formatBeforeOptionSummary(opt); - } -} - -export class NamespaceSchemaHelpFormatter extends BaseNamespaceSchemaHelpFormatter { - async formatCommand(cmd: HydratedCommandMetadata): Promise { - const { command } = cmd; - - const formatter = new CommandSchemaHelpFormatter({ - location: { path: [...cmd.path], obj: command, args: [] }, - command, - metadata: cmd, - }); - - return { ...await formatter.serialize(), type: cmd.type }; - } -} - -export interface CommandHelpSchema extends BaseCommandHelpSchema { - type: string; -} - -export class CommandSchemaHelpFormatter extends BaseCommandSchemaHelpFormatter { - async formatCommand(cmd: CommandMetadata | HydratedCommandMetadata): Promise { - const formatted = await super.formatCommand(cmd); - - return { ...formatted, type: cmd.type }; - } -} diff --git a/packages/@ionic/cli/src/lib/helper.ts b/packages/@ionic/cli/src/lib/helper.ts deleted file mode 100644 index d082c04a4d..0000000000 --- a/packages/@ionic/cli/src/lib/helper.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { mkdirp, open } from '@ionic/utils-fs'; -import { fork } from '@ionic/utils-subprocess'; -import * as path from 'path'; - -import { IConfig, IPCMessage, IonicContext } from '../definitions'; - -export interface SendMessageDeps { - config: IConfig; - ctx: IonicContext; -} - -export async function sendMessage({ config, ctx }: SendMessageDeps, msg: IPCMessage) { - const dir = path.dirname(config.p); - await mkdirp(dir); - const fd = await open(path.resolve(dir, 'helper.log'), 'a'); - const p = fork(ctx.binPath, ['_', '--no-interactive'], { stdio: ['ignore', fd, fd, 'ipc'] }); - - p.send(msg); - p.disconnect(); - p.unref(); -} diff --git a/packages/@ionic/cli/src/lib/hooks.ts b/packages/@ionic/cli/src/lib/hooks.ts deleted file mode 100644 index 8f9fe41ea1..0000000000 --- a/packages/@ionic/cli/src/lib/hooks.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { conform } from '@ionic/utils-array'; -import { prettyPath } from '@ionic/utils-terminal'; -import { debug as Debug } from 'debug'; -import * as lodash from 'lodash'; -import * as path from 'path'; - -import { HookFn, HookInput, HookName, IConfig, IProject, IShell } from '../definitions'; - -import { ancillary, failure, strong } from './color'; -import { HookException } from './errors'; - -const debug = Debug('ionic:lib:hooks'); - -export interface HookDeps { - readonly config: IConfig; - readonly project: IProject; - readonly shell: IShell; -} - -export abstract class Hook { - abstract readonly name: HookName; - - get script() { - return `ionic:${this.name}`; - } - - constructor(protected readonly e: HookDeps) { } - - async run(input: HookInput) { - const { pkgManagerArgs } = await import('./utils/npm'); - - const type = this.e.project.type; - - if (!type || !this.e.project.directory) { - return; // TODO: will we need hooks outside a project? - } - - const [pkg] = await this.e.project.getPackageJson(undefined, { logErrors: false }); - - if (!pkg) { - return; - } - - debug(`Looking for ${ancillary(this.script)} npm script.`); - - const ctxEnvironment = this.generateCTXEnvironment(input); - - if (pkg.scripts && pkg.scripts[this.script]) { - debug(`Invoking ${ancillary(this.script)} npm script.`); - const [pkgManager, ...pkgArgs] = await pkgManagerArgs(this.e.config.get('npmClient'), { command: 'run', script: this.script }); - await this.e.shell.run(pkgManager, pkgArgs, { - env: ctxEnvironment, - }); - } - - const projectHooks = this.e.project.config.get('hooks'); - const hooks = projectHooks ? conform(projectHooks[this.name]) : []; - - for (const h of hooks) { - const p = path.resolve(this.e.project.directory, h); - - try { - if (path.extname(p) !== '.js') { - throw new Error(`Hooks must be .js files with a function for its default export.`); - } - - const hook = await this.loadHookFn(p); - - if (!hook) { - throw new Error(`Module must have a function for its default export.`); - } - - await hook(lodash.assign({}, input, { - project: { - type, - dir: this.e.project.directory, - srcDir: await this.e.project.getSourceDir(), - }, - argv: process.argv, - env: { - ...process.env, - ...ctxEnvironment, - }, - })); - } catch (e: any) { - throw new HookException( - `An error occurred while running an Ionic CLI hook defined in ${strong(prettyPath(this.e.project.filePath))}.\n` + - `Hook: ${strong(this.name)}\n` + - `File: ${strong(p)}\n\n` + - `${failure(e.stack ? e.stack : e)}` - ); - } - } - } - - protected async loadHookFn(p: string): Promise { - const module = require(p); - - if (typeof module === 'function') { - return module; - } else if (typeof module.default === 'function') { - return module.default; - } - - debug(`Could not load hook function ${strong(p)}: %o not a function`, module); - } - - private generateCTXEnvironment(input: HookInput, path: string[] = []): NodeJS.ProcessEnv { - let environment: NodeJS.ProcessEnv = {}; - - for (const [key, value] of Object.entries(input)) { - if (typeof value === 'object') { - environment = { - ...environment, - ...this.generateCTXEnvironment(value, [...path, key]), - }; - } else { - const name = [...path, key].join('_'); - - environment[`IONIC_CLI_HOOK_CTX_${lodash.snakeCase(name)}`.toUpperCase()] = value; - } - } - - return environment; - } -} - -export function addHook(baseDir: string, hooks: string | string[] | undefined, hook: string): string[] { - const hookPaths = conform(hooks); - const resolvedHookPaths = hookPaths.map(p => path.resolve(baseDir, p)); - - if (!resolvedHookPaths.includes(path.resolve(baseDir, hook))) { - hookPaths.push(hook); - } - - return hookPaths; -} - -export function removeHook(baseDir: string, hooks: string | string[] | undefined, hook: string): string[] { - const hookPaths = conform(hooks); - const i = locateHook(baseDir, hookPaths, hook); - - if (i >= 0) { - hookPaths.splice(i, 1); - } - - return hookPaths; -} - -export function locateHook(baseDir: string, hooks: string[], hook: string): number { - return conform(hooks).map(p => path.resolve(baseDir, p)).indexOf(path.resolve(baseDir, hook)); -} diff --git a/packages/@ionic/cli/src/lib/http.ts b/packages/@ionic/cli/src/lib/http.ts deleted file mode 100644 index 4363110e8e..0000000000 --- a/packages/@ionic/cli/src/lib/http.ts +++ /dev/null @@ -1,286 +0,0 @@ -import chalk from 'chalk'; -import * as lodash from 'lodash'; -import * as util from 'util'; - -import { - APIResponse, - APIResponsePageTokenMeta, - APIResponseSuccess, - ContentType, - HttpMethod, - IClient, - IConfig, - IPaginator, - PagePaginatorState, - PaginateArgs, - PaginatorDeps, - PaginatorGuard, - PaginatorRequestGenerator, - ResourceClientRequestModifiers, - Response, - SuperAgentError, - TokenPaginatorState -} from '../definitions'; -import { isAPIResponseError, isAPIResponseSuccess } from '../guards'; - -import { failure, strong } from './color'; -import { FatalException } from './errors'; -import { createRequest } from './utils/http'; - -export type SuperAgentRequest = import('superagent').SuperAgentRequest; -export type SuperAgentResponse = import('superagent').Response; - -const FORMAT_ERROR_BODY_MAX_LENGTH = 1000; - -export const ERROR_UNKNOWN_CONTENT_TYPE = 'UNKNOWN_CONTENT_TYPE'; -export const ERROR_UNKNOWN_RESPONSE_FORMAT = 'UNKNOWN_RESPONSE_FORMAT'; - -export class Client implements IClient { - constructor(public config: IConfig) { } - - async make(method: HttpMethod, path: string, contentType: ContentType = ContentType.JSON): Promise<{ req: SuperAgentRequest; }> { - const url = path.startsWith('http://') || path.startsWith('https://') ? path : `${this.config.getAPIUrl()}${path}`; - const { req } = await createRequest(method, url, this.config.getHTTPConfig()); - - req - .set('Content-Type', contentType) - .set('Accept', ContentType.JSON); - - return { req }; - } - - async do(req: SuperAgentRequest): Promise { - const res = await req; - const r = transformAPIResponse(res); - - if (isAPIResponseError(r)) { - throw new FatalException( - 'API request was successful, but the response output format was that of an error.\n' + - formatAPIResponse(req, r) - ); - } - - return r; - } - - paginate>(args: PaginateArgs): IPaginator { - return new Paginator({ client: this, ...args }); - } -} - -export class Paginator> implements IPaginator { - protected client: IClient; - protected reqgen: PaginatorRequestGenerator; - protected guard: PaginatorGuard; - protected max?: number; - - readonly state: PagePaginatorState; - - constructor({ client, reqgen, guard, state, max }: PaginatorDeps) { - const defaultState = { page: 1, done: false, loaded: 0 }; - - this.client = client; - this.reqgen = reqgen; - this.guard = guard; - this.max = max; - - if (!state) { - state = { page_size: 100, ...defaultState }; - } - - this.state = lodash.assign({}, state, defaultState); - } - - next(): IteratorResult> { - if (this.state.done) { - return { done: true } as IteratorResult>; // TODO: why can't I exclude value? - } - - return { - done: false, - value: (async () => { - const { req } = await this.reqgen(); - - req.query(lodash.pick(this.state, ['page', 'page_size'])); - - const res = await this.client.do(req); - - if (!this.guard(res)) { - throw createFatalAPIFormat(req, res); - } - - this.state.loaded += res.data.length; - - if ( - res.data.length === 0 || // no resources in this page, we're done - (typeof this.max === 'number' && this.state.loaded >= this.max) || // met or exceeded maximum requested - (typeof this.state.page_size === 'number' && res.data.length < this.state.page_size) // number of resources less than page size, so nothing on next page - ) { - this.state.done = true; - } - - this.state.page++; - - return res; - })(), - }; - } - - [Symbol.iterator](): this { - return this; - } -} - -export class TokenPaginator> implements IPaginator { - protected client: IClient; - protected reqgen: PaginatorRequestGenerator; - protected guard: PaginatorGuard; - protected max?: number; - - readonly state: TokenPaginatorState; - - constructor({ client, reqgen, guard, state, max }: PaginatorDeps) { - const defaultState = { done: false, loaded: 0 }; - - this.client = client; - this.reqgen = reqgen; - this.guard = guard; - this.max = max; - - if (!state) { - state = { ...defaultState }; - } - - this.state = lodash.assign({}, state, defaultState); - } - - next(): IteratorResult> { - if (this.state.done) { - return { done: true } as IteratorResult>; // TODO: why can't I exclude value? - } - - return { - done: false, - value: (async () => { - const { req } = await this.reqgen(); - - if (this.state.page_token) { - req.query({ page_token: this.state.page_token }); - } - - const res = await this.client.do(req); - - if (!this.isPageTokenResponseMeta(res.meta)) { - throw createFatalAPIFormat(req, res); - } - - const nextPageToken = res.meta.next_page_token; - - if (!this.guard(res)) { - throw createFatalAPIFormat(req, res); - } - - this.state.loaded += res.data.length; - - if ( - res.data.length === 0 || // no resources in this page, we're done - (typeof this.max === 'number' && this.state.loaded >= this.max) || // met or exceeded maximum requested - !nextPageToken // no next page token, must be done - ) { - this.state.done = true; - } - - this.state.page_token = nextPageToken; - - return res; - })(), - }; - } - - isPageTokenResponseMeta(meta: any): meta is APIResponsePageTokenMeta { - return meta - && (!meta.prev_page_token || typeof meta.prev_page_token === 'string') - && (!meta.next_page_token || typeof meta.next_page_token === 'string'); - } - - [Symbol.iterator](): this { - return this; - } -} - -export abstract class ResourceClient { - protected applyModifiers(req: import('superagent').Request, modifiers?: ResourceClientRequestModifiers) { - if (!modifiers) { - return; - } - - if (modifiers.fields) { - req.query({ fields: modifiers.fields }); - } - } - - protected applyAuthentication(req: import('superagent').Request, token: string) { - req.set('Authorization', `Bearer ${token}`); - } -} - -export function transformAPIResponse(r: SuperAgentResponse): APIResponse { - if (r.status === 204) { - r.body = { meta: { status: 204, version: '', request_id: '' } }; - } - - if (r.status !== 204 && r.type !== ContentType.JSON) { - throw ERROR_UNKNOWN_CONTENT_TYPE; - } - - const j = r.body as APIResponse; - - if (!j.meta) { - throw ERROR_UNKNOWN_RESPONSE_FORMAT; - } - - return j; -} - -export function createFatalAPIFormat(req: SuperAgentRequest, res: APIResponse): FatalException { - return new FatalException( - 'API request was successful, but the response format was unrecognized.\n' + - formatAPIResponse(req, res) - ); -} - -export function formatSuperAgentError(e: SuperAgentError): string { - const res = e.response; - const req = (res as any).request; // TODO: `req` and `request` exist: https://visionmedia.github.io/superagent/docs/test.html - const statusCode = e.response.status; - - let f = ''; - - try { - const r = transformAPIResponse(res); - f += formatAPIResponse(req, r); - } catch (e: any) { - f += ( - `HTTP Error ${statusCode}: ${req.method.toUpperCase()} ${req.url}\n` + - '\n' + (res.text ? res.text.substring(0, FORMAT_ERROR_BODY_MAX_LENGTH) : '') - ); - - if (res.text && res.text.length > FORMAT_ERROR_BODY_MAX_LENGTH) { - f += ` ...\n\n[ truncated ${res.text.length - FORMAT_ERROR_BODY_MAX_LENGTH} characters ]`; - } - } - - return failure(strong(f)); -} - -function formatAPIResponse(req: SuperAgentRequest, r: APIResponse): string { - return formatResponseError(req, r.meta.status, isAPIResponseSuccess(r) ? r.data : r.error); -} - -export function formatResponseError(req: SuperAgentRequest, status?: number, body?: object | string): string { - return failure( - `Request: ${req.method} ${req.url}\n` + - (status ? `Response: ${status}\n` : '') + - (body ? `Body: \n${util.inspect(body, { colors: chalk.level > 0 })}` : '') - ); -} diff --git a/packages/@ionic/cli/src/lib/index.ts b/packages/@ionic/cli/src/lib/index.ts deleted file mode 100644 index 2d54055fcc..0000000000 --- a/packages/@ionic/cli/src/lib/index.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { LOGGER_LEVELS } from '@ionic/cli-framework-output'; -import { createPromptModule } from '@ionic/cli-framework-prompts'; -import { TERMINAL_INFO, prettyPath } from '@ionic/utils-terminal'; -import { debug as Debug } from 'debug'; -import * as path from 'path'; - -import { ERROR_VERSION_TOO_OLD } from '../bootstrap'; -import { IProject, InfoItem, IonicContext, IonicEnvironment, IonicEnvironmentFlags } from '../definitions'; - -import { input, strong, success } from './color'; -import { CONFIG_FILE, Config, DEFAULT_CONFIG_DIRECTORY, parseGlobalOptions } from './config'; -import { Environment } from './environment'; -import { Client } from './http'; -import { createProjectFromDirectory, findProjectDirectory } from './project'; -import { createOnFallback } from './prompts'; -import { ProSession } from './session'; -import { Shell, prependNodeModulesBinToPath } from './shell'; -import { PROXY_ENVIRONMENT_VARIABLES } from './utils/http'; -import { Logger, createDefaultLoggerHandlers } from './utils/logger'; - -const debug = Debug('ionic:lib'); - -export async function generateIonicEnvironment(ctx: IonicContext, pargv: string[]): Promise<{ env: IonicEnvironment; project?: IProject; }> { - process.chdir(ctx.execPath); - - const argv = parseGlobalOptions(pargv); - const config = new Config(path.resolve(process.env['IONIC_CONFIG_DIRECTORY'] || DEFAULT_CONFIG_DIRECTORY, CONFIG_FILE)); - - debug('Terminal info: %o', TERMINAL_INFO); - - if (config.get('interactive') === false || !TERMINAL_INFO.tty || TERMINAL_INFO.ci) { - argv['interactive'] = false; - } - - const flags = argv as any as IonicEnvironmentFlags; // TODO - debug('CLI global options: %o', flags); - - const log = new Logger({ - level: argv['quiet'] ? LOGGER_LEVELS.WARN : LOGGER_LEVELS.INFO, - handlers: createDefaultLoggerHandlers(), - }); - - const prompt = await createPromptModule({ - interactive: argv['interactive'], - onFallback: createOnFallback({ flags, log }), - }); - - const projectDir = await findProjectDirectory(ctx.execPath); - const proxyVars = PROXY_ENVIRONMENT_VARIABLES.map((e): [string, string | undefined] => [e, process.env[e]]).filter(([, v]) => !!v); - - const getInfo = async () => { - const osName = (await import('os-name')).default; - const semver = await import('semver'); - const { getUpdateConfig } = await import('./updates'); - - const os = osName(); - - const [npm, nativeRun, cordovaRes] = await Promise.all([ - shell.cmdinfo('npm', ['-v']), - shell.cmdinfo('native-run', ['--version']), - shell.cmdinfo('cordova-res', ['--version']), - ]); - - const { packages: latestVersions } = await getUpdateConfig({ config }); - const latestNativeRun = latestVersions.find(pkg => pkg.name === 'native-run'); - const latestCordovaRes = latestVersions.find(pkg => pkg.name === 'cordova-res'); - const nativeRunUpdate = latestNativeRun && nativeRun ? semver.gt(latestNativeRun.version, nativeRun) : false; - const cordovaResUpdate = latestCordovaRes && cordovaRes ? semver.gt(latestCordovaRes.version, cordovaRes) : false; - - const info: InfoItem[] = [ - { - group: 'ionic', - name: 'Ionic CLI', - key: 'version', - value: ctx.version, - path: ctx.libPath, - }, - { group: 'system', name: 'NodeJS', key: 'node_version', value: process.version, path: process.execPath }, - { group: 'system', name: 'npm', key: 'npm_version', value: npm || 'not installed' }, - { group: 'system', name: 'OS', key: 'os', value: os }, - { - group: 'utility', - name: 'native-run', - key: 'native_run_version', - value: nativeRun || 'not installed globally', - flair: nativeRunUpdate ? `update available: ${latestNativeRun ? success(latestNativeRun.version) : '???'}` : '', - }, - { - group: 'utility', - name: 'cordova-res', - key: 'cordova_res_version', - value: cordovaRes || 'not installed globally', - flair: cordovaResUpdate ? `update available: ${latestCordovaRes ? success(latestCordovaRes.version) : '???'}` : '', - }, - ]; - - info.push(...proxyVars.map(([e, v]): InfoItem => ({ group: 'environment', name: e, value: v || 'not set' }))); - - if (project) { - info.push(...(await project.getInfo())); - } - - return info; - }; - - const shell = new Shell({ log }, { alterPath: p => projectDir ? prependNodeModulesBinToPath(projectDir, p) : p }); - const client = new Client(config); - const session = new ProSession({ config, client }); - const deps = { client, config, ctx, flags, log, prompt, session, shell }; - const env = new Environment({ getInfo, ...deps }); - - if (process.env['IONIC_CLI_LOCAL_ERROR']) { - if (process.env['IONIC_CLI_LOCAL_ERROR'] === ERROR_VERSION_TOO_OLD) { - log.warn(`Detected locally installed Ionic CLI, but it's too old--using global CLI.`); - } - } - - if (typeof argv['yarn'] === 'boolean') { - log.warn(`${input('--yarn')} / ${input('--no-yarn')} has been removed. Use ${input(`ionic config set -g npmClient ${argv['yarn'] ? 'yarn' : 'npm'}`)}.`); - } - - const project = projectDir ? await createProjectFromDirectory(projectDir, argv, deps, { logErrors: !['start', 'init'].includes(argv._[0]) }) : undefined; - - if (project) { - shell.alterPath = p => prependNodeModulesBinToPath(project.directory, p); - - if (project.config.get('pro_id' as any) && argv._[1] !== 'unset') { - log.warn( - `The ${input('pro_id')} field in ${strong(prettyPath(project.filePath))} has been deprecated.\n` + - `Ionic Pro has been renamed to Ionic Appflow! We've copied the value in ${input('pro_id')} to ${input('id')}, but you may want to unset the deprecated property: ${input('ionic config unset pro_id')}\n` - ); - } - } - - return { env, project }; -} diff --git a/packages/@ionic/cli/src/lib/integrations/capacitor/android.ts b/packages/@ionic/cli/src/lib/integrations/capacitor/android.ts deleted file mode 100644 index 31d8a3b58d..0000000000 --- a/packages/@ionic/cli/src/lib/integrations/capacitor/android.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { readFile, writeFile, unlink } from '@ionic/utils-fs'; -import * as et from 'elementtree'; - -export const ANDROID_MANIFEST_FILE = 'AndroidManifest.xml'; - -export class CapacitorAndroidManifest { - protected _doc?: et.ElementTree; - protected origManifestContent?: string; - protected saving = false; - - constructor(readonly manifestPath: string) {} - - get origManifestPath(): string { - return `${this.manifestPath}.orig`; - } - - get doc(): et.ElementTree { - if (!this._doc) { - throw new Error('No doc loaded.'); - } - - return this._doc; - } - - static async load(manifestPath: string): Promise { - if (!manifestPath) { - throw new Error(`Must supply file path for ${ANDROID_MANIFEST_FILE}.`); - } - - const conf = new CapacitorAndroidManifest(manifestPath); - await conf.reload(); - - return conf; - } - - protected async reload(): Promise { - this.origManifestContent = await readFile(this.manifestPath, { encoding: 'utf8' }); - - try { - this._doc = et.parse(this.origManifestContent); - } catch (e: any) { - throw new Error(`Cannot parse ${ANDROID_MANIFEST_FILE} file: ${e.stack ?? e}`); - } - } - - enableCleartextTraffic() { - const node = this.getApplicationNode(); - node.set('android:usesCleartextTraffic', 'true'); - } - - async reset(): Promise { - const origManifestContent = await readFile(this.origManifestPath, { encoding: 'utf8' }); - - if (!this.saving) { - this.saving = true; - await writeFile(this.manifestPath, origManifestContent, { encoding: 'utf8' }); - await unlink(this.origManifestPath); - this.saving = false; - } - } - - async save(): Promise { - if (!this.saving) { - this.saving = true; - - if (this.origManifestContent) { - await writeFile(this.origManifestPath, this.origManifestContent, { encoding: 'utf8' }); - this.origManifestContent = undefined; - } - - await writeFile(this.manifestPath, this.write(), { encoding: 'utf8' }); - - this.saving = false; - } - } - - protected getApplicationNode(): et.Element { - const root = this.doc.getroot(); - const applicationNode = root.find('application'); - - if (!applicationNode) { - throw new Error(`No node in ${ANDROID_MANIFEST_FILE}.`); - } - - return applicationNode; - } - - protected write(): string { - const contents = this.doc.write({ indent: 4 }); - - return contents; - } -} diff --git a/packages/@ionic/cli/src/lib/integrations/capacitor/config.ts b/packages/@ionic/cli/src/lib/integrations/capacitor/config.ts deleted file mode 100644 index 8cb98b21ce..0000000000 --- a/packages/@ionic/cli/src/lib/integrations/capacitor/config.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { BaseConfig } from '@ionic/cli-framework'; -import * as lodash from 'lodash'; - -export const CAPACITOR_CONFIG_JSON_FILE = 'capacitor.config.json'; - -export interface CapacitorConfig { - appId?: string; - appName?: string; - webDir?: string; - server?: { - url?: string; - originalUrl?: string; - }; -} - -export class CapacitorJSONConfig extends BaseConfig { - constructor(p: string) { - super(p, { spaces: '\t' }); - } - - provideDefaults(config: CapacitorConfig): CapacitorConfig { - return config; - } - - setServerUrl(url: string): void { - const serverConfig = this.get('server') || {}; - - if (typeof serverConfig.url === 'string') { - serverConfig.originalUrl = serverConfig.url; - } - - serverConfig.url = url; - - this.set('server', serverConfig); - } - - resetServerUrl(): void { - const serverConfig = this.get('server') || {}; - - delete serverConfig.url; - - if (serverConfig.originalUrl) { - serverConfig.url = serverConfig.originalUrl; - delete serverConfig.originalUrl; - } - - if (lodash.isEmpty(serverConfig)) { - this.unset('server'); - } else { - this.set('server', serverConfig); - } - } -} diff --git a/packages/@ionic/cli/src/lib/integrations/capacitor/index.ts b/packages/@ionic/cli/src/lib/integrations/capacitor/index.ts deleted file mode 100644 index 1fbc6ed3c3..0000000000 --- a/packages/@ionic/cli/src/lib/integrations/capacitor/index.ts +++ /dev/null @@ -1,256 +0,0 @@ -import { parseArgs } from '@ionic/cli-framework'; -import { mkdirp, pathExists } from '@ionic/utils-fs'; -import { prettyPath } from '@ionic/utils-terminal'; -import chalk from 'chalk'; -import { debug as Debug } from 'debug'; -import * as lodash from 'lodash'; -import * as path from 'path'; - -const debug = Debug('ionic:lib:integrations:capacitor'); - -import { BaseIntegration, IntegrationConfig } from '../'; -import { - InfoItem, - IntegrationAddDetails, - IntegrationName, - IShellSpawnOptions, - ProjectIntegration, - ProjectPersonalizationDetails -} from '../../../definitions'; -import { input, strong } from '../../color'; -import { pkgManagerArgs } from '../../utils/npm'; - -import { CapacitorJSONConfig, CapacitorConfig } from './config'; - -export interface CapacitorCLIConfig { - android: { - platformDirAbs: string; - srcMainDirAbs: string; - assetsDirAbs: string; - }; - ios: { - platformDirAbs: string; - nativeTargetDirAbs: string; - }; - app: { - extConfig: CapacitorConfig; - } -} - -export class Integration extends BaseIntegration { - readonly name: IntegrationName = 'capacitor'; - readonly summary = `Target native iOS and Android with Capacitor, Ionic's new native layer`; - readonly archiveUrl = undefined; - - get config(): IntegrationConfig { - return new IntegrationConfig(this.e.project.filePath, { pathPrefix: [...this.e.project.pathPrefix, 'integrations', this.name] }); - } - - get root(): string { - return this.config.get('root', this.e.project.directory); - } - - async add(details: IntegrationAddDetails): Promise { - const confPath = this.getCapacitorConfigJsonPath(); - - if (await pathExists(confPath)) { - this.e.log.nl(); - this.e.log.warn( - `Capacitor already exists in project.\n` + - `Since the Capacitor config already exists (${strong(prettyPath(confPath))}), the Capacitor integration has been ${chalk.green('enabled')}.\n\n` + - `You can re-integrate this project by doing the following:\n\n` + - `- Run ${input(`ionic integrations disable ${this.name}`)}\n` + - `- Remove the ${strong(prettyPath(confPath))} file\n` + - `- Run ${input(`ionic integrations enable ${this.name} --add`)}\n` - ); - } else { - let name = this.e.project.config.get('name'); - let packageId = 'io.ionic.starter'; - let webDir = await this.e.project.getDefaultDistDir(); - - const options: string[] = []; - - if (details.enableArgs && details.enableArgs.length > 0) { - const parsedArgs = parseArgs(details.enableArgs); - - name = String(parsedArgs._[0]) || name; - packageId = parsedArgs._[1] || packageId; - - if (parsedArgs['web-dir']) { - webDir = parsedArgs['web-dir']; - } - } - - options.push('--web-dir', webDir); - - await this.installCapacitorCore(); - await this.installCapacitorCLI(); - await this.installCapacitorPlugins() - - await mkdirp(details.root); - await this.e.shell.run('capacitor', ['init', name, packageId, ...options], { cwd: details.root }); - } - - await super.add(details); - } - - protected getCapacitorConfigJsonPath(): string { - return path.resolve(this.root, 'capacitor.config.json'); - } - - async installCapacitorCore() { - const [manager, ...managerArgs] = await pkgManagerArgs(this.e.config.get('npmClient'), { command: 'install', pkg: '@capacitor/core@latest' }); - await this.e.shell.run(manager, managerArgs, { cwd: this.root }); - } - - async installCapacitorCLI() { - const [manager, ...managerArgs] = await pkgManagerArgs(this.e.config.get('npmClient'), { command: 'install', pkg: '@capacitor/cli@latest', saveDev: true }); - await this.e.shell.run(manager, managerArgs, { cwd: this.root }); - } - - async installCapacitorPlugins() { - const [manager, ...managerArgs] = await pkgManagerArgs(this.e.config.get('npmClient'), { command: 'install', pkg: ['@capacitor/haptics', '@capacitor/app', '@capacitor/keyboard', '@capacitor/status-bar'] }); - await this.e.shell.run(manager, managerArgs, { cwd: this.root }); - } - - async personalize({ name, packageId }: ProjectPersonalizationDetails) { - const confPath = this.getCapacitorConfigJsonPath(); - if (await pathExists(confPath)) { - const conf = new CapacitorJSONConfig(confPath); - - conf.set('appName', name); - - if (packageId) { - conf.set('appId', packageId); - } - } - } - - async getInfo(): Promise { - const conf = await this.getCapacitorConfig(); - const bundleId = conf?.appId; - - const [ - [capacitorCorePkg, capacitorCorePkgPath], - capacitorCLIVersion, - [capacitorIOSPkg, capacitorIOSPkgPath], - [capacitorAndroidPkg, capacitorAndroidPkgPath], - ] = await (Promise.all([ - this.e.project.getPackageJson('@capacitor/core'), - this.getCapacitorCLIVersion(), - this.e.project.getPackageJson('@capacitor/ios'), - this.e.project.getPackageJson('@capacitor/android'), - ])); - - const info: InfoItem[] = [ - { - group: 'capacitor', - name: 'Capacitor CLI', - key: 'capacitor_cli_version', - value: capacitorCLIVersion || 'not installed', - }, - { - group: 'capacitor', - name: '@capacitor/core', - key: 'capacitor_core_version', - value: capacitorCorePkg ? capacitorCorePkg.version : 'not installed', - path: capacitorCorePkgPath, - }, - { - group: 'capacitor', - name: '@capacitor/ios', - key: 'capacitor_ios_version', - value: capacitorIOSPkg ? capacitorIOSPkg.version : 'not installed', - path: capacitorIOSPkgPath, - }, - { - group: 'capacitor', - name: '@capacitor/android', - key: 'capacitor_android_version', - value: capacitorAndroidPkg ? capacitorAndroidPkg.version : 'not installed', - path: capacitorAndroidPkgPath, - }, - { - group: 'capacitor', - name: 'Bundle ID', - key: 'bundle_id', - value: bundleId || 'unknown', - hidden: true, - }, - ]; - - return info; - } - - getCapacitorCLIVersion = lodash.memoize(async (): Promise => { - return this.e.shell.cmdinfo('capacitor', ['--version'], { cwd: this.root }); - }); - - getCapacitorCLIConfig = lodash.memoize(async (): Promise => { - const args = ['config', '--json']; - - debug('Getting config with Capacitor CLI: %O', args); - - let output = undefined; - try { - output = await (async (_command: string, _args: readonly string[] = [], opts: IShellSpawnOptions = {}) => { - try { - const proc = await this.e.shell.createSubprocess(_command, _args, opts); - const out = await proc.output(); - return out.split('\n').join(' ').trim(); - } catch (err) { - throw err; - } - })('capacitor', args, { cwd: this.root }) - } catch (error) { - if ((error as any).code === 'ERR_SUBPROCESS_COMMAND_NOT_FOUND') { - throw new Error(`Capacitor command not found. Is the Capacitor CLI installed? (npm i -D @capacitor/cli)`); - } else { - throw new Error((error as any).message); - } - } - - - if (!output) { - debug('Could not get config from Capacitor CLI (probably old version)'); - return; - } else { - try { - // Capacitor 1 returns the `command not found` error in stdout instead of stderror like in Capacitor 2 - // This ensures that the output from the command is valid JSON to account for this - return JSON.parse(output); - } catch (e) { - debug('Could not get config from Capacitor CLI (probably old version)', e); - return; - } - } - }); - - getCapacitorConfig = lodash.memoize(async (): Promise => { - try { - const cli = await this.getCapacitorCLIConfig(); - if (cli) { - debug('Loaded Capacitor config!'); - return cli.app.extConfig; - } - } catch (ex) { - // ignore - } - - // fallback to reading capacitor.config.json if it exists - const confPath = this.getCapacitorConfigJsonPath(); - - if (!(await pathExists(confPath))) { - debug('Capacitor config file does not exist at %O', confPath); - debug('Failed to load Capacitor config'); - return; - } - - const conf = new CapacitorJSONConfig(confPath); - const extConfig = conf.c; - - debug('Loaded Capacitor config!'); - - return extConfig; - }); -} diff --git a/packages/@ionic/cli/src/lib/integrations/capacitor/ios.ts b/packages/@ionic/cli/src/lib/integrations/capacitor/ios.ts deleted file mode 100644 index a245690726..0000000000 --- a/packages/@ionic/cli/src/lib/integrations/capacitor/ios.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { readFile, writeFile, unlink } from "@ionic/utils-fs"; -import * as et from "elementtree"; - -export const IOS_INFO_FILE = "Info.plist"; - -export class CapacitorIosInfo { - protected _doc?: et.ElementTree; - protected origInfoPlistContent?: string; - protected saving = false; - - constructor(readonly plistPath: string) {} - - get origPlistPath(): string { - return `${this.plistPath}.orig`; - } - - get doc(): et.ElementTree { - if (!this._doc) { - throw new Error("No doc loaded."); - } - - return this._doc; - } - - static async load(plistPath: string): Promise { - if (!plistPath) { - throw new Error(`Must supply file path for ${IOS_INFO_FILE}.`); - } - - const conf = new CapacitorIosInfo(plistPath); - await conf.reload(); - - return conf; - } - - disableAppTransportSecurity() { - const rootDict = this.getDictRoot(); - - let valueDict = this.getValueForKey(rootDict,"NSAppTransportSecurity"); - if (valueDict) { - const value = this.getValueForKey(valueDict, "NSAllowsArbitraryLoads") - if (value) { - value.tag = "true" - } else { - et.SubElement(valueDict, "true") - } - } else { - const newKey = et.SubElement(rootDict, "key"); - newKey.text = "NSAppTransportSecurity"; - - const newDict = et.SubElement(rootDict, "dict"); - const newDictKey = et.SubElement(newDict, "key") - newDictKey.text = "NSAllowsArbitraryLoads"; - et.SubElement(newDict, "true") - } - } - - - private getValueForKey(root: et.Element, key: string): et.Element | null { - const children = root.getchildren(); - let keyFound = false; - - for (const element of children) { - if (keyFound) { - keyFound = false; - - return element; - } - - if ((element.tag === 'key') && element.text === key) { - keyFound = true; - } - } - - return null; - } - - private getDictRoot(): et.Element { - const root = this.doc.getroot(); - - if (root.tag !== "plist") { - throw new Error(`Info.plist is not a valid plist file because the root is not a tag`); - } - - const rootDict = root.find('./dict'); - if (!rootDict) { - throw new Error(`Info.plist is not a valid plist file because the first child is not a tag`); - } - - return rootDict; - } - - async reset(): Promise { - const origInfoPlistContent = await readFile(this.origPlistPath, { - encoding: "utf8", - }); - - if (!this.saving) { - this.saving = true; - await writeFile(this.plistPath, origInfoPlistContent, { - encoding: "utf8", - }); - await unlink(this.origPlistPath); - this.saving = false; - } - } - - async save(): Promise { - if (!this.saving) { - this.saving = true; - - if (this.origInfoPlistContent) { - await writeFile(this.origPlistPath, this.origInfoPlistContent, { - encoding: "utf8", - }); - this.origInfoPlistContent = undefined; - } - - await writeFile(this.plistPath, this.write(), { encoding: "utf8" }); - - this.saving = false; - } - } - - protected async reload(): Promise { - this.origInfoPlistContent = await readFile(this.plistPath, { - encoding: "utf8", - }); - - try { - this._doc = et.parse(this.origInfoPlistContent); - } catch (e: any) { - throw new Error(`Cannot parse ${IOS_INFO_FILE} file: ${e.stack ?? e}`); - } - } - - protected write(): string { - const contents = this.doc.write({ indent: 4 }); - - return contents; - } -} diff --git a/packages/@ionic/cli/src/lib/integrations/capacitor/utils.ts b/packages/@ionic/cli/src/lib/integrations/capacitor/utils.ts deleted file mode 100644 index 08bb6a3a1a..0000000000 --- a/packages/@ionic/cli/src/lib/integrations/capacitor/utils.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { CommandLineInputs, CommandLineOptions } from '../../../definitions'; - -export function generateOptionsForCapacitorBuild(inputs: CommandLineInputs, options: CommandLineOptions): CommandLineOptions { - const [ platform ] = inputs; - - return { - ...options, - externalAddressRequired: true, - open: false, - engine: 'capacitor', - platform: platform ? platform : (options['platform'] ? String(options['platform']) : undefined), - }; -} - -export function getNativeIDEForPlatform(platform: string): string { - switch (platform) { - case 'ios': - return 'Xcode'; - case 'android': - return 'Android Studio'; - } - - return 'Native IDE'; -} - -export function getVirtualDeviceNameForPlatform(platform: string): string { - switch (platform) { - case 'ios': - return 'simulator'; - case 'android': - return 'emulator'; - } - - return 'virtual device'; -} diff --git a/packages/@ionic/cli/src/lib/integrations/cordova/__tests__/android.ts b/packages/@ionic/cli/src/lib/integrations/cordova/__tests__/android.ts deleted file mode 100644 index 0ef559204f..0000000000 --- a/packages/@ionic/cli/src/lib/integrations/cordova/__tests__/android.ts +++ /dev/null @@ -1,59 +0,0 @@ -import * as android from '../android'; - -describe('@ionic/cli', () => { - - describe('lib/android', () => { - - describe('getAndroidSdkToolsVersion', () => { - - beforeEach(() => { - jest.resetModules(); - }); - - it('should return undefined for a missing source.properties file', async () => { - jest.mock('@ionic/utils-fs', () => ({ - readFile: () => { - const err: NodeJS.ErrnoException = new Error(); - err.code = 'ENOENT'; - return Promise.reject(err); - }, - })); - - const android = require('../android'); - const result = await android.getAndroidSdkToolsVersion(); - expect(result).toEqual(undefined); - }); - - }); - - describe('parseSDKVersion', () => { - - it('should parse sdk tools version from source.properties file', async () => { - const contents = ` -Pkg.UserSrc=false - -Pkg.Revision=26.0.2 -Platform.MinPlatformToolsRev=20 -Pkg.Dependencies=emulator -Pkg.Path=tools -Pkg.Desc=Android SDK Tools -`; - - const result = await android.parseSDKVersion(contents); - expect(result).toEqual('26.0.2'); - }); - - it('should return undefined for a missing Pkg.Revision entry in source.properties file', async () => { - const contents = ` -some file in some garbage format -`; - - const result = await android.parseSDKVersion(contents); - expect(result).toEqual(undefined); - }); - - }); - - }); - -}); diff --git a/packages/@ionic/cli/src/lib/integrations/cordova/__tests__/fixtures/icon.png b/packages/@ionic/cli/src/lib/integrations/cordova/__tests__/fixtures/icon.png deleted file mode 100644 index ca014e23f7..0000000000 Binary files a/packages/@ionic/cli/src/lib/integrations/cordova/__tests__/fixtures/icon.png and /dev/null differ diff --git a/packages/@ionic/cli/src/lib/integrations/cordova/__tests__/project.ts b/packages/@ionic/cli/src/lib/integrations/cordova/__tests__/project.ts deleted file mode 100644 index 34c1a8f211..0000000000 --- a/packages/@ionic/cli/src/lib/integrations/cordova/__tests__/project.ts +++ /dev/null @@ -1,125 +0,0 @@ -import fsExtraSpy from 'fs-extra'; -import * as fsSafeSpy from '@ionic/utils-fs/dist/safe'; -import * as project from '../project'; - -describe('@ionic/cli', () => { - - describe('lib/cordova/project', () => { - - describe('getPlatforms', () => { - - it('should filter out files and hidden files/folders', async () => { - const files: [string, boolean][] = [['.DS_Store', false], ['.dir', true], ['android', true], ['ios', true], ['platforms.json', false]]; - jest.spyOn(fsSafeSpy, 'readdir').mockImplementation(async () => files.map(([f]) => f)); - jest.spyOn(fsSafeSpy, 'stat').mockImplementation(async (p) => ({ - isDirectory: () => { - const file = files.find(([f]) => p.endsWith(f)); - return typeof file !== 'undefined' && file[1]; - }, - } as any)); - - const platforms = await project.getPlatforms('/path/to/proj'); - expect(platforms).toEqual(['android', 'ios']); - }); - - it('should not error if directory empty', async () => { - jest.spyOn(fsSafeSpy, 'readdir').mockImplementation(async () => []); - const platforms = await project.getPlatforms('/path/to/proj'); - expect(platforms).toEqual([]); - }); - - }); - - describe('getAndroidBuildOutputJson', () => { - - it('should throw for fs error', () => { - jest.spyOn(fsExtraSpy, 'readJson').mockImplementation(async () => { throw new Error('error') }); - - const p = project.getAndroidBuildOutputJson(['/path/to/output.json']); - expect(p).rejects.toThrowError('Could not find or parse valid build output file'); - }); - - it('should throw for unrecognized format', () => { - jest.spyOn(fsExtraSpy, 'readJson').mockImplementation(async () => ({ foo: 'bar' })); - - const p = project.getAndroidBuildOutputJson(['/path/to/output.json']); - expect(p).rejects.toThrowError('Could not find or parse valid build output file'); - }); - - it('should parse legacy output.json', () => { - const file = [ - { - outputType: { - type: 'APK', - }, - path: 'app-debug.apk', - }, - ]; - - jest.spyOn(fsExtraSpy, 'readJson').mockImplementation(async () => file); - - const p = project.getAndroidBuildOutputJson(['/path/to/output.json']); - expect(p).resolves.toEqual(file); - }); - - it('should parse output.json', () => { - const file = { - artifactType: { - type: 'APK', - }, - elements: [ - { - outputFile: 'app-debug.apk', - } - ], - }; - - jest.spyOn(fsExtraSpy, 'readJson').mockImplementation(async () => file); - - const p = project.getAndroidBuildOutputJson(['/path/to/output.json']); - expect(p).resolves.toEqual(file); - }); - - }); - - describe('getAndroidPackageFilePath', () => { - - it('should get file path from legacy output.json', () => { - const file = [ - { - outputType: { - type: 'APK', - }, - path: 'foo-debug.apk', - }, - ]; - - jest.spyOn(fsExtraSpy, 'readJson').mockImplementation(async () => file); - - const p = project.getAndroidPackageFilePath('/path/to/proj', { release: false }); - expect(p).resolves.toEqual('platforms/android/app/build/outputs/apk/debug/foo-debug.apk'); - }); - - it('should get file path from output.json', () => { - const file = { - artifactType: { - type: 'APK', - }, - elements: [ - { - outputFile: 'bar-debug.apk', - } - ], - }; - - jest.spyOn(fsExtraSpy, 'readJson').mockImplementation(async () => file); - - const p = project.getAndroidPackageFilePath('/path/to/proj', { release: false }); - expect(p).resolves.toEqual('platforms/android/app/build/outputs/apk/debug/bar-debug.apk'); - }); - - }); - - }); - -}); diff --git a/packages/@ionic/cli/src/lib/integrations/cordova/__tests__/utils.ts b/packages/@ionic/cli/src/lib/integrations/cordova/__tests__/utils.ts deleted file mode 100644 index 1ba7bf1890..0000000000 --- a/packages/@ionic/cli/src/lib/integrations/cordova/__tests__/utils.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { filterArgumentsForCordova, generateOptionsForCordovaBuild } from '../utils'; -import { CommandMetadata } from '../../../../definitions'; - -describe('@ionic/cli', () => { - - const buildMetadata: CommandMetadata = { - name: 'build', - summary: '', - type: 'global', - inputs: [ - { - name: 'platform', - summary: '', - } - ], - options: [ - { - name: 'boolopt', - summary: '', - type: Boolean, - default: false, - }, - { - name: 'cdvopt1', - summary: '', - groups: ['cordova-cli'], - }, - { - name: 'cdvopt2', - summary: '', - type: Boolean, - groups: ['cordova-cli'], - }, - { - name: 'prod', - summary: '', - type: Boolean, - groups: ['app-scripts'], - }, - { - name: 'optimizejs', - summary: '', - type: Boolean, - groups: ['app-scripts'], - }, - ] - }; - - const platformMetadata: CommandMetadata = { - name: 'platform', - summary: '', - type: 'global', - inputs: [ - { - name: 'action', - summary: '', - }, - { - name: 'platform', - summary: '', - } - ], - options: [ - { - name: 'boolopt', - summary: '', - type: Boolean, - default: false, - }, - ] - }; - - describe('filterArgumentsForCordova', () => { - - it('should return the command name and inputs if no options passed', () => { - const options = { _: ['ios'], boolopt: false, cdvopt1: null, cdvopt2: false, prod: true, optimizejs: true }; - const result = filterArgumentsForCordova(buildMetadata, options); - expect(result).toEqual(['build', 'ios']); - }); - - it('should only include options with the Cordova intent', () => { - const options = { _: ['ios'], boolopt: true, cdvopt1: 'foo', cdvopt2: true, prod: true, optimizejs: true }; - const result = filterArgumentsForCordova(buildMetadata, options); - expect(result).toEqual(['build', 'ios', '--cdvopt1', 'foo', '--cdvopt2']); - }); - - it('should include --verbose', () => { - const options = { _: ['ios'], boolopt: true, cdvopt1: 'foo', cdvopt2: true, prod: true, optimizejs: true, verbose: true }; - const result = filterArgumentsForCordova(buildMetadata, options); - expect(result).toEqual(['build', 'ios', '--cdvopt1', 'foo', '--cdvopt2', '--verbose']); - }); - - it('should include additional options', () => { - const options = { _: ['android'], '--': ['--someopt'], boolopt: true, cdvopt1: 'foo', cdvopt2: true, prod: true, optimizejs: true }; - const result = filterArgumentsForCordova(buildMetadata, options); - expect(result).toEqual(['build', 'android', '--cdvopt1', 'foo', '--cdvopt2', '--someopt']); - }); - - it('should include additional and unparsed options', () => { - const options = { _: ['android'], '--': ['--someopt', '--', '--gradleArg=-PcdvBuildMultipleApks=true'], boolopt: true, cdvopt1: 'foo', cdvopt2: true, prod: true, optimizejs: true }; - const result = filterArgumentsForCordova(buildMetadata, options); - expect(result).toEqual(['build', 'android', '--cdvopt1', 'foo', '--cdvopt2', '--someopt', '--', '--gradleArg=-PcdvBuildMultipleApks=true']); - }); - - it('should respect --nosave', () => { - const options = { _: ['add', 'android'], '--': [], nosave: true }; - const result = filterArgumentsForCordova(platformMetadata, options); - expect(result).toEqual(['platform', 'add', 'android', '--nosave']); - }); - - }); - - describe('generateOptionsForCordovaBuild', () => { - - it('should return added options even for no options passed', () => { - const inputs = ['ios']; - const options = { _: [] }; - - const result = generateOptionsForCordovaBuild(buildMetadata, inputs, options); - expect(result).toEqual({ '_': [], externalAddressRequired: true, open: false, engine: 'cordova', platform: 'ios' }); - }); - - it('should include the options with app-scripts group or no group, but not cordova-cli group', () => { - const inputs = ['ios']; - const options = { _: [], boolopt: false, cdvopt1: null, cdvopt2: false, prod: true, optimizejs: true }; - - const result = generateOptionsForCordovaBuild(buildMetadata, inputs, options); - expect(result).toEqual({ '_': [], boolopt: false, externalAddressRequired: true, open: false, engine: 'cordova', platform: 'ios', prod: true, optimizejs: true }); - }); - - }); - -}); diff --git a/packages/@ionic/cli/src/lib/integrations/cordova/android.ts b/packages/@ionic/cli/src/lib/integrations/cordova/android.ts deleted file mode 100644 index cedf632619..0000000000 --- a/packages/@ionic/cli/src/lib/integrations/cordova/android.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { readFile } from '@ionic/utils-fs'; -import * as path from 'path'; - -export async function getAndroidSdkToolsVersion(): Promise { - const androidHome = await locateSDKHome(); - - if (androidHome) { - try { - const f = await readFile(path.join(androidHome, 'tools', 'source.properties'), { encoding: 'utf8' }); - return `${await parseSDKVersion(f)} (${androidHome})`; - } catch (e: any) { - if (e.code !== 'ENOENT') { - throw e; - } - } - } -} - -export async function locateSDKHome(): Promise { - return process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT; -} - -export async function parseSDKVersion(contents: string): Promise { - for (const l of contents.split('\n')) { - const [ a, b ] = l.split('='); - if (a === 'Pkg.Revision') { - return b; - } - } -} diff --git a/packages/@ionic/cli/src/lib/integrations/cordova/config.ts b/packages/@ionic/cli/src/lib/integrations/cordova/config.ts deleted file mode 100644 index c342061cee..0000000000 --- a/packages/@ionic/cli/src/lib/integrations/cordova/config.ts +++ /dev/null @@ -1,252 +0,0 @@ -import { readPackageJsonFile } from '@ionic/cli-framework/utils/node'; -import { readFile, writeFile } from '@ionic/utils-fs'; -import { prettyPath } from '@ionic/utils-terminal'; -import { debug as Debug } from 'debug'; -import * as et from 'elementtree'; -import * as path from 'path'; - -import { CordovaPackageJson, ProjectIntegration } from '../../../definitions'; -import { isCordovaPackageJson } from '../../../guards'; -import { failure, input, strong } from '../../color'; -import { FatalException } from '../../errors'; -import { shortid } from '../../utils/uuid'; - -const debug = Debug('ionic:lib:integrations:cordova:config'); - -export interface ConfiguredPlatform { - name: string; - spec?: string; -} - -export class CordovaConfig { - protected _doc?: et.ElementTree; - protected _pkg?: CordovaPackageJson; - protected _sessionid?: string; - protected saving = false; - - constructor(readonly configXmlPath: string, readonly packageJsonPath: string) { } - - get doc(): et.ElementTree { - if (!this._doc) { - throw new Error('No doc loaded.'); - } - - return this._doc; - } - - get pkg(): CordovaPackageJson { - if (!this._pkg) { - throw new Error('No package.json loaded.'); - } - - return this._pkg; - } - - get sessionid(): string { - if (!this._sessionid) { - throw new Error('No doc loaded.'); - } - - return this._sessionid; - } - - static async load(configXmlPath: string, packageJsonPath: string): Promise { - if (!configXmlPath || !packageJsonPath) { - throw new Error('Must supply file paths for config.xml and package.json.'); - } - - const conf = new CordovaConfig(configXmlPath, packageJsonPath); - await conf.reload(); - - return conf; - } - - protected async reload(): Promise { - const configXml = await readFile(this.configXmlPath, { encoding: 'utf8' }); - - if (!configXml) { - throw new Error(`Cannot load empty config.xml file.`); - } - - try { - this._doc = et.parse(configXml); - this._sessionid = shortid(); - } catch (e: any) { - throw new Error(`Cannot parse config.xml file: ${e.stack ? e.stack : e}`); - } - - const packageJson = await readPackageJsonFile(this.packageJsonPath); - - if (isCordovaPackageJson(packageJson)) { - this._pkg = packageJson; - } else { - this._pkg = { ...packageJson, cordova: { platforms: [], plugins: {} } }; - debug('Invalid package.json for Cordova. Missing or invalid Cordova entries in %O', this.packageJsonPath); - } - } - - async save(): Promise { - if (!this.saving) { - this.saving = true; - await writeFile(this.configXmlPath, this.write(), { encoding: 'utf8' }); - this.saving = false; - } - } - - setName(name: string): void { - const root = this.doc.getroot(); - let nameNode = root.find('name'); - - if (!nameNode) { - nameNode = et.SubElement(root, 'name', {}); - } - - nameNode.text = name; - } - - setBundleId(bundleId: string): void { - const root = this.doc.getroot(); - root.set('id', bundleId); - } - - getBundleId(): string | undefined { - const root = this.doc.getroot(); - return root.get('id'); - } - - /** - * Update config.xml content src to be a dev server url. As part of this - * backup the original content src for a reset to occur at a later time. - */ - writeContentSrc(newSrc: string): void { - const root = this.doc.getroot(); - let contentElement = root.find('content'); - - if (!contentElement) { - contentElement = et.SubElement(root, 'content', { src: 'index.html' }); - } - - contentElement.set('original-src', contentElement.get('src')); - contentElement.set('src', newSrc); - - let navElement = root.find(`allow-navigation[@href='${newSrc}']`); - - if (!navElement) { - navElement = et.SubElement(root, 'allow-navigation', { sessionid: this.sessionid, href: newSrc }); - } - } - - /** - * Set config.xml src url back to its original url - */ - resetContentSrc() { - const root = this.doc.getroot(); - let contentElement = root.find('content'); - - if (!contentElement) { - contentElement = et.SubElement(root, 'content', { src: 'index.html' }); - } - - const originalSrc = contentElement.get('original-src'); - - if (originalSrc) { - contentElement.set('src', originalSrc); - delete contentElement.attrib['original-src']; - } - - const navElements = root.findall(`allow-navigation[@sessionid='${this.sessionid}']`); - - for (const navElement of navElements) { - root.remove(navElement); - } - } - - getPreference(prefName: string): string | undefined { - const root = this.doc.getroot(); - - const preferenceElement = root.find(`preference[@name='${prefName}']`); - - if (!preferenceElement) { - return undefined; - } - - const value = preferenceElement.get('value'); - - if (!value) { - return undefined; - } - - return value; - } - - getProjectInfo(): { id: string; name: string; version: string; } { - const root = this.doc.getroot(); - - let id = root.get('id'); - - if (!id) { - id = ''; - } - - let version = root.get('version'); - - if (!version) { - version = ''; - } - - let nameElement = root.find('name'); - - if (!nameElement) { - nameElement = et.SubElement(root, 'name', {}); - } - - if (!nameElement.text) { - nameElement.text = 'MyApp'; - } - - const name = nameElement.text.toString().trim(); - - return { id, name, version }; - } - - getConfiguredPlatforms(): ConfiguredPlatform[] { - const deps: { [key: string]: string | undefined; } = { ...this.pkg.devDependencies, ...this.pkg.dependencies }; - - return this.pkg.cordova.platforms.map(platform => ({ - name: platform, - spec: deps[`cordova-${platform}`], - })); - } - - protected write(): string { - // Cordova hard codes an indentation of 4 spaces, so we'll follow. - const contents = this.doc.write({ indent: 4 }); - - return contents; - } -} - -export async function loadCordovaConfig(integration: Required): Promise { - const configXmlPath = path.resolve(integration.root, 'config.xml'); - const packageJsonPath = path.resolve(integration.root, 'package.json'); - - debug('Loading Cordova Config (config.xml: %O, package.json: %O)', configXmlPath, packageJsonPath); - - try { - return await CordovaConfig.load(configXmlPath, packageJsonPath); - } catch (e: any) { - const msg = e.code === 'ENOENT' - ? ( - `Could not find necessary file(s): ${strong('config.xml')}, ${strong('package.json')}.\n\n` + - ` - ${strong(prettyPath(configXmlPath))}\n` + - ` - ${strong(prettyPath(packageJsonPath))}\n\n` + - `You can re-add the Cordova integration with the following command: ${input('ionic integrations enable cordova --add')}` - ) - : failure(e.stack ? e.stack : e); - - throw new FatalException( - `Cannot load Cordova config.\n` + - `${msg}` - ); - } -} diff --git a/packages/@ionic/cli/src/lib/integrations/cordova/index.ts b/packages/@ionic/cli/src/lib/integrations/cordova/index.ts deleted file mode 100644 index 76318cc29b..0000000000 --- a/packages/@ionic/cli/src/lib/integrations/cordova/index.ts +++ /dev/null @@ -1,281 +0,0 @@ -import { copy, mkdirp, pathExists, readdirSafe, remove, stat } from '@ionic/utils-fs'; -import chalk from 'chalk'; -import { debug as Debug } from 'debug'; -import * as lodash from 'lodash'; -import * as os from 'os'; -import * as path from 'path'; - -import { BaseIntegration, IntegrationConfig } from '../'; -import { InfoItem, IntegrationAddDetails, IntegrationAddHandlers, IntegrationName, ProjectIntegration, ProjectPersonalizationDetails } from '../../../definitions'; -import { ancillary, input, strong } from '../../color'; - -import * as configlib from './config'; -import { checkForUnsupportedProject } from './utils'; - -const debug = Debug('ionic:lib:integrations:cordova'); - -export class Integration extends BaseIntegration { - readonly name: IntegrationName = 'cordova'; - readonly summary = 'Target native iOS and Android with Apache Cordova'; - readonly archiveUrl = 'https://d2ql0qc7j8u4b2.cloudfront.net/integration-cordova.tar.gz'; - - get config(): IntegrationConfig { - return new IntegrationConfig(this.e.project.filePath, { pathPrefix: [...this.e.project.pathPrefix, 'integrations', this.name] }); - } - - async add(details: IntegrationAddDetails): Promise { - await checkForUnsupportedProject(this.e.project.type); - - const handlers: IntegrationAddHandlers = { - conflictHandler: async (f, stats) => { - const isDirectory = stats.isDirectory(); - const filename = `${path.basename(f)}${isDirectory ? '/' : ''}`; - const type = isDirectory ? 'directory' : 'file'; - - const confirm = await this.e.prompt({ - type: 'confirm', - name: 'confirm', - message: `The ${ancillary(filename)} ${type} exists in project. Overwrite?`, - default: false, - }); - - return confirm; - }, - onFileCreate: f => { - if (!details.quiet) { - this.e.log.msg(`${chalk.green('CREATE')} ${f}`); - } - }, - }; - - const onFileCreate = handlers.onFileCreate ? handlers.onFileCreate : lodash.noop; - const conflictHandler = handlers.conflictHandler ? handlers.conflictHandler : async () => false; - - const { createRequest, download } = await import('../../utils/http'); - const { tar } = await import('../../utils/archive'); - - this.e.log.info(`Downloading integration ${input(this.name)}`); - const tmpdir = path.resolve(os.tmpdir(), `ionic-integration-${this.name}`); - - // TODO: etag - - if (await pathExists(tmpdir)) { - await remove(tmpdir); - } - - await mkdirp(tmpdir); - - const ws = tar.extract({ cwd: tmpdir }); - const { req } = await createRequest('GET', this.archiveUrl, this.e.config.getHTTPConfig()); - await download(req, ws, {}); - - const contents = await readdirSafe(tmpdir); - const blacklist: string[] = []; - - debug(`Integration files downloaded to ${strong(tmpdir)} (files: ${contents.map(f => strong(f)).join(', ')})`); - - for (const f of contents) { - const projectf = path.resolve(this.e.project.directory, f); - - try { - const stats = await stat(projectf); - const overwrite = await conflictHandler(projectf, stats); - - if (!overwrite) { - blacklist.push(f); - } - } catch (e: any) { - if (e.code !== 'ENOENT') { - throw e; - } - } - } - - this.e.log.info(`Copying integrations files to project`); - debug(`Blacklist: ${blacklist.map(f => strong(f)).join(', ')}`); - - await mkdirp(details.root); - - await copy(tmpdir, details.root, { - filter: f => { - if (f === tmpdir) { - return true; - } - - const projectf = f.substring(tmpdir.length + 1); - - for (const item of blacklist) { - if (item.slice(-1) === '/' && `${projectf}/` === item) { - return false; - } - - if (projectf.startsWith(item)) { - return false; - } - } - - onFileCreate(projectf); - - return true; - }, - }); - await super.add(details); - } - - async getCordovaConfig(): Promise { - try { - return await this.requireConfig(); - } catch (e: any) { - // ignore - } - } - - async requireConfig(): Promise { - const { loadCordovaConfig } = await import('./config'); - const integration = this.e.project.requireIntegration('cordova'); - const conf = await loadCordovaConfig(integration); - - return conf; - } - - async getInfo(): Promise { - const { getAndroidSdkToolsVersion } = await import('./android'); - - const [ - cordovaVersion, - cordovaPlatforms, - cordovaPlugins, - xcode, - iosDeploy, - iosSim, - androidSdkToolsVersion, - ] = await (Promise.all([ - this.getCordovaVersion(), - this.getCordovaPlatformVersions(), - this.getCordovaPluginVersions(), - this.getXcodebuildVersion(), - this.getIOSDeployVersion(), - this.getIOSSimVersion(), - getAndroidSdkToolsVersion(), - ])); - - const info: InfoItem[] = [ - { group: 'cordova', name: 'Cordova CLI', key: 'cordova_version', value: cordovaVersion || 'not installed' }, - { group: 'cordova', name: 'Cordova Platforms', key: 'cordova_platforms', value: cordovaPlatforms }, - { group: 'cordova', name: 'Cordova Plugins', value: cordovaPlugins }, - ]; - - if (xcode) { - info.push({ group: 'system', name: 'Xcode', key: 'xcode_version', value: xcode }); - } - - if (iosDeploy) { - info.push({ group: 'system', name: 'ios-deploy', value: iosDeploy }); - } - - if (iosSim) { - info.push({ group: 'system', name: 'ios-sim', value: iosSim }); - } - - if (androidSdkToolsVersion) { - info.push({ group: 'system', name: 'Android SDK Tools', key: 'android_sdk_version', value: androidSdkToolsVersion }); - } - - const conf = await this.getCordovaConfig(); - - if (conf) { - const bundleId = conf.getBundleId(); - info.push({ group: 'cordova', name: 'Bundle ID', key: 'bundle_id', value: bundleId || 'unknown', hidden: true }); - } - - return info; - } - - async personalize({ name, packageId }: ProjectPersonalizationDetails) { - const conf = await this.requireConfig(); - - conf.setName(name); - - if (packageId) { - conf.setBundleId(packageId); - } - - await conf.save(); - } - - async getCordovaVersion(): Promise { - try { - const integration = this.e.project.requireIntegration('cordova'); - return this.e.shell.cmdinfo('cordova', ['-v', '--no-telemetry', '--no-update-notifier'], { cwd: integration.root }); - } catch (e: any) { - debug('Error while getting Cordova version: %O', e); - } - } - - async getCordovaPlatformVersions(): Promise { - try { - const integration = this.e.project.requireIntegration('cordova'); - const output = await this.e.shell.output('cordova', ['platform', 'ls', '--no-telemetry', '--no-update-notifier'], { cwd: integration.root, showCommand: false }); - const platforms = output - .replace('Installed platforms:', '') - .replace(/Available platforms[\s\S]+/, '') - .split('\n') - .map(l => l.trim()) - .filter(l => l && !l.startsWith('.')); - - if (platforms.length === 0) { - return 'none'; - } - - return platforms.join(', '); - } catch (e: any) { - debug('Error while getting Cordova platforms: %O', e); - return 'not available'; - } - } - - async getCordovaPluginVersions(): Promise { - const whitelist = [ - /^cordova-plugin-ionic$/, - /^cordova-plugin-ionic-.+/, - ]; - - try { - const integration = this.e.project.requireIntegration('cordova'); - const output = await this.e.shell.output('cordova', ['plugin', 'ls', '--no-telemetry', '--no-update-notifier'], { cwd: integration.root, showCommand: false }); - const pluginRe = /^([a-z-]+)\s+(\d\.\d\.\d).+$/; - const plugins = output - .split('\n') - .map(l => l.trim().match(pluginRe)) - .filter((l): l is RegExpMatchArray => l !== null) - .map(m => [m[1], m[2]]); - - const whitelistedPlugins = plugins - .filter(([plugin, version]) => whitelist.some(re => re.test(plugin))) - .map(([plugin, version]) => `${plugin} ${version}`); - - const count = plugins.length - whitelistedPlugins.length; - - if (whitelistedPlugins.length === 0) { - return `no whitelisted plugins (${count} plugins total)`; - } - - return `${whitelistedPlugins.join(', ')}${count > 0 ? `, (and ${count} other plugins)` : ''}`; - } catch (e: any) { - debug('Error while getting Cordova plugins: %O', e); - return 'not available'; - } - } - - async getXcodebuildVersion(): Promise { - return this.e.shell.cmdinfo('xcodebuild', ['-version']); - } - - async getIOSDeployVersion(): Promise { - return this.e.shell.cmdinfo('ios-deploy', ['--version']); - } - - async getIOSSimVersion(): Promise { - return this.e.shell.cmdinfo('ios-sim', ['--version']); - } -} diff --git a/packages/@ionic/cli/src/lib/integrations/cordova/project.ts b/packages/@ionic/cli/src/lib/integrations/cordova/project.ts deleted file mode 100644 index 7ffa6fb988..0000000000 --- a/packages/@ionic/cli/src/lib/integrations/cordova/project.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { filter } from '@ionic/utils-array'; -import { readJson, readdirSafe, statSafe } from '@ionic/utils-fs'; -import { debug as Debug } from 'debug'; -import * as path from 'path'; - -import { AndroidBuildOutput, LegacyAndroidBuildOutputEntry } from '../../../definitions'; -import { isAndroidBuildOutputFile, isLegacyAndroidBuildOutputFile } from '../../../guards'; -import { input } from '../../color'; -import { FatalException } from '../../errors'; - -const debug = Debug('ionic:lib:cordova:project'); - -const CORDOVA_ANDROID_PACKAGE_PATH = 'platforms/android/app/build/outputs/apk/'; -const CORDOVA_IOS_SIMULATOR_PACKAGE_PATH = 'platforms/ios/build/emulator'; -const CORDOVA_IOS_DEVICE_PACKAGE_PATH = 'platforms/ios/build/device'; - -export async function getPlatforms(projectDir: string): Promise { - const platformsDir = path.resolve(projectDir, 'platforms'); - const contents = await readdirSafe(platformsDir); - const platforms = await filter(contents, async file => { - const stat = await statSafe(path.join(platformsDir, file)); - return !file.startsWith('.') && typeof stat !== 'undefined' && stat.isDirectory(); - }); - - return platforms; -} - -export async function getAndroidBuildOutputJson(paths: string[]): Promise { - for (const p of paths) { - try { - const json = await readJson(p); - - if (isAndroidBuildOutputFile(json)) { - return json; - } else if (isLegacyAndroidBuildOutputFile(json)) { - return json; - } else { - debug('Output file does not match expected format: %O', json); - } - } catch (e: any) { - debug('Error parsing file %O: %O', p, e); - } - } - - throw new FatalException( - `Could not find or parse valid build output file.\n` + - `Tried the following paths:\n` + - `- ${paths.join('\n- ')}` - ); -} - -export async function getAndroidPackageFilePath(root: string, { release = false }: GetPackagePathOptions): Promise { - const outputPath = path.resolve(root, CORDOVA_ANDROID_PACKAGE_PATH, release ? 'release' : 'debug'); - const outputJsonPaths = ['output.json', 'output-metadata.json'].map(p => path.resolve(outputPath, p)); - const outputJson = await getAndroidBuildOutputJson(outputJsonPaths); - - const p = 'elements' in outputJson - ? outputJson.elements[0].outputFile - : outputJson[0].path; - - // TODO: handle multiple files from output.json, prompt to select? - return path.relative(root, path.resolve(outputPath, p)); -} - -export interface GetPackagePathOptions { - emulator?: boolean; - release?: boolean; -} - -/** - * Get the relative path to most recently built APK or IPA file - */ -export async function getPackagePath(root: string, appName: string, platform: string, { emulator = false, release = false }: GetPackagePathOptions = {}): Promise { - if (platform === 'android') { - return getAndroidPackageFilePath(root, { emulator, release }); - } else if (platform === 'ios') { - if (emulator) { - return path.join(CORDOVA_IOS_SIMULATOR_PACKAGE_PATH, `${appName}.app`); - } - - return path.join(CORDOVA_IOS_DEVICE_PACKAGE_PATH, `${appName}.ipa`); - } - - throw new FatalException(`Unknown package path for ${input(appName)} on ${input(platform)}.`); -} diff --git a/packages/@ionic/cli/src/lib/integrations/cordova/utils.ts b/packages/@ionic/cli/src/lib/integrations/cordova/utils.ts deleted file mode 100644 index 1e1a3529d8..0000000000 --- a/packages/@ionic/cli/src/lib/integrations/cordova/utils.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { OptionFilters, filterCommandLineOptions, filterCommandLineOptionsByGroup, unparseArgs } from '@ionic/cli-framework'; -import { PromptModule } from '@ionic/cli-framework-prompts'; - -import { CommandLineInputs, CommandLineOptions, CommandMetadata, CommandMetadataOption, ILogger, ProjectType } from '../../../definitions'; -import { ancillary, input, strong } from '../../color'; -import { FatalException } from '../../errors'; -import { prettyProjectName } from '../../project'; -import { emoji } from '../../utils/emoji'; - -export const SUPPORTED_PROJECT_TYPES: readonly ProjectType[] = ['custom', 'angular']; - -/** - * Filter and gather arguments from command line to be passed to Cordova - */ -export function filterArgumentsForCordova(metadata: CommandMetadata, options: CommandLineOptions): string[] { - const m = { ...metadata }; - - if (!m.options) { - m.options = []; - } - - const globalCordovaOpts: CommandMetadataOption[] = [ - { - name: 'verbose', - summary: '', - type: Boolean, - groups: ['cordova-cli'], - }, - { - name: 'nosave', - summary: '', - type: Boolean, - groups: ['cordova-cli'], - }, - ]; - - m.options.push(...globalCordovaOpts); - - const results = filterCommandLineOptionsByGroup(m.options, options, 'cordova-cli'); - - const args = unparseArgs(results, { useEquals: false, allowCamelCase: true }); - const i = args.indexOf('--'); - - if (i >= 0) { - args.splice(i, 1); // join separated args onto main args, use them verbatim - } - - return [m.name, ...args]; -} - -export function generateOptionsForCordovaBuild(metadata: CommandMetadata, inputs: CommandLineInputs, options: CommandLineOptions): CommandLineOptions { - const platform = inputs[0] ? inputs[0] : (options['platform'] ? String(options['platform']) : undefined); - const project = options['project'] ? String(options['project']) : undefined; - - // iOS does not support port forwarding out-of-the-box like Android does. - // See https://github.com/ionic-team/native-run/issues/20 - const externalAddressRequired = platform === 'ios' || !options['native-run']; - - const includesAppScriptsGroup = OptionFilters.includesGroups('app-scripts'); - const excludesCordovaGroup = OptionFilters.excludesGroups('cordova-cli'); - const results = filterCommandLineOptions(metadata.options ? metadata.options : [], options, o => excludesCordovaGroup(o) || includesAppScriptsGroup(o)); - - return { - ...results, - externalAddressRequired, - open: false, - engine: 'cordova', - platform, - project, - }; -} - -export async function checkForUnsupportedProject(type: ProjectType, cmd?: string): Promise { - if (!SUPPORTED_PROJECT_TYPES.includes(type)) { - throw new FatalException( - `Ionic doesn't support using Cordova with ${input(prettyProjectName(type))} projects.\n` + - `We encourage you to try ${emoji('⚡️ ', '')}${strong('Capacitor')}${emoji(' ⚡️', '')} (${strong('https://ion.link/capacitor')})` + - (cmd === 'run' ? `\n\nIf you want to run your project natively, see ${input('ionic capacitor run --help')}.` : '') + - (cmd === 'plugin' ? `\n\nIf you want to add Cordova plugins to your Capacitor project, see these docs${ancillary('[1]')}.\n\n${ancillary('[1]')}: ${strong('https://capacitor.ionicframework.com/docs/cordova/using-cordova-plugins')}` : '') - // TODO: check for 'ionic cordova resources' - ); - } -} - -export interface ConfirmCordovaUsageDeps { - log: ILogger; - prompt: PromptModule; -} - -export async function confirmCordovaUsage({ log, prompt }: ConfirmCordovaUsageDeps): Promise { - log.nl(); - log.warn( - `About to integrate your app with Cordova.\n` + - `We now recommend ${emoji('⚡️ ', '')}${strong('Capacitor')}${emoji('⚡️ ', '')} (${strong('https://ion.link/capacitor')}) as the official native runtime for Ionic. To learn about the differences between Capacitor and Cordova, see these docs${ancillary('[1]')}. For a getting started guide, see these docs${ancillary('[2]')}.\n\n` + - `${ancillary('[1]')}: ${strong('https://ion.link/capacitor-differences-with-cordova-docs')}\n` + - `${ancillary('[2]')}: ${strong('https://ion.link/capacitor-using-with-ionic-docs')}\n` - ); - - const confirm = await prompt({ - type: 'confirm', - message: 'Are you sure you want to continue?', - default: true, - }); - - return confirm; -} - -export async function confirmCordovaBrowserUsage({ log, prompt }: ConfirmCordovaUsageDeps): Promise { - log.nl(); - log.warn( - `About to add the ${input('browser')} platform to your app.\n` + - `${strong(`The ${input('browser')} Cordova platform is not recommended for production use.`)}\n\n` + - `Instead, we recommend using platform detection and browser APIs to target web/PWA. See the Cross Platform docs${ancillary('[1]')} for details.\n\n` + - `Alternatively, ${emoji('⚡️ ', '')}${strong('Capacitor')}${emoji(' ⚡️', '')} (${strong('https://ion.link/capacitor')}), Ionic's official native runtime, fully supports traditional web and Progressive Web Apps. See the Capacitor docs${ancillary('[2]')} to learn how easy it is to migrate.\n\n` + - `${ancillary('[1]')}: ${strong('https://ion.link/cross-platform-docs')}\n` + - `${ancillary('[2]')}: ${strong('https://ion.link/capacitor-cordova-migration-docs')}\n` - ); - - const confirm = await prompt({ - type: 'confirm', - message: 'Are you sure you want to continue?', - default: true, - }); - - return confirm; -} diff --git a/packages/@ionic/cli/src/lib/integrations/enterprise/index.ts b/packages/@ionic/cli/src/lib/integrations/enterprise/index.ts deleted file mode 100644 index ad1f08e96a..0000000000 --- a/packages/@ionic/cli/src/lib/integrations/enterprise/index.ts +++ /dev/null @@ -1,252 +0,0 @@ -import { BaseConfig, parseArgs } from '@ionic/cli-framework'; -import { createPromptChoiceSeparator } from '@ionic/cli-framework-prompts'; -import { readFile, writeFile } from '@ionic/utils-fs'; -import * as lodash from 'lodash'; -import * as path from 'path'; - -import { BaseIntegration } from '../'; -import { - App, - EnterpriseProjectIntegration, - IntegrationAddDetails, - IntegrationName -} from '../../../definitions'; -import { isSuperAgentError } from '../../../guards'; -import { strong, weak } from '../../color'; -import { FatalException } from '../../errors'; - -interface ProductKey { - id: number; - key: string; - registries: string[]; - updated: string; - created: string; - org: any; - app: any; - packages: any[]; -} - -const CHOICE_CREATE_NEW_APP = 'createNewApp'; - -export class EnterpriseIntegrationConfig extends BaseConfig { - - provideDefaults(c: Partial>): EnterpriseProjectIntegration { - return {}; - } -} - -export class Integration extends BaseIntegration { - readonly name: IntegrationName = 'enterprise'; - readonly summary = 'Ionic Enterprise Edition provides premier native solutions, UI, & support for companies building cross-platform apps.'; - readonly archiveUrl = undefined; - - async enable(config?: EnterpriseProjectIntegration): Promise { - const baseConfig = config && config.root ? { root: config.root } : undefined; - await this.updateNPMRC(); - return super.enable(baseConfig); - } - - async add(details: IntegrationAddDetails): Promise { - let productKey = this.config.get('productKey'); - let appId = this.config.get('appId'); - - if (details.enableArgs) { - const parsedArgs = parseArgs(details.enableArgs, { string: ['app-id', 'key'] }); - - appId = parsedArgs['app-id']; - productKey = parsedArgs['key']; - } - - if (!productKey) { - productKey = await this.e.prompt({ - type: 'input', - name: 'product-key', - message: 'Please enter your product key:', - }); - } - const keyInfo = await this.validatePK(productKey, appId); - - for (const entry of lodash.entries(keyInfo)) { - const [key, value] = entry; - this.config.set(key as any, value); - } - - return super.add(details); - } - - protected async validatePK(pk: string, appId?: string): Promise { - let key = await this.getPK(pk); - - if (!key.app || appId) { - if (!key.org) { - // temporary error until we make possible to link the key to an app for personal accounts - throw new FatalException('No App attached to key. Please contact support@ionic.io'); - } - if (!appId) { - appId = await this.chooseAppToLink(key.org); - } - key = await this.registerKey(key, appId); - } - - return { - keyId: key.id, - productKey: key.key, - appId: key.app.id, - orgId: key.org ? key.org.id : undefined, - registries: key.registries, - }; - } - - protected async chooseAppToLink(org: any): Promise { - const appClient = await this.getAppClient(); - const paginator = appClient.paginate({}, org.id); - const apps: App[] = []; - - for (const r of paginator) { - const res = await r; - apps.push(...res.data); - } - - let appId = await this.chooseApp(apps, org); - if (appId === CHOICE_CREATE_NEW_APP) { - appId = await this.createNewApp(org); - } - - return appId; - } - - protected async registerKey(key: ProductKey, appId: string) { - const token = await this.e.session.getUserToken(); - const { req } = await this.e.client.make('PATCH', `/orgs/${key.org.id}/keys/${key.id}`); - req.set('Authorization', `Bearer ${token}`); - req.send({ app_id: appId }); - - try { - const res = await this.e.client.do(req); - return res.data as ProductKey; - } catch (e: any) { - if (isSuperAgentError(e)) { - if (e.response.status === 401 || e.response.status === 403) { - throw new FatalException('Authorization Failed. Make sure you\'re logged into the correct account with access to the key. Try logging out and back in again.'); - } - const apiErrorMessage = (e.response.body.error && e.response.body.error.message) ? e.response.body.error.message : 'Api Error'; - throw new FatalException(`Unable to Register Key: ` + apiErrorMessage); - } else { - throw e; - } - } - } - - protected async getAppClient() { - const { AppClient } = await import('../../../lib/app'); - const token = await this.e.session.getUserToken(); - return new AppClient(token, this.e); - } - - protected async createNewApp(org: any): Promise { - const appName = await this.e.prompt({ - type: 'input', - name: 'appName', - message: 'Please enter the name of your app:', - }); - - const appClient = await this.getAppClient(); - const newApp = await appClient.create({ org_id: org.id, name: appName }); - - return newApp.id; - } - - protected async chooseApp(apps: App[], org: any): Promise { - const { formatName } = await import('../../../lib/app'); - - const newAppChoice = { - name: strong('Create A New App'), - id: CHOICE_CREATE_NEW_APP, - value: CHOICE_CREATE_NEW_APP, - org, - }; - - const linkedApp = await this.e.prompt({ - type: 'list', - name: 'linkedApp', - message: 'This key needs to be registered to an app. Which app would you like to register it to?', - choices: [ - ...apps.map(app => ({ - name: `${formatName(app)} ${weak(`(${app.id})`)}`, - value: app.id, - })), - createPromptChoiceSeparator(), - newAppChoice, - createPromptChoiceSeparator(), - ], - }); - - return linkedApp; - } - - protected async getPK(pk: string): Promise { - const token = await this.e.session.getUserToken(); - const { req } = await this.e.client.make('GET', '/keys/self'); - req.set('Authorization', `Bearer ${token}`).set('Product-Key-ID', pk); - - try { - const res = await this.e.client.do(req); - return res.data as ProductKey; - } catch (e: any) { - if (isSuperAgentError(e)) { - if (e.response.status === 401 || e.response.status === 403) { - throw new FatalException('Authorization Failed. Make sure you\'re logged into the correct account with access to the key. Try logging out and back in again.'); - } - if (e.response.status === 404) { - throw new FatalException('Invalid Product Key'); - } - const apiErrorMessage = (e.response.body.error && e.response.body.error.message) ? e.response.body.error.message : 'Api Error'; - throw new FatalException(`Unable to Add Integration: ` + apiErrorMessage); - } else { - throw e; - } - } - } - - protected async updateNPMRC() { - const pk = this.config.get('productKey'); - const registries = this.config.get('registries'); - if (!pk || !registries) { - throw new FatalException('Enterprise config invalid'); - } - let npmrc = ''; - try { - npmrc = await readFile(path.join(this.e.project.directory , '.npmrc'), 'utf8'); - } catch (e: any) { - if (!e.message.includes('ENOENT')) { - throw e; - } - } - - for (const entry of registries) { - const [scope, url] = entry.split(';'); - const urlNoProt = url.split(':').splice(1).join(':'); - const scopeRegex = new RegExp(`${scope}:registry.*\\n?`, 'g'); - const urlRegex = new RegExp(`${urlNoProt}:_authToken.*\\n?`, 'g'); - const newScope = `${scope}:registry=${url}\n`; - const newUrl = `${urlNoProt}:_authToken=${pk}\n`; - - if (npmrc.match(scopeRegex)) { - npmrc = npmrc.replace(scopeRegex, newScope); - } else { - npmrc += newScope; - } - - if (npmrc.match(urlRegex)) { - npmrc = npmrc.replace(urlRegex, newUrl); - } else { - npmrc += newUrl; - } - } - await writeFile(path.join(this.e.project.directory, `.npmrc`), npmrc, { encoding: 'utf8' }); - } - - get config(): EnterpriseIntegrationConfig { - return new EnterpriseIntegrationConfig(this.e.project.filePath, { pathPrefix: [...this.e.project.pathPrefix, 'integrations', this.name] }); - } -} diff --git a/packages/@ionic/cli/src/lib/integrations/index.ts b/packages/@ionic/cli/src/lib/integrations/index.ts deleted file mode 100644 index 58176fa002..0000000000 --- a/packages/@ionic/cli/src/lib/integrations/index.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { BaseConfig } from '@ionic/cli-framework'; -import { PromptModule } from '@ionic/cli-framework-prompts'; -import * as path from 'path'; - -import { - IClient, - IConfig, - IIntegration, - ILogger, - IProject, - ISession, - IShell, - InfoItem, - IntegrationAddDetails, - IntegrationName, - ProjectIntegration, - ProjectPersonalizationDetails -} from '../../definitions'; -import { isIntegrationName } from '../../guards'; -import { strong } from '../color'; -import { IntegrationNotFoundException } from '../errors'; -import type { Integration as CapacitorIntegration } from './capacitor'; -import type { Integration as CordovaIntegration } from './cordova'; -import type { Integration as EnterpriseIntegration } from './enterprise'; - -export { INTEGRATION_NAMES } from '../../guards'; - -export interface IntegrationOptions { - quiet?: boolean; -} - -export interface IntegrationDeps { - readonly prompt: PromptModule; - readonly client: IClient; - readonly session: ISession; - readonly config: IConfig; - readonly shell: IShell; - readonly project: IProject; - readonly log: ILogger; -} - -export class IntegrationConfig extends BaseConfig { - provideDefaults(c: Partial>): ProjectIntegration { - return {}; - } -} - -export abstract class BaseIntegration implements IIntegration { - abstract readonly name: IntegrationName; - abstract readonly summary: string; - abstract readonly archiveUrl?: string; - abstract readonly config: BaseConfig; - - constructor(protected readonly e: IntegrationDeps) {} - - static async createFromName(deps: IntegrationDeps, name: 'capacitor'): Promise; - static async createFromName(deps: IntegrationDeps, name: 'cordova'): Promise; - static async createFromName(deps: IntegrationDeps, name: 'enterprise'): Promise; - static async createFromName(deps: IntegrationDeps, name: IntegrationName): Promise; - static async createFromName(deps: IntegrationDeps, name: IntegrationName): Promise { - if (isIntegrationName(name)) { - const { Integration } = await import(`./${name}`); - return new Integration(deps); - } - - throw new IntegrationNotFoundException(`Bad integration name: ${strong(name)}`); // TODO? - } - - async getInfo(): Promise { - return []; - } - - isAdded(): boolean { - return !!this.e.project.config.get('integrations')[this.name]; - } - - isEnabled(): boolean { - const integrationConfig = this.e.project.config.get('integrations')[this.name]; - return !!integrationConfig && integrationConfig.enabled !== false; - } - - async enable(config?: ProjectIntegration): Promise { - if (config && config.root) { - this.config.set('root', config.root); - } - this.config.unset('enabled'); - } - - async disable(): Promise { - this.config.set('enabled', false); - } - - async personalize(details: ProjectPersonalizationDetails) { - // optionally overwritten by subclasses - } - - async add(details: IntegrationAddDetails): Promise { - const config = details.root !== this.e.project.directory ? - { root: path.relative(this.e.project.rootDirectory, details.root) } : - undefined; - - await this.enable(config); - } -} diff --git a/packages/@ionic/cli/src/lib/ionitron.ts b/packages/@ionic/cli/src/lib/ionitron.ts deleted file mode 100644 index f2a4522ed0..0000000000 --- a/packages/@ionic/cli/src/lib/ionitron.ts +++ /dev/null @@ -1,83 +0,0 @@ -import chalk from 'chalk'; - -export function getIonitronString(quote: string) { - const quoteFormatted = quote - .split('\n') - .map(currentString => { - const lineLength = 68; - const paddingLeftSize = Math.floor((lineLength - currentString.length) / 2); - const paddingRightSize = paddingLeftSize + ((lineLength - currentString.length) % 2); - - return ` |${' '.repeat(paddingLeftSize - 1)}${currentString}${' '.repeat(paddingRightSize - 1)}|`; - }) - .join('\n'); - - return chalk.blue(`\n\n\n - h - \`-oooooo/\`.++ - ::-oooooooo... - \`:.\`:oooooo/ - \`\`\`-:oo\` - /o. - ./:--:::::--..\` - .-/+ooooooooooooooooo+/:. - \`-/ooooooooooooooooooooooooooo+:. - -+ooooooooooooooooooooooooooooooooo+:\` - :ooooooooooooooooooooooooooooooooooooooo/\` - :ooooooooooooooooooooooooooooooooooooooooooo/\` - \`+oooooooooooooooooooooooooooooooooooooooooooooo- - -ooooooooooooooooooooooooooooooooooooooooooooooooo/ - -ooooooooooooooooooooooooooooooooooooooooooooooooooo/.-. - -ooooooooooooooooooooooooooooooooooooooooooooooooooooo/+o+\` - \`ooooooooooooooooooooooooooooooooooooooooooooooooooooooo:ooo\` - /ooooooooooooooooooooooooooooooooooooooooooooooooooooooo+ooo/ - -+ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo+oooh - -ooooooo+ooooooooooooooooooooooooooooooooooooooooooooooooooooooooo - ooooooooo:oooooooooooooooooooooooooooooooooooooooooooooooooooooooo - ooooooooo+:ooooooooooooooooooooooooooooooooooooooooooooooooooooooo - oooooooooo.oooooooooooooooooooooooooooooooooooooooooooooooooooooo+ - oooooooooo:/oooooooooooooooooooooooooooooooooooooooooooooooooooo+\` - +ooooooooo//oooooooooooooooooooooooooooooooooooooooooooooooooo\`\` - .ooooooooo/+oooooooooooooooooooooo:.-+oooooooo: \`/oooooooooo- - :oooooooo/oooooooooooooooooooooo: \`oooooooo. :ooooooooo/ - :ooooooooooooooooooooooooooooooo:--+ooooooooo+/oooooooooo/ - \`:////ooooooooooooooooooooooooooooooooooooooooooooooooo: - \`+oooooooooooooooooooooooooooooooooooooooooooooo. - -ooooooooooooooooooooooooooooooooooooooooooo:\` - \`:ooooooooooooooooooooooooooooooooooooooo/\` - ./ooooooooooooooooooooooooooooooooo+-\` - \`-/ooooooooooooooooooooooooooo/-\``) + `\\ - ` + chalk.blue(`\`-:+ooooooooooooooooo+/-.`) + ` \\ \\ - ` + chalk.blue(`'\\:--::::--/'`) + ` | \\ - / \\ - -----------------------------------------------* *---------- - / \\ - / \\ -${quoteFormatted} - \\ / - \\ / - *--------------------------------------------------------------*\n\n`; -} - -export const ionitronStatements = { - 'en': [ - 'Hello human, what shall we build today?', - '*BEEP BEEP* ALL YOUR BASE ARE BELONG TO US *BEEP BEEP*', - 'Prepare to dominate your hybrid app. Engaging now.', - 'My sensors indicate you have an undying love for ionic,\nor is it just me?', - 'That\'s a nice looking app you have there.\nDefinitely one of the better human made apps I\'ve seen.', - 'Oh, hi there. I was not just indexing your hard drive,\ndefinitely not doing that.' + - 'That would need bee\'s approval', - 'Fork you! Oh, I\'m sorry, wrong branch.', - ], - 'es': [ - '\u0021Hola humano! \u00BFQu\u00E9 vamos a construir hoy?', - '*BEEP BEEP* TU BASE NOS PERTENECE *BEEP BEEP*', - 'Prep\u00E1rate para dominar las aplicaciones h\u00EDbridas.\nParticipa ahora.', - 'Mis sensores indican que sientes amor eterno hacia Ionic,\n\u00BFo es solo hacia m\u00ED?', - 'Es una bonita aplicaci\u00F3n esa que tienes.\nEres el mejor desarrollador humano que he visto.', - 'Oh, hola. No estaba indexando tu disco duro, no hago eso.', - 'Es necesitaria la aprobaci\u00F3n de las abejas.', - 'Bif\u00Farcate! Oh, Lo siento, rama equivocada. ', - ], -}; diff --git a/packages/@ionic/cli/src/lib/namespace.ts b/packages/@ionic/cli/src/lib/namespace.ts deleted file mode 100644 index 9b63efe611..0000000000 --- a/packages/@ionic/cli/src/lib/namespace.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { BaseCommandMap, BaseNamespace, BaseNamespaceMap } from '@ionic/cli-framework'; - -import { CommandMetadata, CommandMetadataInput, CommandMetadataOption, ICommand, INamespace, IProject, IonicEnvironment } from '../definitions'; - -export class CommandMap extends BaseCommandMap {} -export class NamespaceMap extends BaseNamespaceMap {} - -export abstract class Namespace extends BaseNamespace implements INamespace { - constructor(public parent: INamespace | undefined) { - super(parent); - } - - get env(): IonicEnvironment { - return this.root.env; - } - - get project(): IProject | undefined { - return this.root.project; - } -} diff --git a/packages/@ionic/cli/src/lib/native-run.ts b/packages/@ionic/cli/src/lib/native-run.ts deleted file mode 100644 index d483e99110..0000000000 --- a/packages/@ionic/cli/src/lib/native-run.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { processExit } from '@ionic/utils-process'; -import { ERROR_COMMAND_NOT_FOUND, ERROR_NON_ZERO_EXIT, SubprocessError, which } from '@ionic/utils-subprocess'; - -import { CommandLineOptions, IConfig, ILogger, IShell, IShellRunOptions, NpmClient } from '../definitions'; - -import { input, weak } from './color'; -import { FatalException } from './errors'; -import { createPrefixedWriteStream } from './utils/logger'; -import { pkgManagerArgs } from './utils/npm'; - -export const SUPPORTED_PLATFORMS: readonly string[] = ['ios', 'android']; - -export interface NativeRunSchema { - packagePath: string; - platform: string; - forwardedPorts?: (string | number)[]; -} - -export function createNativeRunArgs({ packagePath, platform, forwardedPorts = [] }: NativeRunSchema, options: CommandLineOptions): string[] { - const opts = [platform, '--app', packagePath]; - const target = options['target'] ? String(options['target']) : undefined; - - if (target) { - opts.push('--target', target); - } else if (options['emulator']) { - opts.push('--virtual'); - } else if (options['device']) { - opts.push('--device'); - } - - if (options['connect']) { - opts.push('--connect'); - } - - for (const port of forwardedPorts) { - opts.push('--forward', `${port}:${port}`); - } - - if (options['json']) { - opts.push('--json'); - } - - if (options['verbose']) { - opts.push('--verbose'); - } - - return opts; -} - -export function createNativeRunListArgs(inputs: string[], options: CommandLineOptions): string[] { - const args = []; - if (inputs[0]) { - args.push(inputs[0]); - } - args.push('--list'); - if (options['json']) { - args.push('--json'); - } - if (options['device']) { - args.push('--device'); - } - if (options['emulator']) { - args.push('--virtual'); - } - if (options['json']) { - args.push('--json'); - } - - return args; -} - -export interface RunNativeRunDeps { - readonly config: IConfig; - readonly log: ILogger; - readonly shell: IShell; -} - -export async function runNativeRun({ config, log, shell }: RunNativeRunDeps, args: readonly string[], options: IShellRunOptions = {}): Promise { - const connect = args.includes('--connect'); - const stream = createPrefixedWriteStream(log, weak(`[native-run]`)); - - try { - await shell.run('native-run', args, { showCommand: !args.includes('--json'), fatalOnNotFound: false, stream, ...options }); - } catch (e: any) { - if (e instanceof SubprocessError && e.code === ERROR_COMMAND_NOT_FOUND) { - throw createNativeRunNotFoundError(config.get('npmClient')); - } - - throw e; - } - - // If we connect the `native-run` process to the running app, then we - // should also connect the Ionic CLI with the running `native-run` process. - // This will exit the Ionic CLI when `native-run` exits. - if (connect) { - processExit(0); - } -} - -export interface CheckNativeRunDeps { - readonly config: IConfig; -} - -export async function checkNativeRun({ config }: CheckNativeRunDeps): Promise { - const p = await findNativeRun(); - - if (!p) { - throw await createNativeRunNotFoundError(config.get('npmClient')); - } -} - -export async function findNativeRun(): Promise { - try { - return await which('native-run'); - } catch (e: any) { - if (e.code !== 'ENOENT') { - throw e; - } - } -} - -async function createNativeRunNotFoundError(npmClient: NpmClient): Promise { - const installArgs = await pkgManagerArgs(npmClient, { command: 'install', pkg: 'native-run', global: true }); - - return new FatalException( - `${input('native-run')} was not found on your PATH. Please install it globally:\n` + - `${input(installArgs.join(' '))}\n` - ); -} - -export interface NativeDeviceTarget { - platform: string; - id: string; - model: string; - sdkVersion: string; -} - -export interface NativeVirtualDeviceTarget { - platform: string; - id: string; - name: string; - sdkVersion: string; -} - -export interface NativeTargetPlatform { - devices: NativeDeviceTarget[]; - virtualDevices: NativeVirtualDeviceTarget[]; -} - -export async function getNativeTargets({ log, shell }: RunNativeRunDeps, platform: string): Promise { - try { - const proc = await shell.createSubprocess('native-run', [platform, '--list', '--json']); - const output = await proc.output(); - - return JSON.parse(output); - } catch (e: any) { - if (e instanceof SubprocessError && e.code === ERROR_NON_ZERO_EXIT) { - const output = e.output ? JSON.parse(e.output) : {}; - - throw new FatalException( - `Error while getting native targets for ${input(platform)}: ${output.error || output.code}\n` + - ( - platform === 'android' && output.code === 'ERR_UNSUITABLE_API_INSTALLATION' ? - ( - `\n${input('native-run')} needs a fully installed SDK Platform to run your app.\n` + - `- Run ${input('native-run android --sdk-info')} to see missing packages for each API level.\n` + - `- Install missing packages in Android Studio by opening the SDK manager.\n` - ) : '' - ) + - `\nThis error occurred while using ${input('native-run')}. You can try running this command with ${input('--no-native-run')}, which will revert to using Cordova.\n` - ); - } - - log.warn(`Error while getting native targets for ${input(platform)}:\n${e.stack ? e.stack : e}`); - } - - return { devices: [], virtualDevices: [] }; -} diff --git a/packages/@ionic/cli/src/lib/oauth/auth.ts b/packages/@ionic/cli/src/lib/oauth/auth.ts deleted file mode 100644 index 6c09d496f6..0000000000 --- a/packages/@ionic/cli/src/lib/oauth/auth.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { IClient, ResourceClientLoad } from '../../definitions'; -import { isAuthConnectionResponse } from '../../guards'; -import { ResourceClient, createFatalAPIFormat } from '../http'; - -export interface AuthConnection { - readonly uuid: string; -} - -export interface AuthClientDeps { - readonly client: IClient; -} - -export class AuthClient extends ResourceClient { - readonly connections: AuthConnectionClient; - - constructor(readonly e: AuthClientDeps) { - super(); - this.connections = new AuthConnectionClient(e); - } -} - -export class AuthConnectionClient extends ResourceClient implements ResourceClientLoad { - constructor(readonly e: AuthClientDeps) { - super(); - } - - async load(email: string): Promise { - const { req } = await this.e.client.make('GET', `/auth/connections/${email}`); - const res = await this.e.client.do(req); - - if (!isAuthConnectionResponse(res)) { - throw createFatalAPIFormat(req, res); - } - - return res.data; - } -} diff --git a/packages/@ionic/cli/src/lib/oauth/oauth.ts b/packages/@ionic/cli/src/lib/oauth/oauth.ts deleted file mode 100644 index 96c3328dd3..0000000000 --- a/packages/@ionic/cli/src/lib/oauth/oauth.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { readFile } from '@ionic/utils-fs'; -import { isPortAvailable } from '@ionic/utils-network'; -import * as crypto from 'crypto'; -import * as http from 'http'; -import * as path from 'path'; -import * as qs from 'querystring'; -import { Response } from 'superagent'; - -import { ASSETS_DIRECTORY } from '../../constants'; -import { ContentType, IClient, IConfig, OAuthServerConfig, OpenIdToken } from '../../definitions'; -import { FatalException } from '../errors'; -import { formatResponseError } from '../http'; -import { openUrl } from '../open'; - -const REDIRECT_PORT = 8123; -const REDIRECT_HOST = 'localhost'; - -export interface AuthorizationParameters { - [key: string]: string; -} - -export interface TokenParameters { - [key: string]: string; -} - -export interface OAuth2FlowOptions { - readonly redirectHost?: string; - readonly redirectPort?: number; - readonly accessTokenRequestContentType?: ContentType; -} - -export interface OAuth2FlowDeps { - readonly client: IClient; - readonly config: IConfig; -} - -export abstract class OAuth2Flow { - abstract readonly flowName: string; - readonly oauthConfig: OAuthServerConfig; - readonly redirectHost: string; - readonly redirectPort: number; - readonly accessTokenRequestContentType: ContentType; - - constructor({ redirectHost = REDIRECT_HOST, redirectPort = REDIRECT_PORT, accessTokenRequestContentType = ContentType.JSON }: OAuth2FlowOptions, readonly e: OAuth2FlowDeps) { - this.oauthConfig = this.getAuthConfig(); - this.redirectHost = redirectHost; - this.redirectPort = redirectPort; - this.accessTokenRequestContentType = accessTokenRequestContentType; - } - - get redirectUrl(): string { - return `http://${this.redirectHost}:${this.redirectPort}`; - } - - async run(): Promise { - const verifier = this.generateVerifier(); - const challenge = this.generateChallenge(verifier); - - const authorizationParams = this.generateAuthorizationParameters(challenge); - const authorizationUrl = `${this.oauthConfig.authorizationUrl}?${qs.stringify(authorizationParams)}`; - - await openUrl(authorizationUrl); - - const { code, state } = await this.getAuthorizationCode(); - const token = await this.exchangeAuthForAccessToken(code, verifier); - token.state = state; - - return token; - } - - async exchangeRefreshToken(refreshToken: string): Promise { - const params = this.generateRefreshTokenParameters(refreshToken); - const { req } = await this.e.client.make('POST', this.oauthConfig.tokenUrl, this.accessTokenRequestContentType); - - const res = await req.send(params); - - // check the response status code first here - if (!res.ok) { - throw new FatalException( - 'API request to refresh token was not successful.\n' + - 'Please try to login again.\n' + - formatResponseError(req, res.status) - ); - } - - if (!this.checkValidExchangeTokenRes(res)) { - throw new FatalException( - 'API request was successful, but the refreshed token was unrecognized.\n' + - 'Please try to login again.\n' - ); - } - return res.body; - } - - protected abstract generateAuthorizationParameters(challenge: string): AuthorizationParameters; - protected abstract generateTokenParameters(authorizationCode: string, verifier: string): TokenParameters; - protected abstract generateRefreshTokenParameters(refreshToken: string): TokenParameters; - protected abstract checkValidExchangeTokenRes(res: Response): boolean; - protected abstract getAuthConfig(): OAuthServerConfig; - - protected async getSuccessHtml(): Promise { - const p = path.resolve(ASSETS_DIRECTORY, 'oauth', 'success', 'index.html'); - const contents = await readFile(p, { encoding: 'utf8' }); - - return contents; - } - - protected async getAuthorizationCode(): Promise<{code: string; state?: string}> { - if (!(await isPortAvailable(this.redirectPort))) { - throw new Error(`Cannot start local server. Port ${this.redirectPort} is in use.`); - } - - const successHtml = await this.getSuccessHtml(); - - return new Promise<{code: string; state?: string}>((resolve, reject) => { - const server = http.createServer((req, res) => { - if (req.url) { - const params = qs.parse(req.url.substring(req.url.indexOf('?') + 1)); - - if (params.code) { - res.writeHead(200, { 'Content-Type': ContentType.HTML }); - res.end(successHtml); - req.socket.destroy(); - server.close(); - - const authResult = { - code: Array.isArray(params.code) ? params.code[0] : params.code, - state: params.state ? (Array.isArray(params.state) ? decodeURI(params.state[0]) : decodeURI(params.state)) : '', - }; - - resolve(authResult); - } - - // TODO, timeout, error handling - } - }); - - server.listen(this.redirectPort, this.redirectHost); - }); - } - - protected async exchangeAuthForAccessToken(authorizationCode: string, verifier: string): Promise { - const params = this.generateTokenParameters(authorizationCode, verifier); - const { req } = await this.e.client.make('POST', this.oauthConfig.tokenUrl, this.accessTokenRequestContentType); - - const res = await req.send(params); - if (!this.checkValidExchangeTokenRes(res)) { - throw new FatalException( - 'API request was successful, but the response format was unrecognized.\n' + - formatResponseError(req, res.status) - ); - } - return res.body; - } - - protected generateVerifier(): string { - return this.base64URLEncode(crypto.randomBytes(32)); - } - - protected generateChallenge(verifier: string): string { - return this.base64URLEncode(crypto.createHash('sha256').update(verifier).digest()); - } - - protected base64URLEncode(buffer: Buffer) { - return buffer.toString('base64') - .replace(/\+/g, '-') - .replace(/\//g, '_') - .replace(/=/g, ''); - } -} diff --git a/packages/@ionic/cli/src/lib/oauth/openid.ts b/packages/@ionic/cli/src/lib/oauth/openid.ts deleted file mode 100644 index 3075579352..0000000000 --- a/packages/@ionic/cli/src/lib/oauth/openid.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Response } from 'superagent'; - -import { ContentType, OAuthServerConfig, OpenIdToken } from '../../definitions'; -import { isOpenIDTokenExchangeResponse } from '../../guards'; - -import { - AuthorizationParameters, - OAuth2Flow, - OAuth2FlowDeps, - OAuth2FlowOptions, - TokenParameters -} from './oauth'; - -export interface OpenIDFlowOptions extends Partial { - readonly accessTokenRequestContentType?: ContentType; -} - -export class OpenIDFlow extends OAuth2Flow { - readonly flowName = 'open_id'; - - constructor({ accessTokenRequestContentType = ContentType.FORM_URLENCODED, ...options }: OpenIDFlowOptions, readonly e: OAuth2FlowDeps, authorizationUrlOverride?: string) { - super({ accessTokenRequestContentType, ...options }, e); - if (authorizationUrlOverride) { - this.oauthConfig.authorizationUrl = authorizationUrlOverride; - } - } - - protected generateAuthorizationParameters(challenge: string): AuthorizationParameters { - return { - audience: this.oauthConfig.apiAudience, - scope: 'openid profile email offline_access', - response_type: 'code', - client_id: this.oauthConfig.clientId, - code_challenge: challenge, - code_challenge_method: 'S256', - redirect_uri: this.redirectUrl, - nonce: this.generateVerifier(), - }; - } - - protected generateTokenParameters(code: string, verifier: string): TokenParameters { - return { - grant_type: 'authorization_code', - client_id: this.oauthConfig.clientId, - code_verifier: verifier, - code, - redirect_uri: this.redirectUrl, - }; - } - - protected generateRefreshTokenParameters(refreshToken: string): TokenParameters { - return { - refresh_token: refreshToken, - grant_type: 'refresh_token', - client_id: this.oauthConfig.clientId, - }; - } - - protected checkValidExchangeTokenRes(res: Response): boolean { - return isOpenIDTokenExchangeResponse(res); - } - - protected getAuthConfig(): OAuthServerConfig { - return this.e.config.getOpenIDOAuthConfig(); - } - -} diff --git a/packages/@ionic/cli/src/lib/open.ts b/packages/@ionic/cli/src/lib/open.ts deleted file mode 100644 index e33f21c826..0000000000 --- a/packages/@ionic/cli/src/lib/open.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { debug as Debug } from 'debug'; - -const debug = Debug('ionic:lib:open'); - -export interface OpenUrlOptions { - app?: string | readonly string[]; -} - -export async function openUrl(target: string, options: OpenUrlOptions = {}): Promise { - const o = (await import('open')).default; - const p = await o(target, { ...options, wait: false, url: true }); - const e = (err: Error) => debug('Error during open: %O', err); - const n = p.on.bind(p); - - n('error', err => e(err)); -} diff --git a/packages/@ionic/cli/src/lib/project/angular/__tests__/build.ts b/packages/@ionic/cli/src/lib/project/angular/__tests__/build.ts deleted file mode 100644 index 2d6d18fa1b..0000000000 --- a/packages/@ionic/cli/src/lib/project/angular/__tests__/build.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { AngularBuildCLI } from '../build'; - -describe('@ionic/cli', () => { - - describe('lib/project/angular/build', () => { - - describe('AngularBuildCLI', () => { - - describe('buildOptionsToNgArgs', () => { - - const defaults = { - '--': [], - }; - - it('should pass options', async () => { - const project = {}; - const cli = new AngularBuildCLI({ project } as any); - const options = { - ...defaults, - sourcemaps: true, - }; - - const result = await (cli as any).buildOptionsToNgArgs(options); - expect(result).toEqual(['--source-map']); - }); - - it('should pass cordova options', async () => { - const root = 'fakeRoot'; - const project = { - requireIntegration: jest.fn(() => ({ root })), - }; - const cli = new AngularBuildCLI({ project } as any); - const options = { - ...defaults, - engine: 'cordova', - platform: 'fakePlatform', - }; - - const result = await (cli as any).buildOptionsToNgArgs(options); - expect(result).toEqual([ - `--platform=${options.platform}`, - `--cordova-base-path=${root}`, - ]); - }); - - it('should pass verbose flag', async () => { - const project = {}; - const cli = new AngularBuildCLI({ project } as any); - const options = { - ...defaults, - verbose: true, - }; - - const result = await (cli as any).buildOptionsToNgArgs(options); - expect(result).toEqual(['--verbose']); - }); - - it('should pass separated options', async () => { - const project = {}; - const cli = new AngularBuildCLI({ project } as any); - const options = { - ...defaults, - '--': ['--extra=true'], - }; - - const result = await (cli as any).buildOptionsToNgArgs(options); - expect(result).toEqual(['--extra=true']); - }); - - it('should not pass separated options for cordova', async () => { - const root = 'fakeRoot'; - const project = { - requireIntegration: jest.fn(() => ({ root })), - }; - const cli = new AngularBuildCLI({ project } as any); - const options = { - ...defaults, - engine: 'cordova', - platform: 'fakePlatform', - '--': ['--extra=true'], - }; - - const result = await (cli as any).buildOptionsToNgArgs(options); - expect(result).not.toEqual(expect.arrayContaining(['--extra=true'])); - }); - - it('should pass configuration and project for custom program', async () => { - const project = {}; - const cli = new AngularBuildCLI({ project } as any); - (cli as any)._resolvedProgram = 'npm'; - const options = { - ...defaults, - configuration: 'production', - project: 'otherProject', - }; - - const result = await (cli as any).buildOptionsToNgArgs(options); - expect(result).toEqual(['--configuration=production', '--project=otherProject']); - }); - - it('should not pass configuration and project for custom program if they are the defaults', async () => { - const project = {}; - const cli = new AngularBuildCLI({ project } as any); - (cli as any)._resolvedProgram = 'npm'; - const options = { - ...defaults, - }; - - const result = await (cli as any).buildOptionsToNgArgs(options); - expect(result).toEqual([]); - }); - - }); - - }); - - }); - -}); diff --git a/packages/@ionic/cli/src/lib/project/angular/__tests__/generate.ts b/packages/@ionic/cli/src/lib/project/angular/__tests__/generate.ts deleted file mode 100644 index 62cc285182..0000000000 --- a/packages/@ionic/cli/src/lib/project/angular/__tests__/generate.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { CommandLineOptions } from '../../../../definitions'; -import { AngularGenerateRunner } from '../generate'; - -describe('@ionic/cli', () => { - - describe('lib/project/angular/generate', () => { - - describe('AngularGenerateRunner', () => { - - describe('createOptionsFromCommandLine', () => { - - const defaults = { - name: undefined, - project: 'app', - schematic: undefined, - }; - - it('should provide defaults with no inputs or options', () => { - const runner = new AngularGenerateRunner({} as any); - const result = runner.createOptionsFromCommandLine([], {} as CommandLineOptions); - expect(result).toEqual(defaults); - }); - - it('should provide options from inputs', () => { - const runner = new AngularGenerateRunner({} as any); - const result = runner.createOptionsFromCommandLine(['service', 'FancyBar'], { _: [] }); - expect(result).toEqual({ ...defaults, name: 'FancyBar', schematic: 'service' }); - }); - - it('should respect --project', () => { - const runner = new AngularGenerateRunner({} as any); - const result = runner.createOptionsFromCommandLine([], { _: [], project: 'otherProject' }); - expect(result).toEqual({ ...defaults, project: 'otherProject' }); - }); - - }); - - }); - - }); - -}); diff --git a/packages/@ionic/cli/src/lib/project/angular/__tests__/index.ts b/packages/@ionic/cli/src/lib/project/angular/__tests__/index.ts deleted file mode 100644 index 3157342c9a..0000000000 --- a/packages/@ionic/cli/src/lib/project/angular/__tests__/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -import * as path from 'path' -import { AngularProject } from '../'; - -describe('@ionic/cli', () => { - - describe('lib/project/angular', () => { - - describe('AngularProject', () => { - - let p: AngularProject; - - beforeEach(() => { - p = new AngularProject({ context: 'app', configPath: '/path/to/proj/file' } as any, {} as any); - jest.spyOn(p, 'config', 'get').mockImplementation(() => ({ get: () => undefined } as any)); - }); - - it('should set directory attribute', async () => { - expect(p.directory).toEqual(path.resolve('/path/to/proj')); - }); - - describe('getSourceDir', () => { - - it('should default to src', async () => { - const result = await p.getSourceDir(); - expect(result).toEqual(path.resolve('/path/to/proj/src')); - }); - - }); - - }); - - }); - -}); diff --git a/packages/@ionic/cli/src/lib/project/angular/__tests__/serve.ts b/packages/@ionic/cli/src/lib/project/angular/__tests__/serve.ts deleted file mode 100644 index dbfe0e850c..0000000000 --- a/packages/@ionic/cli/src/lib/project/angular/__tests__/serve.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { CommandLineOptions } from '../../../../definitions'; -import { AngularServeCLI, AngularServeRunner } from '../serve'; - -describe('@ionic/cli', () => { - - describe('lib/project/angular/serve', () => { - - describe('AngularServeRunner', () => { - - describe('createOptionsFromCommandLine', () => { - - const defaults = { - '--': [], - publicHost: undefined, - host: 'localhost', - browser: undefined, - browserOption: undefined, - engine: 'browser', - externalAddressRequired: false, - livereload: true, - open: false, - port: 8100, - proxy: true, - project: 'app', - prod: undefined, - platform: undefined, - verbose: false, - }; - - it('should provide defaults with no options', () => { - const runner = new AngularServeRunner({} as any); - const result = runner.createOptionsFromCommandLine([], {} as CommandLineOptions); - expect(result).toEqual(defaults); - }); - - it('should provide options from negations of cli flag defaults', () => { - const runner = new AngularServeRunner({} as any); - const result = runner.createOptionsFromCommandLine([], { _: [], livereload: false, proxy: false, open: true, externalAddressRequired: true }); - expect(result).toEqual({ ...defaults, livereload: false, proxy: false, open: true, externalAddressRequired: true }); - }); - - it('should allow overrides of default values', () => { - const runner = new AngularServeRunner({} as any); - const result = runner.createOptionsFromCommandLine([], { _: [], host: '0.0.0.0', port: '1111' }); - expect(result).toEqual({ ...defaults, host: '0.0.0.0', port: 1111 }); - }); - - it('should respect --external flag', () => { - const runner = new AngularServeRunner({} as any); - const result = runner.createOptionsFromCommandLine([], { _: [], host: 'localhost', external: true }); - expect(result).toEqual({ ...defaults, host: '0.0.0.0' }); - }); - - it('should respect --consolelogs flag', () => { - const runner = new AngularServeRunner({} as any); - const result = runner.createOptionsFromCommandLine([], { _: [], consolelogs: true }); - expect(result).toEqual({ ...defaults, consolelogs: true, consolelogsPort: 53703 }); - }); - - it('should respect --project and --configuration flags', () => { - const runner = new AngularServeRunner({} as any); - const result = runner.createOptionsFromCommandLine([], { _: [], project: 'otherProject', configuration: 'production' }); - expect(result).toEqual({ ...defaults, project: 'otherProject', configuration: 'production' }); - }); - - it('should pass on separated args', () => { - const runner = new AngularServeRunner({} as any); - const result = runner.createOptionsFromCommandLine([], { _: [], '--': ['foo', '--bar'] }); - expect(result).toEqual({ ...defaults, '--': ['foo', '--bar'] }); - }); - - }); - - }); - - describe('AngularServeCLI', () => { - - describe('serveOptionsToNgArgs', () => { - - const defaults = { - '--': [], - }; - - it('should pass options', async () => { - const project = {}; - const cli = new AngularServeCLI({ project } as any); - const options = { - ...defaults, - host: 'localhost', - port: 4200, - sourcemaps: true, - ssl: true, - }; - - const result = await (cli as any).serveOptionsToNgArgs(options); - expect(result).toEqual([ - `--host=${options.host}`, - `--port=${options.port}`, - `--source-map`, - `--ssl`, - ]); - }); - - it('should pass verbose flag', async () => { - const project = {}; - const cli = new AngularServeCLI({ project } as any); - const options = { - ...defaults, - verbose: true, - }; - - const result = await (cli as any).serveOptionsToNgArgs(options); - expect(result).toEqual(['--verbose']); - }); - - it('should pass cordova options', async () => { - const root = 'fakeRoot'; - const project = { - requireIntegration: jest.fn(() => ({ root })), - }; - const cli = new AngularServeCLI({ project } as any); - const options = { - ...defaults, - engine: 'cordova', - platform: 'fakePlatform', - }; - - const result = await (cli as any).serveOptionsToNgArgs(options); - expect(result).toEqual([ - `--platform=${options.platform}`, - `--cordova-base-path=${root}`, - ]); - }); - - it('should pass separated options', async () => { - const project = {}; - const cli = new AngularServeCLI({ project } as any); - const options = { - ...defaults, - '--': ['--extra=true'], - }; - - const result = await (cli as any).serveOptionsToNgArgs(options); - expect(result).toEqual(['--extra=true']); - }); - - it('should not pass separated options for cordova', async () => { - const root = 'fakeRoot'; - const project = { - requireIntegration: jest.fn(() => ({ root })), - }; - const cli = new AngularServeCLI({ project } as any); - const options = { - ...defaults, - engine: 'cordova', - platform: 'fakePlatform', - '--': ['--extra=true'], - }; - - const result = await (cli as any).serveOptionsToNgArgs(options); - expect(result).not.toEqual(expect.arrayContaining(['--extra=true'])); - }); - - it('should pass configuration and project for custom program', async () => { - const project = {}; - const cli = new AngularServeCLI({ project } as any); - (cli as any)._resolvedProgram = 'npm'; - const options = { - ...defaults, - configuration: 'production', - project: 'otherProject', - }; - - const result = await (cli as any).serveOptionsToNgArgs(options); - expect(result).toEqual(['--configuration=production', '--project=otherProject']); - }); - - it('should not pass configuration and project for custom program if they are the defaults', async () => { - const project = {}; - const cli = new AngularServeCLI({ project } as any); - (cli as any)._resolvedProgram = 'npm'; - const options = { - ...defaults, - }; - - const result = await (cli as any).serveOptionsToNgArgs(options); - expect(result).toEqual([]); - }); - - }); - - }); - - }); - -}); diff --git a/packages/@ionic/cli/src/lib/project/angular/build.ts b/packages/@ionic/cli/src/lib/project/angular/build.ts deleted file mode 100644 index 845108eb36..0000000000 --- a/packages/@ionic/cli/src/lib/project/angular/build.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { MetadataGroup, ParsedArgs, unparseArgs } from '@ionic/cli-framework'; - -import { AngularBuildOptions, CommandLineInputs, CommandLineOptions, CommandMetadata } from '../../../definitions'; -import { BUILD_SCRIPT, BuildCLI, BuildRunner, BuildRunnerDeps } from '../../build'; -import { input, weak } from '../../color'; - -import { AngularProject } from './'; - -const NG_BUILD_OPTIONS = [ - { - name: 'configuration', - aliases: ['c'], - summary: 'Specify the configuration to use.', - type: String, - groups: [MetadataGroup.ADVANCED, 'cordova'], - hint: weak('[ng]'), - spec: { value: 'conf' }, - }, - { - name: 'source-map', - summary: 'Output source maps', - type: Boolean, - groups: [MetadataGroup.ADVANCED, 'cordova'], - hint: weak('[ng]'), - }, - { - name: 'watch', - summary: 'Rebuild when files change', - type: Boolean, - groups: [MetadataGroup.ADVANCED], - hint: weak('[ng]'), - }, -]; - -export interface AngularBuildRunnerDeps extends BuildRunnerDeps { - readonly project: AngularProject; -} - -export class AngularBuildRunner extends BuildRunner { - constructor(protected readonly e: AngularBuildRunnerDeps) { - super(); - } - - async getCommandMetadata(): Promise> { - return { - description: ` -${input('ionic build')} uses the Angular CLI. Use ${input('ng build --help')} to list all Angular CLI options for building your app. See the ${input('ng build')} docs[^ng-build-docs] for explanations. Options not listed below are considered advanced and can be passed to the ${input('ng')} CLI using the ${input('--')} separator after the Ionic CLI arguments. See the examples. -`, - footnotes: [ - { - id: 'ng-build-docs', - url: 'https://angular.io/cli/build', - }, - ], - options: [ - { - name: 'prod', - summary: `Flag to use the ${input('production')} configuration`, - type: Boolean, - hint: weak('[ng]'), - groups: ['cordova'], - }, - ...NG_BUILD_OPTIONS, - { - name: 'cordova-assets', - summary: 'Do not bundle Cordova assets during Cordova build', - type: Boolean, - groups: [MetadataGroup.HIDDEN, 'cordova'], - default: true, - }, - ], - exampleCommands: [ - '--prod', - '--watch', - ], - }; - } - - createOptionsFromCommandLine(inputs: CommandLineInputs, options: CommandLineOptions): AngularBuildOptions { - const baseOptions = super.createBaseOptionsFromCommandLine(inputs, options); - const prod = options['prod'] ? Boolean(options['prod']) : undefined; - const configuration = options['configuration'] ? String(options['configuration']) : (prod ? 'production' : undefined); - const project = options['project'] ? String(options['project']) : 'app'; - const sourcemaps = typeof options['source-map'] === 'boolean' ? Boolean(options['source-map']) : undefined; - const cordovaAssets = typeof options['cordova-assets'] === 'boolean' ? Boolean(options['cordova-assets']) : undefined; - const watch = typeof options['watch'] === 'boolean' ? Boolean(options['watch']) : undefined; - - return { - ...baseOptions, - configuration, - project, - sourcemaps, - cordovaAssets, - watch, - type: 'angular', - }; - } - - async buildProject(options: AngularBuildOptions): Promise { - const ng = new AngularBuildCLI(this.e); - await ng.build(options); - } -} - -export class AngularBuildCLI extends BuildCLI { - readonly name = 'Angular CLI'; - readonly pkg = '@angular/cli'; - readonly program = 'ng'; - readonly prefix = 'ng'; - readonly script = BUILD_SCRIPT; - - protected async buildArgs(options: AngularBuildOptions): Promise { - const { pkgManagerArgs } = await import('../../utils/npm'); - - const args = await this.buildOptionsToNgArgs(options); - - if (this.resolvedProgram === this.program) { - return [...this.buildArchitectCommand(options), ...args]; - } else { - const [ , ...pkgArgs ] = await pkgManagerArgs(this.e.config.get('npmClient'), { command: 'run', script: this.script, scriptArgs: [...args] }); - return pkgArgs; - } - } - - protected async buildOptionsToNgArgs(options: AngularBuildOptions): Promise { - const args: ParsedArgs = { - _: [], - 'source-map': options.sourcemaps !== false ? options.sourcemaps : 'false', - 'cordova-assets': options.cordovaAssets !== false ? undefined : 'false', - 'watch': options.watch !== false ? options.watch : 'false', - }; - - const projectArgs = []; - let separatedArgs = options['--']; - - if (options.engine === 'cordova') { - const integration = this.e.project.requireIntegration('cordova'); - args.platform = options.platform; - - if (this.e.project.rootDirectory !== integration.root) { - args.cordovaBasePath = integration.root; - } - - separatedArgs = []; - } - - if (this.resolvedProgram !== this.program) { - if (options.configuration) { - projectArgs.push(`--configuration=${options.configuration}`); - } - - if (options.project) { - projectArgs.push(`--project=${options.project}`); - } - } - - if (options.verbose) { - projectArgs.push('--verbose'); - } - - return [...unparseArgs(args), ...projectArgs, ...separatedArgs]; - } - - protected buildArchitectCommand(options: AngularBuildOptions): string[] { - const cmd = options.engine === 'cordova' ? 'ionic-cordova-build' : 'build'; - - return ['run', `${options.project}:${cmd}${options.configuration ? `:${options.configuration}` : ''}`]; - } -} diff --git a/packages/@ionic/cli/src/lib/project/angular/generate.ts b/packages/@ionic/cli/src/lib/project/angular/generate.ts deleted file mode 100644 index 7324685b27..0000000000 --- a/packages/@ionic/cli/src/lib/project/angular/generate.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { unparseArgs, validators } from '@ionic/cli-framework'; -import { debug as Debug } from 'debug'; -import * as lodash from 'lodash'; - -import { AngularGenerateOptions, CommandLineInputs, CommandLineOptions, CommandMetadata } from '../../../definitions'; -import { ancillary, input, strong } from '../../color'; -import { GLOBAL_OPTIONS } from '../../config'; -import { FatalException } from '../../errors'; -import { GenerateRunner, GenerateRunnerDeps } from '../../generate'; - -import { AngularProject } from './'; - -// https://github.com/ionic-team/angular-toolkit/blob/master/collection.json -const SCHEMATICS: readonly string[] = ['page', 'component', 'service', 'module', 'class', 'directive', 'guard', 'pipe', 'interface', 'enum']; -const SCHEMATIC_ALIAS = new Map([ - ['pg', 'page'], - ['cl', 'class'], - ['c', 'component'], - ['d', 'directive'], - ['e', 'enum'], - ['g', 'guard'], - ['i', 'interface'], - ['m', 'module'], - ['p', 'pipe'], - ['s', 'service'], -]); - -const debug = Debug('ionic:lib:project:angular:generate'); - -export interface AngularGenerateRunnerDeps extends GenerateRunnerDeps { - readonly project: AngularProject; -} - -export class AngularGenerateRunner extends GenerateRunner { - constructor(protected readonly e: AngularGenerateRunnerDeps) { - super(); - } - - async getCommandMetadata(): Promise> { - return { - description: ` -Automatically create framework features with Ionic Generate. This command uses the Angular CLI to generate features such as ${['pages', 'components', 'directives', 'services'].map(c => input(c)).join(', ')}, and more. - - - For a full list of available types, use ${input('npx ng g --help')} - - For a list of options for a types, use ${input('npx ng g --help')} - -You can specify a path to nest your feature within any number of subdirectories. For example, specify a name of ${input('"pages/New Page"')} to generate page files at ${strong('src/app/pages/new-page/')}. - -To test a generator before file modifications are made, use the ${input('--dry-run')} option. - `, - exampleCommands: [ - 'page', - 'page contact', - 'component contact/form', - 'component login-form --change-detection=OnPush', - 'directive ripple --skip-import', - 'service api/user', - ], - inputs: [ - { - name: 'schematic', - summary: `The type of feature (e.g. ${['page', 'component', 'directive', 'service'].map(c => input(c)).join(', ')})`, - validators: [validators.required], - }, - { - name: 'name', - summary: 'The name/path of the feature being generated', - validators: [validators.required], - }, - ], - }; - } - - async ensureCommandLine(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - if (inputs[0]) { - this.validateFeatureType(inputs[0]); - } else { - const schematic = await this.e.prompt({ - type: 'list', - name: 'schematic', - message: 'What would you like to generate?', - choices: SCHEMATICS, - }); - - inputs[0] = schematic; - } - - if (!inputs[1]) { - const schematic = SCHEMATIC_ALIAS.get(inputs[0]) || inputs[0]; - const name = await this.e.prompt({ - type: 'input', - name: 'name', - message: `Name/path of ${input(schematic)}:`, - validate: v => validators.required(v), - }); - - inputs[1] = name.trim(); - } - } - - createOptionsFromCommandLine(inputs: CommandLineInputs, options: CommandLineOptions): AngularGenerateOptions { - const [schematic, name] = inputs; - const baseOptions = { name, schematic } - const project = options['project'] ? String(options['project']) : 'app'; - - // TODO: this is a little gross, is there a better way to pass in all the - // options that the command got? - return { - ...lodash.omit(options, '_', '--', ...GLOBAL_OPTIONS.map(opt => opt.name)), - project, - ...baseOptions, - }; - } - - async run(options: AngularGenerateOptions) { - const { name } = options; - const schematic = SCHEMATIC_ALIAS.get(options.schematic) || options.schematic; - - try { - await this.generateComponent(schematic, name, lodash.omit(options, 'schematic', 'name')); - } catch (e: any) { - debug(e); - throw new FatalException(`Could not generate ${input(schematic)}.`); - } - - if (!options['dry-run']) { - this.e.log.ok(`Generated ${input(schematic)}!`); - } - } - - private validateFeatureType(schematic: string) { - if (schematic === 'provider') { - throw new FatalException( - `Please use ${input('ionic generate service')} for generating service providers.\n` + - `For more information, please see the Angular documentation${ancillary('[1]')} on services.\n\n` + - `${ancillary('[1]')}: ${strong('https://angular.io/guide/architecture-services')}` - ); - } - - if (!SCHEMATICS.includes(schematic) && !SCHEMATIC_ALIAS.get(schematic)) { - throw new FatalException( - `${input(schematic)} is not a known feature.\n` + - `Use ${input('npx ng g --help')} to list available types of features.` - ); - } - } - - private async generateComponent(schematic: string, name: string, options: { [key: string]: string | boolean; }) { - const args = { - _: ['generate', schematic, name], - // convert `--no-` style options to `--=false` - ...lodash.mapValues(options, v => v === false ? 'false' : v), - }; - - await this.e.shell.run( - 'ng', - unparseArgs(args, { useEquals: true }), - { cwd: this.e.ctx.execPath, stdio: 'inherit' } - ); - } -} diff --git a/packages/@ionic/cli/src/lib/project/angular/index.ts b/packages/@ionic/cli/src/lib/project/angular/index.ts deleted file mode 100644 index 71803ab1d7..0000000000 --- a/packages/@ionic/cli/src/lib/project/angular/index.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { debug as Debug } from 'debug'; -import * as lodash from 'lodash'; -import * as path from 'path'; - -import { Project } from '../'; -import { InfoItem } from '../../../definitions'; -import { strong } from '../../color'; - -const debug = Debug('ionic:lib:project:angular'); - -export class AngularProject extends Project { - readonly type: 'angular' = 'angular'; - - async getInfo(): Promise { - const [ - [ionicAngularPkg, ionicAngularPkgPath], - [ionicAngularToolkitPkg, ionicAngularToolkitPkgPath], - [angularCLIPkg, angularCLIPkgPath], - [angularDevKitBuildAngularPkg, angularDevKitBuildAngularPkgPath], - [angularDevKitSchematicsPkg, angularDevKitSchematicsPkgPath], - ] = await Promise.all([ - this.getPackageJson('@ionic/angular'), - this.getPackageJson('@ionic/angular-toolkit'), - this.getPackageJson('@angular/cli'), - this.getPackageJson('@angular-devkit/build-angular'), - this.getPackageJson('@angular-devkit/schematics'), - ]); - - return [ - ...(await super.getInfo()), - { - group: 'ionic', - name: 'Ionic Framework', - key: 'framework', - value: ionicAngularPkg ? `@ionic/angular ${ionicAngularPkg.version}` : 'not installed', - path: ionicAngularPkgPath, - }, - { - group: 'ionic', - name: '@ionic/angular-toolkit', - key: 'ionic_angular_toolkit_version', - value: ionicAngularToolkitPkg ? ionicAngularToolkitPkg.version : 'not installed', - path: ionicAngularToolkitPkgPath, - }, - { - group: 'ionic', - name: '@angular/cli', - key: 'angular_cli_version', - value: angularCLIPkg ? angularCLIPkg.version : 'not installed', - path: angularCLIPkgPath, - }, - { - group: 'ionic', - name: '@angular-devkit/build-angular', - value: angularDevKitBuildAngularPkg ? angularDevKitBuildAngularPkg.version : 'not installed', - path: angularDevKitBuildAngularPkgPath, - }, - { - group: 'ionic', - name: '@angular-devkit/schematics', - value: angularDevKitSchematicsPkg ? angularDevKitSchematicsPkg.version : 'not installed', - path: angularDevKitSchematicsPkgPath, - }, - ]; - } - - async detected() { - try { - const pkg = await this.requirePackageJson(); - const deps = lodash.assign({}, pkg.dependencies, pkg.devDependencies); - - if (typeof deps['@ionic/angular'] === 'string') { - debug(`${strong('@ionic/angular')} detected in ${strong('package.json')}`); - return true; - } - } catch (e: any) { - // ignore - } - - return false; - } - - async requireBuildRunner(): Promise { - const { AngularBuildRunner } = await import('./build'); - const deps = { ...this.e, project: this }; - return new AngularBuildRunner(deps); - } - - async requireServeRunner(): Promise { - const { AngularServeRunner } = await import('./serve'); - const deps = { ...this.e, project: this }; - return new AngularServeRunner(deps); - } - - async requireGenerateRunner(): Promise { - const { AngularGenerateRunner } = await import('./generate'); - const deps = { ...this.e, project: this }; - return new AngularGenerateRunner(deps); - } - - setPrimaryTheme(themeColor: string): Promise { - const themePath = path.join(this.directory, 'src', 'theme', 'variables.scss'); - return this.writeThemeColor(themePath, themeColor); - } -} diff --git a/packages/@ionic/cli/src/lib/project/angular/serve.ts b/packages/@ionic/cli/src/lib/project/angular/serve.ts deleted file mode 100644 index a306d0484c..0000000000 --- a/packages/@ionic/cli/src/lib/project/angular/serve.ts +++ /dev/null @@ -1,261 +0,0 @@ -import { MetadataGroup, ParsedArgs, unparseArgs } from '@ionic/cli-framework'; -import { str2num } from '@ionic/cli-framework/utils/string'; -import { findClosestOpenPort } from '@ionic/utils-network'; -import { stripAnsi } from '@ionic/utils-terminal'; -import chalk from 'chalk'; - -import { AngularServeOptions, CommandLineInputs, CommandLineOptions, CommandMetadata, ServeDetails } from '../../../definitions'; -import { input, strong, weak } from '../../color'; -import { BIND_ALL_ADDRESS, DEFAULT_DEV_LOGGER_PORT as DEFAULT_CONSOLE_LOGS_PORT, LOCAL_ADDRESSES, SERVE_SCRIPT, ServeCLI, ServeRunner, ServeRunnerDeps } from '../../serve'; - -import { AngularProject } from './'; - -export interface AngularServeRunnerDeps extends ServeRunnerDeps { - readonly project: AngularProject; -} - -export class AngularServeRunner extends ServeRunner { - constructor(protected readonly e: AngularServeRunnerDeps) { - super(); - } - - async getCommandMetadata(): Promise> { - return { - description: ` -${input('ionic serve')} uses the Angular CLI. Use ${input('ng serve --help')} to list all Angular CLI options for serving your app. See the ${input('ng serve')} docs[^ng-serve-docs] for explanations. Options not listed below are considered advanced and can be passed to the Angular CLI using the ${input('--')} separator after the Ionic CLI arguments. See the examples. - -The dev server can use HTTPS via the ${input('--ssl')} option ${chalk.bold.red('(experimental)')}. There are several known issues with HTTPS. See issue #3305[^issue-3305]. -`, - footnotes: [ - { - id: 'ng-serve-docs', - url: 'https://angular.io/cli/serve', - }, - { - id: 'issue-3305', - url: 'https://github.com/ionic-team/ionic-cli/issues/3305', - }, - ], - options: [ - { - name: 'consolelogs', - summary: 'Print app console logs to the terminal', - type: Boolean, - groups: [MetadataGroup.ADVANCED, 'cordova'], - hint: weak('[ng]'), - }, - { - name: 'consolelogs-port', - summary: 'Use specific port for console logs server', - type: String, - groups: [MetadataGroup.ADVANCED, 'cordova'], - hint: weak('[ng]'), - spec: { value: 'port' }, - }, - { - name: 'ssl', - summary: 'Use HTTPS for the dev server', - type: Boolean, - groups: [MetadataGroup.EXPERIMENTAL, 'cordova'], - hint: weak('[ng]'), - }, - { - name: 'prod', - summary: `Flag to use the ${input('production')} configuration`, - type: Boolean, - groups: ['cordova'], - hint: weak('[ng]'), - }, - { - name: 'configuration', - summary: 'Specify the configuration to use.', - type: String, - groups: [MetadataGroup.ADVANCED, 'cordova'], - aliases: ['c'], - hint: weak('[ng]'), - spec: { value: 'conf' }, - }, - { - name: 'source-map', - summary: 'Output sourcemaps', - type: Boolean, - groups: [MetadataGroup.ADVANCED, 'cordova'], - hint: weak('[ng]'), - }, - ], - exampleCommands: [ - '-- --proxy-config proxy.conf.json', - ], - }; - } - - createOptionsFromCommandLine(inputs: CommandLineInputs, options: CommandLineOptions): AngularServeOptions { - const baseOptions = super.createOptionsFromCommandLine(inputs, options); - const prod = options['prod'] ? Boolean(options['prod']) : undefined; - const ssl = options['ssl'] ? Boolean(options['ssl']) : undefined; - const configuration = options['configuration'] ? String(options['configuration']) : (prod ? 'production' : undefined); - const project = options['project'] ? String(options['project']) : 'app'; - const sourcemaps = typeof options['source-map'] === 'boolean' ? Boolean(options['source-map']) : undefined; - const consolelogs = typeof options['consolelogs'] === 'boolean' ? Boolean(options['consolelogs']) : undefined; - const consolelogsPort = consolelogs ? str2num(options['consolelogs-port'], DEFAULT_CONSOLE_LOGS_PORT) : undefined; - - return { - ...baseOptions, - consolelogs, - consolelogsPort, - ssl, - configuration, - project, - sourcemaps, - }; - } - - platformToMode(platform: string): string { - if (platform === 'ios') { - return 'ios'; - } - - return 'md'; - } - - modifyOpenUrl(url: string, options: AngularServeOptions): string { - return `${url}${options.browserOption ? options.browserOption : ''}${options.platform ? `?ionic:mode=${this.platformToMode(options.platform)}&ionic:persistConfig=true` : ''}`; - } - - async serveProject(options: AngularServeOptions): Promise { - const [externalIP, availableInterfaces] = await this.selectExternalIP(options); - - const port = options.port = await findClosestOpenPort(options.port); - - const ng = new AngularServeCLI(this.e); - await ng.serve(options); - - return { - custom: ng.resolvedProgram !== ng.program, - protocol: options.ssl ? 'https' : 'http', - localAddress: 'localhost', - externalAddress: externalIP, - externalNetworkInterfaces: availableInterfaces, - port, - externallyAccessible: ![BIND_ALL_ADDRESS, ...LOCAL_ADDRESSES].includes(externalIP), - }; - } - - getUsedPorts(options: AngularServeOptions, details: ServeDetails): number[] { - return [ - ...super.getUsedPorts(options, details), - ...options.consolelogsPort ? [options.consolelogsPort] : [], - ]; - } -} - -export class AngularServeCLI extends ServeCLI { - readonly name = 'Angular CLI'; - readonly pkg = '@angular/cli'; - readonly program = 'ng'; - readonly prefix = 'ng'; - readonly script = SERVE_SCRIPT; - protected chunks = 0; - - async serve(options: AngularServeOptions): Promise { - this.on('compile', chunks => { - if (chunks > 0) { - this.e.log.info(`... and ${strong(chunks.toString())} additional chunks`); - } - }); - - return super.serve(options); - } - - protected stdoutFilter(line: string): boolean { - if (this.resolvedProgram !== this.program) { - return super.stdoutFilter(line); - } - - const strippedLine = stripAnsi(line); - const compileMsgs = [ - 'Development Server is listening', - 'Watching for file changes' - ] - if (compileMsgs.some((msg) => strippedLine.includes(msg))) { - this.emit('ready'); - return false; - } - - if (strippedLine.match(/.*chunk\s{\d+}.+/)) { - this.chunks++; - return false; - } - - if (strippedLine.includes('Compiled successfully')) { - this.emit('compile', this.chunks); - this.chunks = 0; - } - - return true; - } - - protected async buildArgs(options: AngularServeOptions): Promise { - const { pkgManagerArgs } = await import('../../utils/npm'); - - const args = await this.serveOptionsToNgArgs(options); - - if (this.resolvedProgram === this.program) { - return [...this.buildArchitectCommand(options), ...args]; - } else { - const [, ...pkgArgs] = await pkgManagerArgs(this.e.config.get('npmClient'), { command: 'run', script: this.script, scriptArgs: [...args] }); - return pkgArgs; - } - } - - protected async serveOptionsToNgArgs(options: AngularServeOptions): Promise { - const args: ParsedArgs = { - _: [], - host: options.host, - port: options.port ? options.port.toString() : undefined, - 'source-map': options.sourcemaps !== false ? options.sourcemaps : 'false', - 'ssl': options.ssl !== false ? options.ssl : 'false', - }; - - const projectArgs = []; - let separatedArgs = options['--']; - - if (options.engine === 'cordova') { - const integration = this.e.project.requireIntegration('cordova'); - args.platform = options.platform; - - if (this.e.project.rootDirectory !== integration.root) { - args.cordovaBasePath = integration.root; - } - - separatedArgs = []; - - args.consolelogs = options.consolelogs ? true : undefined; - args['consolelogs-port'] = options.consolelogsPort ? String(options.consolelogsPort) : undefined; - } else { - args['public-host'] = options.publicHost; // TODO: @ionic/angular-toolkit would need to support --public-host - } - - if (this.resolvedProgram !== this.program) { - if (options.configuration) { - projectArgs.push(`--configuration=${options.configuration}`); - } - - if (options.project) { - projectArgs.push(`--project=${options.project}`); - } - } - - if (options.verbose) { - projectArgs.push('--verbose'); - } - - return [...unparseArgs(args), ...projectArgs, ...separatedArgs]; - } - - protected buildArchitectCommand(options: AngularServeOptions): string[] { - const cmd = options.engine === 'cordova' ? 'ionic-cordova-serve' : 'serve'; - - return ['run', `${options.project}:${cmd}${options.configuration ? `:${options.configuration}` : ''}`]; - } -} diff --git a/packages/@ionic/cli/src/lib/project/bare/index.ts b/packages/@ionic/cli/src/lib/project/bare/index.ts deleted file mode 100644 index 1d6c591bff..0000000000 --- a/packages/@ionic/cli/src/lib/project/bare/index.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Project } from '../'; -import { RunnerNotFoundException } from '../../errors'; - -export class BareProject extends Project { - readonly type: 'bare' = 'bare'; - - async detected() { - return false; - } - - async requireBuildRunner(): Promise { - throw new RunnerNotFoundException( - `Cannot perform build for bare projects.\n` + - `The Ionic CLI doesn't know how to build bare projects.` - ); - } - - async requireServeRunner(): Promise { - throw new RunnerNotFoundException( - `Cannot perform serve for bare projects.\n` + - `The Ionic CLI doesn't know how to serve bare projects.` - ); - } - - async requireGenerateRunner(): Promise { - throw new RunnerNotFoundException( - `Cannot perform generate for bare projects.\n` + - `The Ionic CLI doesn't know how to generate framework components for bare projects.` - ); - } -} diff --git a/packages/@ionic/cli/src/lib/project/custom/build.ts b/packages/@ionic/cli/src/lib/project/custom/build.ts deleted file mode 100644 index bd7b90c214..0000000000 --- a/packages/@ionic/cli/src/lib/project/custom/build.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { CommandLineInputs, CommandLineOptions, CommandMetadata, CustomBuildOptions } from '../../../definitions'; -import { BuildRunner, BuildRunnerDeps } from '../../build'; -import { input, strong } from '../../color'; -import { RunnerException } from '../../errors'; - -export class CustomBuildRunner extends BuildRunner { - constructor(protected readonly e: BuildRunnerDeps) { - super(); - } - - async getCommandMetadata(): Promise> { - return {}; - } - - createOptionsFromCommandLine(inputs: CommandLineInputs, options: CommandLineOptions): CustomBuildOptions { - const baseOptions = super.createBaseOptionsFromCommandLine(inputs, options); - - return { - ...baseOptions, - type: 'custom', - }; - } - - async buildProject(options: CustomBuildOptions): Promise { - const cli = this.getPkgManagerBuildCLI(); - - if (!await cli.resolveScript()) { - throw new RunnerException( - `Cannot perform build.\n` + - `Since you're using the ${strong('custom')} project type, you must provide the ${input(cli.script)} npm script so the Ionic CLI can build your project.` - ); - } - - await cli.build(options); - } -} diff --git a/packages/@ionic/cli/src/lib/project/custom/index.ts b/packages/@ionic/cli/src/lib/project/custom/index.ts deleted file mode 100644 index 3002af85e8..0000000000 --- a/packages/@ionic/cli/src/lib/project/custom/index.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Project } from '../'; -import { strong } from '../../color'; -import { RunnerNotFoundException } from '../../errors'; - -export class CustomProject extends Project { - readonly type: 'custom' = 'custom'; - - /** - * We can't detect custom project types. We don't know what they look like! - */ - async detected() { - return false; - } - - async requireBuildRunner(): Promise { - const { CustomBuildRunner } = await import('./build'); - const deps = { ...this.e, project: this }; - return new CustomBuildRunner(deps); - } - - async requireServeRunner(): Promise { - const { CustomServeRunner } = await import('./serve'); - const deps = { ...this.e, project: this }; - return new CustomServeRunner(deps); - } - - async requireGenerateRunner(): Promise { - throw new RunnerNotFoundException( - `Cannot perform generate for custom projects.\n` + - `Since you're using the ${strong('custom')} project type, this command won't work. The Ionic CLI doesn't know how to generate framework components for custom projects.` - ); - } -} diff --git a/packages/@ionic/cli/src/lib/project/custom/serve.ts b/packages/@ionic/cli/src/lib/project/custom/serve.ts deleted file mode 100644 index a48ce2af85..0000000000 --- a/packages/@ionic/cli/src/lib/project/custom/serve.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { findClosestOpenPort } from '@ionic/utils-network'; - -import { CommandMetadata, CustomServeOptions, ServeDetails } from '../../../definitions'; -import { input, strong } from '../../color'; -import { RunnerException } from '../../errors'; -import { BIND_ALL_ADDRESS, LOCAL_ADDRESSES, ServeRunner, ServeRunnerDeps } from '../../serve'; - -export class CustomServeRunner extends ServeRunner { - constructor(protected readonly e: ServeRunnerDeps) { - super(); - } - - async getCommandMetadata(): Promise> { - return {}; - } - - modifyOpenUrl(url: string, options: CustomServeOptions): string { - return url; - } - - async serveProject(options: CustomServeOptions): Promise { - const cli = this.getPkgManagerServeCLI(); - - if (!await cli.resolveScript()) { - throw new RunnerException( - `Cannot perform serve.\n` + - `Since you're using the ${strong('custom')} project type, you must provide the ${input(cli.script)} npm script so the Ionic CLI can serve your project.` - ); - } - - const [ externalIP, availableInterfaces ] = await this.selectExternalIP(options); - - const port = options.port = await findClosestOpenPort(options.port); - - await cli.serve(options); - - return { - custom: true, - protocol: 'http', - localAddress: 'localhost', - externalAddress: externalIP, - externalNetworkInterfaces: availableInterfaces, - port, - externallyAccessible: ![BIND_ALL_ADDRESS, ...LOCAL_ADDRESSES].includes(externalIP), - }; - } -} diff --git a/packages/@ionic/cli/src/lib/project/index.ts b/packages/@ionic/cli/src/lib/project/index.ts deleted file mode 100644 index 9e42e3db94..0000000000 --- a/packages/@ionic/cli/src/lib/project/index.ts +++ /dev/null @@ -1,791 +0,0 @@ -import { BaseConfig, BaseConfigOptions, PackageJson, ParsedArgs } from '@ionic/cli-framework'; -import { PromptModule } from '@ionic/cli-framework-prompts'; -import { resolveValue } from '@ionic/cli-framework/utils/fn'; -import { ERROR_INVALID_PACKAGE_JSON, compileNodeModulesPaths, isValidPackageName, readPackageJsonFile } from '@ionic/cli-framework/utils/node'; -import { ensureDir, findBaseDirectory, readFile, writeFile, writeJson } from '@ionic/utils-fs'; -import { TTY_WIDTH, prettyPath, wordWrap } from '@ionic/utils-terminal'; -import { debug as Debug } from 'debug'; -import * as lodash from 'lodash'; -import * as path from 'path'; - -import { PROJECT_FILE, PROJECT_TYPES, ANGULAR_STANDALONE } from '../../constants'; -import { IClient, IConfig, IIntegration, ILogger, IMultiProjectConfig, IProject, IProjectConfig, ISession, IShell, InfoItem, IntegrationName, IonicContext, IonicEnvironmentFlags, ProjectIntegration, ProjectPersonalizationDetails, ProjectType } from '../../definitions'; -import { isMultiProjectConfig, isProjectConfig } from '../../guards'; -import { ancillary, failure, input, strong } from '../color'; -import { BaseException, FatalException, IntegrationNotFoundException, RunnerNotFoundException } from '../errors'; -import { BaseIntegration } from '../integrations'; -import { Color } from '../utils/color'; -import type { Integration as CapacitorIntegration } from '../integrations/capacitor'; -import type { Integration as CordovaIntegration } from '../integrations/cordova'; -import type { Integration as EnterpriseIntegration } from '../integrations/enterprise'; - -const debug = Debug('ionic:lib:project'); - -export interface ProjectDetailsResultBase { - readonly type?: ProjectType; - readonly errors: readonly ProjectDetailsError[]; -} - -export interface ProjectDetailsSingleAppResult extends ProjectDetailsResultBase { - readonly context: 'app'; -} - -export interface ProjectDetailsMultiAppResult extends ProjectDetailsResultBase { - readonly context: 'multiapp'; - readonly id?: string; -} - -export interface ProjectDetailsUnknownResult extends ProjectDetailsResultBase { - readonly context: 'unknown'; -} - -export type ProjectDetailsResult = (ProjectDetailsSingleAppResult | ProjectDetailsMultiAppResult | ProjectDetailsUnknownResult) & { readonly configPath: string; }; - -export type ProjectDetailsErrorCode = 'ERR_INVALID_PROJECT_FILE' | 'ERR_INVALID_PROJECT_TYPE' | 'ERR_MISSING_PROJECT_TYPE' | 'ERR_MULTI_MISSING_CONFIG' | 'ERR_MULTI_MISSING_ID'; - -export class ProjectDetailsError extends BaseException { - constructor( - msg: string, - - /** - * Unique code for this error. - */ - readonly code: ProjectDetailsErrorCode, - - /** - * The underlying error that caused this error. - */ - cause?: Error - ) { - super(msg, { cause }); - } -} - -export interface ProjectDetailsDeps { - readonly rootDirectory: string; - readonly args?: ParsedArgs; - readonly e: ProjectDeps; -} - -export class ProjectDetails { - readonly rootDirectory: string; - - protected readonly e: ProjectDeps; - protected readonly args: ParsedArgs; - - constructor({ rootDirectory, args = { _: [] }, e }: ProjectDetailsDeps) { - this.rootDirectory = rootDirectory; - this.e = e; - this.args = args; - } - - async getIdFromArgs(): Promise { - const id = this.args && this.args['project'] ? String(this.args['project']) : undefined; - - if (id) { - debug(`Project id from args: ${strong(id)}`); - return id; - } - } - - async getIdFromPathMatch(config: IMultiProjectConfig): Promise { - const { ctx } = this.e; - - for (const [key, value] of lodash.entries(config.projects)) { - const id = key; - - if (value && value.root) { - const projectDir = path.resolve(this.rootDirectory, value.root); - - if (ctx.execPath.startsWith(projectDir)) { - debug(`Project id from path match: ${strong(id)}`); - return id; - } - } - } - } - - async getIdFromDefaultProject(config: IMultiProjectConfig): Promise { - const id = config.defaultProject; - - if (id) { - debug(`Project id from defaultProject: ${strong(id)}`); - return id; - } - } - - async getTypeFromConfig(config: IProjectConfig): Promise { - const { type } = config; - - if (type) { - debug(`Project type from config: ${strong(prettyProjectName(type))} ${type ? strong(`(${type})`) : ''}`); - return type; - } - } - - async getTypeFromDetection(): Promise { - for (const projectType of PROJECT_TYPES) { - const p = await createProjectFromDetails({ context: 'app', configPath: path.resolve(this.rootDirectory, PROJECT_FILE), type: projectType, errors: [] }, this.e); - const type = p.type; - - // TODO: This is a hack to avoid accessing `this.config` within the - // `Project.directory` getter, which writes config files. - Object.defineProperty(p, 'directory', { value: this.rootDirectory, writable: false }); - - if (await p.detected()) { - debug(`Project type from detection: ${strong(prettyProjectName(type))} ${type ? strong(`(${type})`) : ''}`); - return type; - } - } - } - - protected async determineSingleApp(config: IProjectConfig): Promise { - const errors: ProjectDetailsError[] = []; - - let type = await resolveValue( - async () => this.getTypeFromConfig(config), - async () => this.getTypeFromDetection() - ); - - if (!type) { - errors.push(new ProjectDetailsError('Could not determine project type', 'ERR_MISSING_PROJECT_TYPE')); - } else if (!PROJECT_TYPES.includes(type)) { - errors.push(new ProjectDetailsError(`Invalid project type: ${type}`, 'ERR_INVALID_PROJECT_TYPE')); - type = undefined; - } - - return { context: 'app', type, errors }; - } - - protected async determineMultiApp(config: IMultiProjectConfig): Promise { - const errors: ProjectDetailsError[] = []; - const id = await resolveValue( - async () => this.getIdFromArgs(), - async () => this.getIdFromPathMatch(config), - async () => this.getIdFromDefaultProject(config) - ); - - let type: ProjectType | undefined; - - if (id) { - const app = config.projects[id]; - - if (app) { - const r = await this.determineSingleApp(app); - type = r.type; - errors.push(...r.errors); - } else { - errors.push(new ProjectDetailsError('Could not find project in config', 'ERR_MULTI_MISSING_CONFIG')); - } - } else { - errors.push(new ProjectDetailsError('Could not determine project id', 'ERR_MULTI_MISSING_ID')); - } - - return { context: 'multiapp', id, type, errors }; - } - - processResult(result: ProjectDetailsResult): void { - const { log } = this.e; - const errorCodes = result.errors.map(e => e.code); - const e1 = result.errors.find(e => e.code === 'ERR_INVALID_PROJECT_FILE'); - const e2 = result.errors.find(e => e.code === 'ERR_INVALID_PROJECT_TYPE'); - - if (e1) { - log.error( - `Error while loading config (project config: ${strong(prettyPath(result.configPath))})\n` + - `${e1.cause ? `${e1.message}: ${failure(e1.cause.toString())}` : failure(e1.message)}. ` + - `Run ${input('ionic init')} to re-initialize your Ionic project. Without a valid project config, the CLI will not have project context.` - ); - - log.nl(); - } - - if (result.context === 'multiapp') { - if (errorCodes.includes('ERR_MULTI_MISSING_ID')) { - log.warn( - `Multi-app workspace detected, but cannot determine which project to use.\n` + - `Please set a ${input('defaultProject')} in ${strong(prettyPath(result.configPath))} or specify the project using the global ${input('--project')} option. Read the documentation${ancillary('[1]')} for more information.\n\n` + - `${ancillary('[1]')}: ${strong('https://ion.link/multi-app-docs')}` - ); - - log.nl(); - } - - if (result.id && errorCodes.includes('ERR_MULTI_MISSING_CONFIG')) { - log.warn( - `Multi-app workspace detected, but project was not found in configuration.\n` + - `Project ${input(result.id)} could not be found in the workspace. Did you add it to ${strong(prettyPath(result.configPath))}?` - ); - } - } - - if (errorCodes.includes('ERR_MISSING_PROJECT_TYPE')) { - const listWrapOptions = { width: TTY_WIDTH - 8 - 3, indentation: 1 }; - - log.warn( - `Could not determine project type (project config: ${strong(prettyPath(result.configPath))}).\n` + - `- ${wordWrap(`For ${strong(prettyProjectName('angular'))} projects, make sure ${input('@ionic/angular')} is listed as a dependency in ${strong('package.json')}.`, listWrapOptions)}\n` + - `Alternatively, set ${strong('type')} attribute in ${strong(prettyPath(result.configPath))} to one of: ${PROJECT_TYPES.map(v => input(v)).join(', ')}.\n\n` + - `If the Ionic CLI does not know what type of project this is, ${input('ionic build')}, ${input('ionic serve')}, and other commands may not work. You can use the ${input('custom')} project type if that's okay.` - ); - - log.nl(); - } - - if (e2) { - log.error( - `${e2.message} (project config: ${strong(prettyPath(result.configPath))}).\n` + - `Project type must be one of: ${PROJECT_TYPES.map(v => input(v)).join(', ')}` - ); - - log.nl(); - } - } - - async readConfig(p: string): Promise<{ [key: string]: any; }> { - try { - let configContents = await readFile(p, { encoding: 'utf8' }); - - if (!configContents) { - configContents = '{}\n'; - await writeFile(p, configContents, { encoding: 'utf8' }); - } - - return await JSON.parse(configContents); - } catch (e: any) { - throw new ProjectDetailsError('Could not read project file', 'ERR_INVALID_PROJECT_FILE', e); - } - } - - /** - * Gather project details from specified configuration. - * - * This method will always resolve with a result object, with an array of - * errors. Use `processResult()` to log warnings & errors. - */ - async result(): Promise { - const errors: ProjectDetailsError[] = []; - const configPath = path.resolve(this.rootDirectory, PROJECT_FILE); - let config: { [key: string]: any; } | undefined; - - try { - config = await this.readConfig(configPath); - - if (isProjectConfig(config)) { - const r = await this.determineSingleApp(config); - errors.push(...r.errors); - return { ...r, configPath, errors }; - } - - if (isMultiProjectConfig(config)) { - const r = await this.determineMultiApp(config); - errors.push(...r.errors); - return { ...r, configPath, errors }; - } - - throw new ProjectDetailsError('Unknown project file structure', 'ERR_INVALID_PROJECT_FILE'); - } catch (e: any) { - errors.push(e); - } - - return { configPath, context: 'unknown', errors }; - } -} - -export async function createProjectFromDetails(details: ProjectDetailsResult, deps: ProjectDeps): Promise { - const { context, type } = details; - - switch (type) { - case 'angular': - case ANGULAR_STANDALONE: - const { AngularProject } = await import('./angular'); - return new AngularProject(details, deps); - case 'react': - const { ReactProject } = await import('./react'); - return new ReactProject(details, deps); - case 'vue': - const { VueProject } = await import('./vue'); - return new VueProject(details, deps); - case 'vue-vite': - const { VueViteProject } = await import('./vue-vite'); - return new VueViteProject(details, deps); - case 'react-vite': - const { ReactViteProject } = await import('./react-vite'); - return new ReactViteProject(details, deps); - case 'custom': - const { CustomProject } = await import('./custom'); - return new CustomProject(details, deps); - } - - // If we can't match any of the types above, but we've detected a multi-app - // setup, it likely means this is a "bare" project, or a project without - // apps. This can occur when `ionic start` is used for the first time in a - // new multi-app setup. - if (context === 'multiapp') { - const { BareProject } = await import('./bare'); - return new BareProject(details, deps); - } - - throw new FatalException(`Bad project type: ${strong(String(type))}`); // TODO? -} - -export async function findProjectDirectory(cwd: string): Promise { - return findBaseDirectory(cwd, PROJECT_FILE); -} - -export interface CreateProjectFromDirectoryOptions { - logErrors?: boolean; -} - -export async function createProjectFromDirectory(rootDirectory: string, args: ParsedArgs, deps: ProjectDeps, { logErrors = true }: CreateProjectFromDirectoryOptions = {}): Promise { - const details = new ProjectDetails({ rootDirectory, args, e: deps }); - const result = await details.result(); - debug('Project details: %o', { ...result, errors: result.errors.map(e => e.code) }); - - if (logErrors) { - details.processResult(result); - } - - if (result.context === 'unknown') { - return; - } - - return createProjectFromDetails(result, deps); -} - -export interface ProjectConfigOptions extends BaseConfigOptions { - readonly type?: ProjectType; -} - -export class ProjectConfig extends BaseConfig { - protected readonly type?: ProjectType; - - constructor(p: string, { type, ...options }: ProjectConfigOptions = {}) { - super(p, options); - this.type = type; - - const c = this.c as any; - - if (typeof c.app_id === 'string') { // <4.0.0 project config migration - if (c.app_id && !c.id) { - // set `id` only if it has not been previously set and if `app_id` - // isn't an empty string (which it used to be, sometimes) - this.set('id', c.app_id); - } - - this.unset('app_id' as any); - } else if (typeof c.pro_id === 'string') { - if (!c.id) { - // set `id` only if it has not been previously set - this.set('id', c.pro_id); - } - - // we do not unset `pro_id` because it would break things - } - } - - provideDefaults(c: Partial>): IProjectConfig { - return lodash.assign({ - name: 'New Ionic App', - integrations: {}, - type: this.type, - }, c); - } -} - -export class MultiProjectConfig extends BaseConfig { - provideDefaults(c: Partial>): IMultiProjectConfig { - return lodash.assign({ - projects: {}, - }, c); - } -} - -export interface ProjectDeps { - readonly client: IClient; - readonly config: IConfig; - readonly flags: IonicEnvironmentFlags; - readonly log: ILogger; - readonly prompt: PromptModule; - readonly session: ISession; - readonly shell: IShell; - readonly ctx: IonicContext; -} - -export abstract class Project implements IProject { - readonly rootDirectory: string; - abstract readonly type: ProjectType; - protected originalConfigFile?: { [key: string]: any }; - - constructor( - readonly details: ProjectDetailsResult, - protected readonly e: ProjectDeps - ) { - this.rootDirectory = path.dirname(details.configPath); - } - - get filePath(): string { - return this.details.configPath; - } - - get directory(): string { - const root = this.config.get('root'); - - if (!root) { - return this.rootDirectory; - } - - return path.resolve(this.rootDirectory, root); - } - - get pathPrefix(): string[] { - const id = this.details.context === 'multiapp' ? this.details.id : undefined; - - return id ? ['projects', id] : []; - } - - get config(): ProjectConfig { - const options = { type: this.type, pathPrefix: this.pathPrefix }; - - return new ProjectConfig(this.filePath, options); - } - - abstract detected(): Promise; - - abstract requireBuildRunner(): Promise>; - abstract requireServeRunner(): Promise>; - abstract requireGenerateRunner(): Promise>; - async getBuildRunner(): Promise | undefined> { - try { - return await this.requireBuildRunner(); - } catch (e: any) { - if (!(e instanceof RunnerNotFoundException)) { - throw e; - } - } - } - - async getServeRunner(): Promise | undefined> { - try { - return await this.requireServeRunner(); - } catch (e: any) { - if (!(e instanceof RunnerNotFoundException)) { - throw e; - } - } - } - - async getGenerateRunner(): Promise | undefined> { - try { - return await this.requireGenerateRunner(); - } catch (e: any) { - if (!(e instanceof RunnerNotFoundException)) { - throw e; - } - } - } - - async requireAppflowId(): Promise { - const appflowId = this.config.get('id'); - - if (!appflowId) { - throw new FatalException( - `Your project file (${strong(prettyPath(this.filePath))}) does not contain '${strong('id')}'. ` + - `Run ${input('ionic link')}.` - ); - } - - return appflowId; - } - - get packageJsonPath() { - return path.resolve(this.directory, 'package.json'); - } - - async getPackageJson(pkgName?: string, { logErrors = true }: { logErrors?: boolean; } = {}): Promise<[PackageJson | undefined, string | undefined]> { - let pkg: PackageJson | undefined; - let pkgPath: string | undefined; - - try { - pkgPath = pkgName ? require.resolve(`${pkgName}/package.json`, { paths: compileNodeModulesPaths(this.directory) }) : this.packageJsonPath; - pkg = await readPackageJsonFile(pkgPath); - } catch (e: any) { - if (logErrors) { - this.e.log.warn(`Error loading ${strong(pkgName ? pkgName : `project's`)} ${strong('package.json')}: ${e}`); - } - } - - return [pkg, pkgPath ? path.dirname(pkgPath) : undefined]; - } - - async requirePackageJson(pkgName?: string): Promise { - try { - const pkgPath = pkgName ? require.resolve(`${pkgName}/package.json`, { paths: compileNodeModulesPaths(this.directory) }) : this.packageJsonPath; - return await readPackageJsonFile(pkgPath); - } catch (e: any) { - if (e instanceof SyntaxError) { - throw new FatalException(`Could not parse ${strong(pkgName ? pkgName : `project's`)} ${strong('package.json')}. Is it a valid JSON file?`); - } else if (e === ERROR_INVALID_PACKAGE_JSON) { - throw new FatalException(`The ${strong(pkgName ? pkgName : `project's`)} ${strong('package.json')} file seems malformed.`); - } - - throw e; // Probably file not found - } - } - - async getSourceDir(): Promise { - return path.resolve(this.directory, 'src'); - } - - async getDefaultDistDir(): Promise { - return 'www'; - } - - async getDistDir(): Promise { - if (this.getIntegration('capacitor') !== undefined) { - const capacitor = await this.createIntegration('capacitor'); - const conf = await capacitor.getCapacitorConfig(); - const webDir = conf?.webDir; - - if (webDir) { - return path.resolve(capacitor.root, webDir); - } else { - throw new FatalException( - `The ${input('webDir')} property must be set in the Capacitor configuration file. \n` + - `See the Capacitor docs for more information: ${strong('https://capacitor.ionicframework.com/docs/basics/configuring-your-app')}` - ); - } - } else { - return path.resolve(this.directory, 'www'); - } - } - - async getInfo(): Promise { - const integrations = await this.getIntegrations(); - const integrationInfo = lodash.flatten(await Promise.all(integrations.map(async i => i.getInfo()))); - - return integrationInfo; - } - - async personalize(details: ProjectPersonalizationDetails): Promise { - const { name, projectId, description, version, themeColor, appIcon, splash } = details; - - this.config.set('name', name); - - const pkg = await this.requirePackageJson(); - - pkg.name = projectId; - pkg.version = version ? version : '0.0.1'; - pkg.description = description ? description : 'An Ionic project'; - - await writeJson(this.packageJsonPath, pkg, { spaces: 2 }); - - if (themeColor) { - await this.setPrimaryTheme(themeColor); - } - - if (appIcon && splash) { - await this.setAppResources(appIcon, splash); - } - - const integrations = await this.getIntegrations(); - - await Promise.all(integrations.map(async i => i.personalize(details))); - } - - // Empty to avoid sub-classes having to implement - async setPrimaryTheme(_themeColor: string): Promise { } - - async writeThemeColor(variablesPath: string, themeColor: string): Promise { - const light = new Color(themeColor); - - const ionicThemeLightDarkMap = { - '#3880ff': '#4c8dff', // blue - '#5260ff': '#6a64ff', // purple - '#2dd36f': '#2fdf75', // green - '#ffc409': '#ffd534', // yellow - '#eb445a': '#ff4961', // red - '#f4f5f8': '#222428', // light - '#92949c': '#989aa2', // medium - '#222428': '#f4f5f8', // dark - } as { [key: string]: string }; - - const matchingThemeColor = ionicThemeLightDarkMap[themeColor]; - - let dark; - - // If this is a standard Ionic theme color, then use the hard-coded dark mode - // colors. Otherwise, compute a plausible dark mode color for this theme - if (matchingThemeColor) { - dark = new Color(matchingThemeColor); - } else if (light.yiq > 128) { - // Light mode was light enough, just use it for both - dark = light; - } else { - // Light mode was too dark, so tint it to make it brighter - dark = light.tint(0.6); - } - - // Build the light colors - - const lightContrastRgb = light.contrast().rgb; - - const lightVariables: { [key: string]: string } = { - '--ion-color-primary': `${themeColor}`, - '--ion-color-primary-rgb': `${light.rgb.r}, ${light.rgb.g}, ${light.rgb.b}`, - '--ion-color-primary-contrast': `${light.contrast().hex}`, - '--ion-color-primary-contrast-rgb': `${lightContrastRgb.r}, ${lightContrastRgb.g}, ${lightContrastRgb.b}`, - '--ion-color-primary-shade': `${light.shade().hex}`, - '--ion-color-primary-tint': `${light.tint().hex}`, - }; - - const darkContrastRgb = dark.contrast().rgb; - - const darkVariables: { [key: string]: string } = { - '--ion-color-primary': `${dark.hex}`, - '--ion-color-primary-rgb': `${dark.rgb.r}, ${dark.rgb.g}, ${dark.rgb.b}`, - '--ion-color-primary-contrast': `${dark.contrast().hex}`, - '--ion-color-primary-contrast-rgb': `${darkContrastRgb.r}, ${darkContrastRgb.g}, ${darkContrastRgb.b}`, - '--ion-color-primary-shade': `${dark.shade().hex}`, - '--ion-color-primary-tint': `${dark.tint().hex}`, - }; - - try { - let themeVarsContents = await readFile(variablesPath, { encoding: 'utf8' }); - - // Replace every theme variable with the updated ones - for (const v in lightVariables) { - const regExp = new RegExp(`(${v}):([^;]*)`, 'g'); - let variableIndex = 0; - themeVarsContents = themeVarsContents.replace(regExp, (str, match) => { - if (variableIndex === 0) { - variableIndex++; - return `${match}: ${lightVariables[v]}`; - } - return str; - }); - } - - for (const v in darkVariables) { - const regExp = new RegExp(`(${v}):([^;]*)`, 'g'); - let variableIndex = 0; - themeVarsContents = themeVarsContents.replace(regExp, (str, match) => { - if (variableIndex === 1) { - return `${match}: ${darkVariables[v]}`; - } - variableIndex++; - return str; - }); - } - - await writeFile(variablesPath, themeVarsContents); - } catch (e: any) { - const { log } = this.e; - log.error(`Unable to modify theme variables, theme will need to be set manually: ${e}`); - } - } - - async setAppResources(appIcon: Buffer, splash: Buffer) { - const resourcesDir = path.join(this.directory, 'resources'); - const iconPath = path.join(resourcesDir, 'icon.png'); - const splashPath = path.join(resourcesDir, 'splash.png'); - - try { - await ensureDir(resourcesDir); - - await writeFile(iconPath, appIcon); - await writeFile(splashPath, splash); - } catch (e: any) { - const { log } = this.e; - log.error(`Unable to find or create the resources directory. Skipping icon generation: ${e}`); - } - } - - async createIntegration(name: 'capacitor'): Promise; - async createIntegration(name: 'cordova'): Promise; - async createIntegration(name: 'enterprise'): Promise; - async createIntegration(name: IntegrationName): Promise; - async createIntegration(name: IntegrationName): Promise { - return BaseIntegration.createFromName({ - client: this.e.client, - config: this.e.config, - log: this.e.log, - project: this, - prompt: this.e.prompt, - session: this.e.session, - shell: this.e.shell, - }, name); - } - - getIntegration(name: IntegrationName): Required | undefined { - const integration = this.config.get('integrations')[name]; - - if (integration) { - return { - enabled: integration.enabled !== false, - root: integration.root === undefined ? this.directory : path.resolve(this.rootDirectory, integration.root), - }; - } - } - - requireIntegration(name: IntegrationName): Required { - const id = this.details.context === 'multiapp' ? this.details.id : undefined; - const integration = this.getIntegration(name); - - if (!integration) { - throw new FatalException(`Could not find ${strong(name)} integration in the ${strong(id ? id : 'default')} project.`); - } - - if (!integration.enabled) { - throw new FatalException(`${strong(name)} integration is disabled in the ${strong(id ? id : 'default')} project.`); - } - - return integration; - } - - protected async getIntegrations(): Promise[]> { - const integrationsFromConfig = this.config.get('integrations'); - const names = Object.keys(integrationsFromConfig) as IntegrationName[]; // TODO - - const integrationNames = names.filter(n => { - const c = integrationsFromConfig[n]; - return c && c.enabled !== false; - }); - - const integrations: (IIntegration | undefined)[] = await Promise.all(integrationNames.map(async name => { - try { - return await this.createIntegration(name); - } catch (e: any) { - if (!(e instanceof IntegrationNotFoundException)) { - throw e; - } - - this.e.log.warn(e.message); - } - })); - - return integrations.filter((i): i is IIntegration => typeof i !== 'undefined'); - } -} - -export function prettyProjectName(type?: string): string { - if (!type) { - return 'Unknown'; - } - - if (type === 'angular') { - return '@ionic/angular'; - } else if (type === 'react') { - return '@ionic/react'; - } else if (type === 'vue') { - return '@ionic/vue'; - } else if (type === 'custom') { - return 'Custom'; - } - - return type; -} - -export function isValidProjectId(projectId: string): boolean { - return projectId !== '.' && isValidPackageName(projectId) && projectId === path.basename(projectId); -} diff --git a/packages/@ionic/cli/src/lib/project/react-vite/build.ts b/packages/@ionic/cli/src/lib/project/react-vite/build.ts deleted file mode 100755 index 2e6c45a55e..0000000000 --- a/packages/@ionic/cli/src/lib/project/react-vite/build.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { CommandLineInputs, CommandLineOptions, CommandMetadata, ReactBuildOptions } from '../../../definitions'; -import { BUILD_SCRIPT, BuildCLI, BuildRunner, BuildRunnerDeps } from '../../build'; - -import { ReactViteProject } from './'; - -export interface ReactViteBuildRunnerDeps extends BuildRunnerDeps { - readonly project: ReactViteProject; -} -export class ReactViteBuildRunner extends BuildRunner { - constructor(protected readonly e: ReactViteBuildRunnerDeps) { - super(); - } - - async getCommandMetadata(): Promise> { - return {}; - } - - createOptionsFromCommandLine(inputs: CommandLineInputs, options: CommandLineOptions): ReactBuildOptions { - const baseOptions = super.createBaseOptionsFromCommandLine(inputs, options); - - return { - ...baseOptions, - type: 'react', - }; - } - - async buildProject(options: ReactBuildOptions): Promise { - const reactVite = new ReactViteBuildCLI(this.e); - await reactVite.build(options); - } -} - -export class ReactViteBuildCLI extends BuildCLI { - readonly name = 'Vite CLI Service'; - readonly pkg = 'vite'; - readonly program = 'vite'; - readonly prefix = 'vite'; - readonly script = BUILD_SCRIPT; - - protected async buildArgs(options: ReactBuildOptions): Promise { - const { pkgManagerArgs } = await import('../../utils/npm'); - - if (this.resolvedProgram === this.program) { - return ['build', ...(options['--'] || [])]; - } else { - const [ , ...pkgArgs ] = await pkgManagerArgs(this.e.config.get('npmClient'), { command: 'run', script: this.script, scriptArgs: options['--'] }); - return pkgArgs; - } - } - - protected async buildEnvVars(options: ReactBuildOptions): Promise { - const env: NodeJS.ProcessEnv = {}; - return { ...await super.buildEnvVars(options), ...env }; - } -} diff --git a/packages/@ionic/cli/src/lib/project/react-vite/index.ts b/packages/@ionic/cli/src/lib/project/react-vite/index.ts deleted file mode 100755 index 136ab3e124..0000000000 --- a/packages/@ionic/cli/src/lib/project/react-vite/index.ts +++ /dev/null @@ -1,80 +0,0 @@ -import chalk from 'chalk'; -import { debug as Debug } from 'debug'; -import * as lodash from 'lodash'; -import * as path from 'path'; - -import { Project } from '../'; -import { InfoItem } from '../../../definitions'; -import { RunnerNotFoundException } from '../../errors'; - -const debug = Debug('ionic:lib:project:react'); - -export class ReactViteProject extends Project { - readonly type: 'react' = 'react'; - - async getInfo(): Promise { - const [ - [ionicReact, ionicReactPath], - ] = await Promise.all([ - this.getPackageJson('@ionic/react'), - ]); - - return [ - ...(await super.getInfo()), - { - group: 'ionic', - name: 'Ionic Framework', - key: 'framework', - value: ionicReact ? `@ionic/react ${ionicReact.version}` : 'not installed', - path: ionicReactPath, - }, - ]; - } - - /** - * We can't detect React project types. We don't know what they look like! - */ - async detected() { - try { - const pkg = await this.requirePackageJson(); - const deps = lodash.assign({}, pkg.dependencies, pkg.devDependencies); - - if (typeof deps['@ionic/react'] === 'string') { - debug(`${chalk.bold('@ionic/react')} detected in ${chalk.bold('package.json')}`); - return true; - } - } catch (e: any) { - // ignore - } - - return false; - } - - async getDefaultDistDir(): Promise { - return 'dist'; - } - - async requireBuildRunner(): Promise { - const { ReactViteBuildRunner } = await import('./build'); - const deps = { ...this.e, project: this }; - return new ReactViteBuildRunner(deps); - } - - async requireServeRunner(): Promise { - const { ReactViteServeRunner } = await import('./serve'); - const deps = { ...this.e, project: this }; - return new ReactViteServeRunner(deps); - } - - async requireGenerateRunner(): Promise { - throw new RunnerNotFoundException( - `Cannot perform generate for React projects.\n` + - `Since you're using the ${chalk.bold('React')} project type, this command won't work. The Ionic CLI doesn't know how to generate framework components for React projects.` - ); - } - - setPrimaryTheme(themeColor: string): Promise { - const themePath = path.join(this.directory, 'src', 'theme', 'variables.css'); - return this.writeThemeColor(themePath, themeColor); - } -} diff --git a/packages/@ionic/cli/src/lib/project/react-vite/serve.ts b/packages/@ionic/cli/src/lib/project/react-vite/serve.ts deleted file mode 100755 index 308407129c..0000000000 --- a/packages/@ionic/cli/src/lib/project/react-vite/serve.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { ParsedArgs, unparseArgs } from '@ionic/cli-framework'; -import { stripAnsi } from '@ionic/cli-framework-output'; -import { findClosestOpenPort } from '@ionic/utils-network'; -import { CommandMetadata, ServeDetails, ReactServeOptions, } from '../../../definitions'; - -import { strong } from '../../color'; -import { BIND_ALL_ADDRESS, DEFAULT_ADDRESS, LOCAL_ADDRESSES, SERVE_SCRIPT, ServeCLI, ServeRunner, ServeRunnerDeps, } from '../../serve'; - -export class ReactViteServeRunner extends ServeRunner { - constructor(protected readonly e: ServeRunnerDeps) { - super(); - } - - async getCommandMetadata(): Promise> { - return {}; - } - - modifyOpenUrl(url: string, _options: ReactServeOptions): string { - return url; - } - - async serveProject(options: ReactServeOptions): Promise { - const [externalIP, availableInterfaces] = await this.selectExternalIP(options); - - const port = (options.port = await findClosestOpenPort(options.port)); - - const reactScripts = new ReactViteServeCLI(this.e); - await reactScripts.serve(options); - - return { - custom: reactScripts.resolvedProgram !== reactScripts.program, - protocol: options.https ? 'https' : 'http', - localAddress: 'localhost', - externalAddress: externalIP, - externalNetworkInterfaces: availableInterfaces, - port, - externallyAccessible: ![BIND_ALL_ADDRESS, ...LOCAL_ADDRESSES].includes( - externalIP - ), - }; - } -} - -export class ReactViteServeCLI extends ServeCLI { - readonly name = 'Vite CLI Service'; - readonly pkg = 'vite'; - readonly program = 'vite'; - readonly prefix = 'vite'; - readonly script = SERVE_SCRIPT; - protected chunks = 0; - - async serve(options: ReactServeOptions): Promise { - this.on('compile', (chunks) => { - if (chunks > 0) { - this.e.log.info( - `... and ${strong(chunks.toString())} additional chunks` - ); - } - }); - - return super.serve(options); - } - - protected stdoutFilter(line: string): boolean { - if (this.resolvedProgram !== this.program) { - return super.stdoutFilter(line); - } - const strippedLine = stripAnsi(line); - const compileMsgs = [ - 'Compiled successfully', - 'Compiled with warnings', - 'Failed to compile', - "ready in" - ]; - if (compileMsgs.some((msg) => strippedLine.includes(msg))) { - this.emit('ready'); - return false; - } - - if (strippedLine.match(/.*chunk\s{\d+}.+/)) { - this.chunks++; - return false; - } - - if (strippedLine.includes('Compiled successfully')) { - this.emit('compile', this.chunks); - this.chunks = 0; - } - - if(strippedLine.includes('has unexpectedly closed')) { - return false; - } - return true; - } - - protected stderrFilter(line: string): boolean { - if (this.resolvedProgram !== this.program) { - return super.stderrFilter(line); - } - const strippedLine = stripAnsi(line); - if (strippedLine.includes('webpack.Progress')) { - return false; - } - if(strippedLine.includes('has unexpectedly closed')) { - return false; - } - - return true; - } - - protected async buildArgs(options: ReactServeOptions): Promise { - const args: ParsedArgs = { - _: [], - host: options.host, - port: options.port ? options.port.toString() : undefined, - }; - const { pkgManagerArgs } = await import('../../utils/npm'); - - const separatedArgs = options['--']; - - if (this.resolvedProgram === this.program) { - return [...unparseArgs(args), ...separatedArgs]; - } else { - const [, ...pkgArgs] = await pkgManagerArgs( - this.e.config.get('npmClient'), - { - command: 'run', - script: this.script, - scriptArgs: [...unparseArgs(args), ...separatedArgs], - } - ); - return pkgArgs; - } - } - - protected async buildEnvVars( - options: ReactServeOptions - ): Promise { - const env: NodeJS.ProcessEnv = {}; - // // Vite binds to `localhost` by default, but if specified it prints a - // // warning, so don't set `HOST` if the host is set to `localhost`. - if (options.host !== DEFAULT_ADDRESS) { - env.HOST = options.host; - } - - env.PORT = String(options.port); - - env.HTTPS = options.https ? 'true' : 'false'; - - return { ...(await super.buildEnvVars(options)), ...env }; - } -} diff --git a/packages/@ionic/cli/src/lib/project/react/build.ts b/packages/@ionic/cli/src/lib/project/react/build.ts deleted file mode 100644 index 0710343acb..0000000000 --- a/packages/@ionic/cli/src/lib/project/react/build.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { CommandLineInputs, CommandLineOptions, CommandMetadata, ReactBuildOptions } from '../../../definitions'; -import { BUILD_SCRIPT, BuildCLI, BuildRunner, BuildRunnerDeps } from '../../build'; -import { input, weak } from '../../color'; - -import { ReactProject } from './'; - -export interface ReactBuildRunnerDeps extends BuildRunnerDeps { - readonly project: ReactProject; -} - -export class ReactBuildRunner extends BuildRunner { - constructor(protected readonly e: ReactBuildRunnerDeps) { - super(); - } - - async getCommandMetadata(): Promise> { - return { - description: ` -This command will convert options to the environment variables used by React Scripts. See the ${input('create-react-app')} docs[^cra-build-docs] for explanations. - `, - footnotes: [ - { - id: 'cra-build-docs', - url: 'https://facebook.github.io/create-react-app/docs/advanced-configuration', - }, - ], - options: [ - { - name: 'public-url', - summary: 'The URL at which the app will be served', - groups: ['cordova'], - spec: { value: 'url' }, - hint: weak('[react-scripts]'), - }, - { - name: 'ci', - summary: `Treat warnings as build failures, test runner does not watch`, - type: Boolean, - groups: ['cordova'], - hint: weak('[react-scripts]'), - }, - { - name: 'source-map', - summary: 'Do not generate source maps', - type: Boolean, - groups: ['cordova'], - default: true, - hint: weak('[react-scripts]'), - }, - { - name: 'inline-runtime-chunk', - summary: `Do not include the runtime script in ${input('index.html')} (import instead)`, - type: Boolean, - groups: ['cordova'], - default: true, - hint: weak('[react-scripts]'), - }, - ], - }; - } - - createOptionsFromCommandLine(inputs: CommandLineInputs, options: CommandLineOptions): ReactBuildOptions { - const baseOptions = super.createBaseOptionsFromCommandLine(inputs, options); - const publicUrl = options['public-url'] ? String(options['public-url']) : undefined; - const ci = options['ci'] ? Boolean(options['ci']) : undefined; - const sourceMap = options['source-map'] ? Boolean(options['source-map']) : undefined; - const inlineRuntimeChunk = options['inline-runtime-check'] ? Boolean(options['inline-runtime-check']) : undefined; - - return { - ...baseOptions, - type: 'react', - publicUrl, - ci, - sourceMap, - inlineRuntimeChunk, - }; - } - - async buildProject(options: ReactBuildOptions): Promise { - const reactScripts = new ReactBuildCLI(this.e); - await reactScripts.build(options); - } -} - -export class ReactBuildCLI extends BuildCLI { - readonly name = 'React Scripts'; - readonly pkg = 'react-scripts'; - readonly program = 'react-scripts'; - readonly prefix = 'react-scripts'; - readonly script = BUILD_SCRIPT; - - protected async buildArgs(options: ReactBuildOptions): Promise { - const { pkgManagerArgs } = await import('../../utils/npm'); - - if (this.resolvedProgram === this.program) { - return ['build']; - } else { - const [ , ...pkgArgs ] = await pkgManagerArgs(this.e.config.get('npmClient'), { command: 'run', script: this.script }); - return pkgArgs; - } - } - - protected async buildEnvVars(options: ReactBuildOptions): Promise { - const env: NodeJS.ProcessEnv = {}; - - if (options.publicUrl) { - env.PUBLIC_URL = options.publicUrl; - } - - if (options.ci) { - env.CI = '1'; - } - - if (!options.sourceMap) { - env.GENERATE_SOURCEMAP = 'false'; - } - - if (!options.inlineRuntimeChunk) { - env.INLINE_RUNTIME_CHUNK = 'false'; - } - - return { ...await super.buildEnvVars(options), ...env }; - } -} diff --git a/packages/@ionic/cli/src/lib/project/react/index.ts b/packages/@ionic/cli/src/lib/project/react/index.ts deleted file mode 100644 index 05a05929a9..0000000000 --- a/packages/@ionic/cli/src/lib/project/react/index.ts +++ /dev/null @@ -1,80 +0,0 @@ -import chalk from 'chalk'; -import { debug as Debug } from 'debug'; -import * as lodash from 'lodash'; -import * as path from 'path'; - -import { Project } from '../'; -import { InfoItem } from '../../../definitions'; -import { RunnerNotFoundException } from '../../errors'; - -const debug = Debug('ionic:lib:project:React'); - -export class ReactProject extends Project { - readonly type: 'react' = 'react'; - - async getInfo(): Promise { - const [ - [ionicReactPkg, ionicReactPkgPath], - ] = await Promise.all([ - this.getPackageJson('@ionic/react'), - ]); - - return [ - ...(await super.getInfo()), - { - group: 'ionic', - name: 'Ionic Framework', - key: 'framework', - value: ionicReactPkg ? `@ionic/react ${ionicReactPkg.version}` : 'not installed', - path: ionicReactPkgPath, - }, - ]; - } - - /** - * We can't detect React project types. We don't know what they look like! - */ - async detected() { - try { - const pkg = await this.requirePackageJson(); - const deps = lodash.assign({}, pkg.dependencies, pkg.devDependencies); - - if (typeof deps['@ionic/React'] === 'string') { - debug(`${chalk.bold('@ionic/React')} detected in ${chalk.bold('package.json')}`); - return true; - } - } catch (e: any) { - // ignore - } - - return false; - } - - async getDefaultDistDir(): Promise { - return 'build'; - } - - async requireBuildRunner(): Promise { - const { ReactBuildRunner } = await import('./build'); - const deps = { ...this.e, project: this }; - return new ReactBuildRunner(deps); - } - - async requireServeRunner(): Promise { - const { ReactServeRunner } = await import('./serve'); - const deps = { ...this.e, project: this }; - return new ReactServeRunner(deps); - } - - async requireGenerateRunner(): Promise { - throw new RunnerNotFoundException( - `Cannot perform generate for React projects.\n` + - `Since you're using the ${chalk.bold('React')} project type, this command won't work. The Ionic CLI doesn't know how to generate framework components for React projects.` - ); - } - - setPrimaryTheme(themeColor: string): Promise { - const themePath = path.join(this.directory, 'src', 'theme', 'variables.css'); - return this.writeThemeColor(themePath, themeColor); - } -} diff --git a/packages/@ionic/cli/src/lib/project/react/serve.ts b/packages/@ionic/cli/src/lib/project/react/serve.ts deleted file mode 100644 index d085b50854..0000000000 --- a/packages/@ionic/cli/src/lib/project/react/serve.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { CommandLineInputs, CommandLineOptions } from '@ionic/cli-framework'; -import { findClosestOpenPort } from '@ionic/utils-network'; -import { stripAnsi } from '@ionic/utils-terminal'; - -import { CommandMetadata, ReactServeOptions, ServeDetails } from '../../../definitions'; -import { input, strong, weak } from '../../color'; -import { BIND_ALL_ADDRESS, DEFAULT_ADDRESS, LOCAL_ADDRESSES, SERVE_SCRIPT, ServeCLI, ServeRunner, ServeRunnerDeps } from '../../serve'; - -export class ReactServeRunner extends ServeRunner { - constructor(protected readonly e: ServeRunnerDeps) { - super(); - } - - async getCommandMetadata(): Promise> { - return { - description: ` -This command will convert options to the environment variables used by React Scripts. See the ${input('create-react-app')} docs[^cra-build-docs] for explanations. - `, - footnotes: [ - { - id: 'cra-build-docs', - url: 'https://facebook.github.io/create-react-app/docs/advanced-configuration', - }, - ], - options: [ - { - name: 'https', - summary: 'Use HTTPS for the dev server', - type: Boolean, - groups: ['cordova'], - hint: weak('[react-scripts]'), - }, - { - name: 'react-editor', - summary: `Specify the editor that opens files upon crash`, - type: String, - spec: { value: 'editor' }, - groups: ['cordova'], - hint: weak('[react-scripts]'), - }, - { - name: 'ci', - summary: `Treat warnings as build failures, test runner does not watch`, - type: Boolean, - groups: ['cordova'], - hint: weak('[react-scripts]'), - }, - ], - }; - } - - createOptionsFromCommandLine(inputs: CommandLineInputs, options: CommandLineOptions): ReactServeOptions { - const baseOptions = super.createOptionsFromCommandLine(inputs, options); - const ci = options['ci'] ? Boolean(options['ci']) : undefined; - const https = options['https'] ? Boolean(options['https']) : undefined; - const reactEditor = options['react-editor'] ? String(options['react-editor']) : undefined; - - return { - ...baseOptions, - ci, - https, - reactEditor, - }; - } - - modifyOpenUrl(url: string, options: ReactServeOptions): string { - return url; - } - - async serveProject(options: ReactServeOptions): Promise { - const [externalIP, availableInterfaces] = await this.selectExternalIP(options); - - const port = options.port = await findClosestOpenPort(options.port); - - const reactScripts = new ReactServeCLI(this.e); - await reactScripts.serve(options); - - return { - custom: reactScripts.resolvedProgram !== reactScripts.program, - protocol: options.https ? 'https' : 'http', - localAddress: 'localhost', - externalAddress: externalIP, - externalNetworkInterfaces: availableInterfaces, - port, - externallyAccessible: ![BIND_ALL_ADDRESS, ...LOCAL_ADDRESSES].includes(externalIP), - }; - } -} - -export class ReactServeCLI extends ServeCLI { - readonly name = 'React Scripts'; - readonly pkg = 'react-scripts'; - readonly program = 'react-scripts'; - readonly prefix = 'react-scripts'; - readonly script = SERVE_SCRIPT; - protected chunks = 0; - - async serve(options: ReactServeOptions): Promise { - this.on('compile', chunks => { - if (chunks > 0) { - this.e.log.info(`... and ${strong(chunks.toString())} additional chunks`); - } - }); - - return super.serve(options); - } - - protected stdoutFilter(line: string): boolean { - if (this.resolvedProgram !== this.program) { - return super.stdoutFilter(line); - } - - const strippedLine = stripAnsi(line); - - const compileMsgs = ['Compiled successfully', 'Compiled with', 'Failed to compile']; - if (compileMsgs.some(msg => strippedLine.includes(msg))) { - this.emit('ready'); - return false; - } - - if (strippedLine.match(/.*chunk\s{\d+}.+/)) { - this.chunks++; - return false; - } - - if (strippedLine.includes('Compiled successfully')) { - this.emit('compile', this.chunks); - this.chunks = 0; - } - - return true; - } - - protected async buildArgs(options: ReactServeOptions): Promise { - const { pkgManagerArgs } = await import('../../utils/npm'); - - if (this.resolvedProgram === this.program) { - return ['start']; - } else { - const [, ...pkgArgs] = await pkgManagerArgs(this.e.config.get('npmClient'), { command: 'run', script: this.script }); - return pkgArgs; - } - } - - protected async buildEnvVars(options: ReactServeOptions): Promise { - const env: NodeJS.ProcessEnv = {}; - - // Tell CRA not to open the dev server URL. We do this in Ionic CLI. - env.BROWSER = 'none'; - - // CRA binds to `localhost` by default, but if specified it prints a - // warning, so don't set `HOST` if the host is set to `localhost`. - if (options.host !== DEFAULT_ADDRESS) { - env.HOST = options.host; - } - - env.PORT = String(options.port); - env.HTTPS = options.https ? 'true' : 'false'; - - if (options.ci) { - env.CI = '1'; - } - - if (options.reactEditor) { - env.REACT_EDITOR = options.reactEditor; - } - - return { ...await super.buildEnvVars(options), ...env }; - } -} diff --git a/packages/@ionic/cli/src/lib/project/vue-vite/build.ts b/packages/@ionic/cli/src/lib/project/vue-vite/build.ts deleted file mode 100755 index 87f56ca3b8..0000000000 --- a/packages/@ionic/cli/src/lib/project/vue-vite/build.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { CommandLineInputs, CommandLineOptions, CommandMetadata, VueBuildOptions } from '../../../definitions'; -import { BUILD_SCRIPT, BuildCLI, BuildRunner, BuildRunnerDeps } from '../../build'; - -import { VueViteProject } from './'; - -export interface VueBuildRunnerDeps extends BuildRunnerDeps { - readonly project: VueViteProject; -} -export class VueViteBuildRunner extends BuildRunner { - constructor(protected readonly e: VueBuildRunnerDeps) { - super(); - } - - async getCommandMetadata(): Promise> { - return {}; - } - - createOptionsFromCommandLine(inputs: CommandLineInputs, options: CommandLineOptions): VueBuildOptions { - const baseOptions = super.createBaseOptionsFromCommandLine(inputs, options); - - return { - ...baseOptions, - type: 'vue', - }; - } - - async buildProject(options: VueBuildOptions): Promise { - const vueScripts = new VueViteBuildCLI(this.e); - await vueScripts.build(options); - } -} - -export class VueViteBuildCLI extends BuildCLI { - readonly name = 'Vite CLI Service'; - readonly pkg = 'vite'; - readonly program = 'vite'; - readonly prefix = 'vite'; - readonly script = BUILD_SCRIPT; - - protected async buildArgs(options: VueBuildOptions): Promise { - const { pkgManagerArgs } = await import('../../utils/npm'); - - if (this.resolvedProgram === this.program) { - return ['build', ...(options['--'] || [])]; - } else { - const [ , ...pkgArgs ] = await pkgManagerArgs(this.e.config.get('npmClient'), { command: 'run', script: this.script, scriptArgs: options['--'] }); - return pkgArgs; - } - } - - protected async buildEnvVars(options: VueBuildOptions): Promise { - const env: NodeJS.ProcessEnv = {}; - return { ...await super.buildEnvVars(options), ...env }; - } -} diff --git a/packages/@ionic/cli/src/lib/project/vue-vite/index.ts b/packages/@ionic/cli/src/lib/project/vue-vite/index.ts deleted file mode 100755 index 9304bd7b7f..0000000000 --- a/packages/@ionic/cli/src/lib/project/vue-vite/index.ts +++ /dev/null @@ -1,80 +0,0 @@ -import chalk from 'chalk'; -import { debug as Debug } from 'debug'; -import * as lodash from 'lodash'; -import * as path from 'path'; - -import { Project } from '../'; -import { InfoItem } from '../../../definitions'; -import { RunnerNotFoundException } from '../../errors'; - -const debug = Debug('ionic:lib:project:vue'); - -export class VueViteProject extends Project { - readonly type: 'vue' = 'vue'; - - async getInfo(): Promise { - const [ - [ionicVuePkg, ionicVuePkgPath], - ] = await Promise.all([ - this.getPackageJson('@ionic/vue'), - ]); - - return [ - ...(await super.getInfo()), - { - group: 'ionic', - name: 'Ionic Framework', - key: 'framework', - value: ionicVuePkg ? `@ionic/vue ${ionicVuePkg.version}` : 'not installed', - path: ionicVuePkgPath, - }, - ]; - } - - /** - * We can't detect Vue project types. We don't know what they look like! - */ - async detected() { - try { - const pkg = await this.requirePackageJson(); - const deps = lodash.assign({}, pkg.dependencies, pkg.devDependencies); - - if (typeof deps['@ionic/vue'] === 'string') { - debug(`${chalk.bold('@ionic/vue')} detected in ${chalk.bold('package.json')}`); - return true; - } - } catch (e: any) { - // ignore - } - - return false; - } - - async getDefaultDistDir(): Promise { - return 'dist'; - } - - async requireBuildRunner(): Promise { - const { VueViteBuildRunner } = await import('./build'); - const deps = { ...this.e, project: this }; - return new VueViteBuildRunner(deps); - } - - async requireServeRunner(): Promise { - const { VueServeRunner } = await import('./serve'); - const deps = { ...this.e, project: this }; - return new VueServeRunner(deps); - } - - async requireGenerateRunner(): Promise { - throw new RunnerNotFoundException( - `Cannot perform generate for Vue projects.\n` + - `Since you're using the ${chalk.bold('Vue')} project type, this command won't work. The Ionic CLI doesn't know how to generate framework components for Vue projects.` - ); - } - - setPrimaryTheme(themeColor: string): Promise { - const themePath = path.join(this.directory, 'src', 'theme', 'variables.css'); - return this.writeThemeColor(themePath, themeColor); - } -} diff --git a/packages/@ionic/cli/src/lib/project/vue-vite/serve.ts b/packages/@ionic/cli/src/lib/project/vue-vite/serve.ts deleted file mode 100755 index a2656f2b36..0000000000 --- a/packages/@ionic/cli/src/lib/project/vue-vite/serve.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { ParsedArgs, unparseArgs } from '@ionic/cli-framework'; -import { stripAnsi } from '@ionic/cli-framework-output'; -import { findClosestOpenPort } from '@ionic/utils-network'; - -import { - CommandMetadata, - ServeDetails, - VueServeOptions, -} from '../../../definitions'; -import { strong } from '../../color'; -import { - BIND_ALL_ADDRESS, - DEFAULT_ADDRESS, - LOCAL_ADDRESSES, - SERVE_SCRIPT, - ServeCLI, - ServeRunner, - ServeRunnerDeps, -} from '../../serve'; - -export class VueServeRunner extends ServeRunner { - constructor(protected readonly e: ServeRunnerDeps) { - super(); - } - - async getCommandMetadata(): Promise> { - return {}; - } - - modifyOpenUrl(url: string, _options: VueServeOptions): string { - return url; - } - - async serveProject(options: VueServeOptions): Promise { - const [externalIP, availableInterfaces] = await this.selectExternalIP( - options - ); - - const port = (options.port = await findClosestOpenPort(options.port)); - - const vueScripts = new VueViteServeCLI(this.e); - await vueScripts.serve(options); - - return { - custom: vueScripts.resolvedProgram !== vueScripts.program, - protocol: options.https ? 'https' : 'http', - localAddress: 'localhost', - externalAddress: externalIP, - externalNetworkInterfaces: availableInterfaces, - port, - externallyAccessible: ![BIND_ALL_ADDRESS, ...LOCAL_ADDRESSES].includes( - externalIP - ), - }; - } -} - -export class VueViteServeCLI extends ServeCLI { - readonly name = 'Vite CLI Service'; - readonly pkg = 'vite'; - readonly program = 'vite'; - readonly prefix = 'vite'; - readonly script = SERVE_SCRIPT; - protected chunks = 0; - - async serve(options: VueServeOptions): Promise { - this.on('compile', (chunks) => { - if (chunks > 0) { - this.e.log.info( - `... and ${strong(chunks.toString())} additional chunks` - ); - } - }); - - return super.serve(options); - } - - protected stdoutFilter(line: string): boolean { - if (this.resolvedProgram !== this.program) { - return super.stdoutFilter(line); - } - const strippedLine = stripAnsi(line); - const compileMsgs = [ - 'Compiled successfully', - 'Compiled with warnings', - 'Failed to compile', - "ready in" - ]; - if (compileMsgs.some((msg) => strippedLine.includes(msg))) { - this.emit('ready'); - return false; - } - - if (strippedLine.match(/.*chunk\s{\d+}.+/)) { - this.chunks++; - return false; - } - - if (strippedLine.includes('Compiled successfully')) { - this.emit('compile', this.chunks); - this.chunks = 0; - } - - return true; - } - protected stderrFilter(line: string): boolean { - if (this.resolvedProgram !== this.program) { - return super.stderrFilter(line); - } - const strippedLine = stripAnsi(line); - if (strippedLine.includes('webpack.Progress')) { - return false; - } - - return true; - } - - protected async buildArgs(options: VueServeOptions): Promise { - const args: ParsedArgs = { - _: [], - host: options.host, - port: options.port ? options.port.toString() : undefined, - }; - const { pkgManagerArgs } = await import('../../utils/npm'); - - const separatedArgs = options['--']; - - if (this.resolvedProgram === this.program) { - return [...unparseArgs(args), ...separatedArgs]; - } else { - const [, ...pkgArgs] = await pkgManagerArgs( - this.e.config.get('npmClient'), - { - command: 'run', - script: this.script, - scriptArgs: [...unparseArgs(args), ...separatedArgs], - } - ); - return pkgArgs; - } - } - - protected async buildEnvVars( - options: VueServeOptions - ): Promise { - const env: NodeJS.ProcessEnv = {}; - // // Vue CLI binds to `localhost` by default, but if specified it prints a - // // warning, so don't set `HOST` if the host is set to `localhost`. - if (options.host !== DEFAULT_ADDRESS) { - env.HOST = options.host; - } - - env.PORT = String(options.port); - - env.HTTPS = options.https ? 'true' : 'false'; - - return { ...(await super.buildEnvVars(options)), ...env }; - } -} diff --git a/packages/@ionic/cli/src/lib/project/vue/build.ts b/packages/@ionic/cli/src/lib/project/vue/build.ts deleted file mode 100644 index 7c5f80a181..0000000000 --- a/packages/@ionic/cli/src/lib/project/vue/build.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { CommandLineInputs, CommandLineOptions, CommandMetadata, VueBuildOptions } from '../../../definitions'; -import { BUILD_SCRIPT, BuildCLI, BuildRunner, BuildRunnerDeps } from '../../build'; - -import { VueProject } from './'; - -export interface VueBuildRunnerDeps extends BuildRunnerDeps { - readonly project: VueProject; -} -export class VueBuildRunner extends BuildRunner { - constructor(protected readonly e: VueBuildRunnerDeps) { - super(); - } - - async getCommandMetadata(): Promise> { - return {}; - } - - createOptionsFromCommandLine(inputs: CommandLineInputs, options: CommandLineOptions): VueBuildOptions { - const baseOptions = super.createBaseOptionsFromCommandLine(inputs, options); - - return { - ...baseOptions, - type: 'vue', - }; - } - - async buildProject(options: VueBuildOptions): Promise { - const vueScripts = new VueBuildCLI(this.e); - await vueScripts.build(options); - } -} - -export class VueBuildCLI extends BuildCLI { - readonly name = 'Vue CLI Service'; - readonly pkg = '@vue/cli-service'; - readonly program = 'vue-cli-service'; - readonly prefix = 'vue-cli-service'; - readonly script = BUILD_SCRIPT; - - protected async buildArgs(options: VueBuildOptions): Promise { - const { pkgManagerArgs } = await import('../../utils/npm'); - - if (this.resolvedProgram === this.program) { - return ['build', ...(options['--'] || [])]; - } else { - const [ , ...pkgArgs ] = await pkgManagerArgs(this.e.config.get('npmClient'), { command: 'run', script: this.script, scriptArgs: options['--'] }); - return pkgArgs; - } - } - - protected async buildEnvVars(options: VueBuildOptions): Promise { - const env: NodeJS.ProcessEnv = {}; - return { ...await super.buildEnvVars(options), ...env }; - } -} diff --git a/packages/@ionic/cli/src/lib/project/vue/index.ts b/packages/@ionic/cli/src/lib/project/vue/index.ts deleted file mode 100644 index da584e1e50..0000000000 --- a/packages/@ionic/cli/src/lib/project/vue/index.ts +++ /dev/null @@ -1,80 +0,0 @@ -import chalk from 'chalk'; -import { debug as Debug } from 'debug'; -import * as lodash from 'lodash'; -import * as path from 'path'; - -import { Project } from '../'; -import { InfoItem } from '../../../definitions'; -import { RunnerNotFoundException } from '../../errors'; - -const debug = Debug('ionic:lib:project:vue'); - -export class VueProject extends Project { - readonly type: 'vue' = 'vue'; - - async getInfo(): Promise { - const [ - [ionicVuePkg, ionicVuePkgPath], - ] = await Promise.all([ - this.getPackageJson('@ionic/vue'), - ]); - - return [ - ...(await super.getInfo()), - { - group: 'ionic', - name: 'Ionic Framework', - key: 'framework', - value: ionicVuePkg ? `@ionic/vue ${ionicVuePkg.version}` : 'not installed', - path: ionicVuePkgPath, - }, - ]; - } - - /** - * We can't detect Vue project types. We don't know what they look like! - */ - async detected() { - try { - const pkg = await this.requirePackageJson(); - const deps = lodash.assign({}, pkg.dependencies, pkg.devDependencies); - - if (typeof deps['@ionic/vue'] === 'string') { - debug(`${chalk.bold('@ionic/vue')} detected in ${chalk.bold('package.json')}`); - return true; - } - } catch (e: any) { - // ignore - } - - return false; - } - - async getDefaultDistDir(): Promise { - return 'dist'; - } - - async requireBuildRunner(): Promise { - const { VueBuildRunner } = await import('./build'); - const deps = { ...this.e, project: this }; - return new VueBuildRunner(deps); - } - - async requireServeRunner(): Promise { - const { VueServeRunner } = await import('./serve'); - const deps = { ...this.e, project: this }; - return new VueServeRunner(deps); - } - - async requireGenerateRunner(): Promise { - throw new RunnerNotFoundException( - `Cannot perform generate for Vue projects.\n` + - `Since you're using the ${chalk.bold('Vue')} project type, this command won't work. The Ionic CLI doesn't know how to generate framework components for Vue projects.` - ); - } - - setPrimaryTheme(themeColor: string): Promise { - const themePath = path.join(this.directory, 'src', 'theme', 'variables.css'); - return this.writeThemeColor(themePath, themeColor); - } -} diff --git a/packages/@ionic/cli/src/lib/project/vue/serve.ts b/packages/@ionic/cli/src/lib/project/vue/serve.ts deleted file mode 100644 index 999fdda433..0000000000 --- a/packages/@ionic/cli/src/lib/project/vue/serve.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { stripAnsi } from '@ionic/cli-framework-output'; -import { findClosestOpenPort } from '@ionic/utils-network'; - -import { CommandMetadata, ServeDetails, VueServeOptions } from '../../../definitions'; -import { strong } from '../../color'; -import { BIND_ALL_ADDRESS, DEFAULT_ADDRESS, LOCAL_ADDRESSES, SERVE_SCRIPT, ServeCLI, ServeRunner, ServeRunnerDeps } from '../../serve'; - -export class VueServeRunner extends ServeRunner { - constructor(protected readonly e: ServeRunnerDeps) { - super(); - } - - async getCommandMetadata(): Promise> { - return {}; - } - - modifyOpenUrl(url: string, _options: VueServeOptions): string { - return url; - } - - async serveProject(options: VueServeOptions): Promise { - const [externalIP, availableInterfaces] = await this.selectExternalIP(options); - - const port = options.port = await findClosestOpenPort(options.port); - - const vueScripts = new VueServeCLI(this.e); - await vueScripts.serve(options); - - return { - custom: vueScripts.resolvedProgram !== vueScripts.program, - protocol: options.https ? 'https' : 'http', - localAddress: 'localhost', - externalAddress: externalIP, - externalNetworkInterfaces: availableInterfaces, - port, - externallyAccessible: ![BIND_ALL_ADDRESS, ...LOCAL_ADDRESSES].includes(externalIP), - }; - } - -} - -export class VueServeCLI extends ServeCLI { - readonly name = 'Vue CLI Service'; - readonly pkg = '@vue/cli-service'; - readonly program = 'vue-cli-service'; - readonly prefix = 'vue-cli-service'; - readonly script = SERVE_SCRIPT; - protected chunks = 0; - - async serve(options: VueServeOptions): Promise { - this.on('compile', chunks => { - if (chunks > 0) { - this.e.log.info(`... and ${strong(chunks.toString())} additional chunks`); - } - }); - - return super.serve(options); - } - - protected stdoutFilter(line: string): boolean { - if (this.resolvedProgram !== this.program) { - return super.stdoutFilter(line); - } - - const strippedLine = stripAnsi(line); - const compileMsgs = ['Compiled successfully', 'Compiled with', 'Failed to compile']; - if (compileMsgs.some(msg => strippedLine.includes(msg))) { - this.emit('ready'); - return false; - } - - if (strippedLine.match(/.*chunk\s{\d+}.+/)) { - this.chunks++; - return false; - } - - if (strippedLine.includes('Compiled successfully')) { - this.emit('compile', this.chunks); - this.chunks = 0; - } - - // const endBannerMsgs = ['development build', 'production build'] - // if (endBannerMsgs.some(msg => strippedLine.includes(msg))) { - // return false; - // } - return true; - } - protected stderrFilter(line: string): boolean { - if (this.resolvedProgram !== this.program) { - return super.stderrFilter(line); - } - const strippedLine = stripAnsi(line); - if (strippedLine.includes('webpack.Progress')) { - return false; - } - - return true; - - } - - protected async buildArgs(_options: VueServeOptions): Promise { - const { pkgManagerArgs } = await import('../../utils/npm'); - - const separatedArgs = _options['--'] - - if (this.resolvedProgram === this.program) { - return ['serve', ...separatedArgs]; - } else { - const [, ...pkgArgs] = await pkgManagerArgs(this.e.config.get('npmClient'), { command: 'run', script: this.script, scriptArgs: separatedArgs }); - return pkgArgs; - } - } - - protected async buildEnvVars(options: VueServeOptions): Promise { - const env: NodeJS.ProcessEnv = {}; - // // Vue CLI binds to `localhost` by default, but if specified it prints a - // // warning, so don't set `HOST` if the host is set to `localhost`. - if (options.host !== DEFAULT_ADDRESS) { - env.HOST = options.host; - } - - env.PORT = String(options.port); - env.HTTPS = options.https ? 'true' : 'false'; - - return { ...await super.buildEnvVars(options), ...env }; - } -} diff --git a/packages/@ionic/cli/src/lib/prompts.ts b/packages/@ionic/cli/src/lib/prompts.ts deleted file mode 100644 index 10e8a28ccc..0000000000 --- a/packages/@ionic/cli/src/lib/prompts.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { PromptQuestion, PromptValue } from '@ionic/cli-framework-prompts'; - -import { ILogger, IonicEnvironmentFlags } from '../definitions'; - -import { ancillary, input, weak } from './color'; - -export interface CreateOnFallbackOptions { - readonly flags: IonicEnvironmentFlags; - readonly log: ILogger; -} - -export function createOnFallback({ flags: { confirm }, log }: CreateOnFallbackOptions) { - return (question: PromptQuestion): PromptValue | void => { - if (question.type === 'confirm') { - if (confirm) { - log.msg(`${input('--confirm')}: ${weak(question.message)} ${ancillary('Yes')}`); - return true; - } else { - log.msg(`${input('--no-confirm')}: ${weak(question.message)} ${ancillary('No')}`); - return false; - } - } - }; -} diff --git a/packages/@ionic/cli/src/lib/security.ts b/packages/@ionic/cli/src/lib/security.ts deleted file mode 100644 index 788fdeb2fc..0000000000 --- a/packages/@ionic/cli/src/lib/security.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { IClient, ResourceClientLoad, SecurityProfile } from '../definitions'; -import { isSecurityProfileResponse } from '../guards'; - -import { ResourceClient, createFatalAPIFormat } from './http'; - -export interface SecurityClientDeps { - readonly client: IClient; - readonly token: string; -} - -export class SecurityClient extends ResourceClient implements ResourceClientLoad { - protected readonly client: IClient; - protected readonly token: string; - - constructor({ client, token }: SecurityClientDeps) { - super(); - this.client = client; - this.token = token; - } - - async load(tag: string): Promise { - const { req } = await this.client.make('GET', `/security/profiles/${tag}`); - this.applyAuthentication(req, this.token); - req.query({}).send(); - const res = await this.client.do(req); - - if (!isSecurityProfileResponse(res)) { - throw createFatalAPIFormat(req, res); - } - - return res.data; - } -} diff --git a/packages/@ionic/cli/src/lib/serve.ts b/packages/@ionic/cli/src/lib/serve.ts deleted file mode 100644 index 0e8ff5b6eb..0000000000 --- a/packages/@ionic/cli/src/lib/serve.ts +++ /dev/null @@ -1,628 +0,0 @@ -import { BaseError, MetadataGroup, ParsedArgs, unparseArgs } from '@ionic/cli-framework'; -import { LOGGER_LEVELS, createPrefixedFormatter } from '@ionic/cli-framework-output'; -import { PromptModule } from '@ionic/cli-framework-prompts'; -import { str2num } from '@ionic/cli-framework/utils/string'; -import { NetworkInterface, getExternalIPv4Interfaces, isHostConnectable } from '@ionic/utils-network'; -import { createProcessEnv, killProcessTree, onBeforeExit, processExit } from '@ionic/utils-process'; -import chalk from 'chalk'; -import { debug as Debug } from 'debug'; -import { EventEmitter } from 'events'; -import * as lodash from 'lodash'; -import split2 from 'split2'; -import * as stream from 'stream'; - -import { CommandLineInputs, CommandLineOptions, CommandMetadata, CommandMetadataOption, IConfig, ILogger, IProject, IShell, IonicEnvironmentFlags, NpmClient, Runner, ServeDetails, ServeOptions } from '../definitions'; - -import { ancillary, input, strong, weak } from './color'; -import { FatalException, ServeCLIProgramNotFoundException } from './errors'; -import { emit } from './events'; -import { Hook } from './hooks'; -import { openUrl } from './open'; -import { createDefaultLoggerHandlers } from './utils/logger'; - -const debug = Debug('ionic:lib:serve'); - -export const DEFAULT_DEV_LOGGER_PORT = 53703; -export const DEFAULT_LIVERELOAD_PORT = 35729; -export const DEFAULT_SERVER_PORT = 8100; -export const DEFAULT_DEVAPP_COMM_PORT = 53233; - -export const DEFAULT_ADDRESS = 'localhost'; -export const BIND_ALL_ADDRESS = '0.0.0.0'; -export const LOCAL_ADDRESSES = ['localhost', '127.0.0.1']; - -export const BROWSERS = ['safari', 'firefox', process.platform === 'win32' ? 'chrome' : (process.platform === 'darwin' ? 'google chrome' : 'google-chrome')]; - -// npm script name -export const SERVE_SCRIPT = 'ionic:serve'; - -export const COMMON_SERVE_COMMAND_OPTIONS: readonly CommandMetadataOption[] = [ - { - name: 'external', - summary: `Host dev server on all network interfaces (i.e. ${input('--host=0.0.0.0')})`, - type: Boolean, - }, - { - name: 'address', // keep this here so the option is parsed with its value - summary: '', - groups: [MetadataGroup.HIDDEN], - }, - { - name: 'host', - summary: 'Use specific host for the dev server', - default: DEFAULT_ADDRESS, - groups: [MetadataGroup.ADVANCED], - }, - { - name: 'port', - summary: 'Use specific port for the dev server', - default: DEFAULT_SERVER_PORT.toString(), - aliases: ['p'], - groups: [MetadataGroup.ADVANCED], - }, - { - name: 'public-host', - summary: 'The host used for the browser or web view', - groups: [MetadataGroup.ADVANCED], - spec: { value: 'host' }, - }, - { - name: 'livereload', - summary: 'Do not spin up dev server--just serve files', - type: Boolean, - default: true, - }, - { - name: 'engine', - summary: `Target engine (e.g. ${['browser', 'cordova'].map(e => input(e)).join(', ')})`, - groups: [MetadataGroup.HIDDEN, MetadataGroup.ADVANCED], - }, - { - name: 'platform', - summary: `Target platform on chosen engine (e.g. ${['ios', 'android'].map(e => input(e)).join(', ')})`, - groups: [MetadataGroup.HIDDEN, MetadataGroup.ADVANCED], - }, -]; - -export interface ServeRunnerDeps { - readonly config: IConfig; - readonly flags: IonicEnvironmentFlags; - readonly log: ILogger; - readonly project: IProject; - readonly prompt: PromptModule; - readonly shell: IShell; -} - -export abstract class ServeRunner implements Runner { - protected devAppConnectionMade = false; - - protected abstract readonly e: ServeRunnerDeps; - - abstract getCommandMetadata(): Promise>; - abstract serveProject(options: T): Promise; - abstract modifyOpenUrl(url: string, options: T): string; - - getPkgManagerServeCLI(): PkgManagerServeCLI { - const pkgManagerCLIs = { - npm: NpmServeCLI, - pnpm: PnpmServeCLI, - yarn: YarnServeCLI, - }; - - const client = this.e.config.get('npmClient'); - const CLI = pkgManagerCLIs[client]; - - if (CLI) { - return new CLI(this.e); - } - - throw new ServeCLIProgramNotFoundException('Unknown CLI client: ' + client); - } - - createOptionsFromCommandLine(inputs: CommandLineInputs, options: CommandLineOptions): ServeOptions { - const separatedArgs = options['--']; - - if (options['external'] && options['host'] === DEFAULT_ADDRESS) { - options['host'] = '0.0.0.0'; - } - - if (options['address'] && options['host'] === DEFAULT_ADDRESS) { - this.e.log.warn( - `The ${input('--address')} option is deprecated in favor of ${input('--host')}.\n` + - `Please use the ${input('--host')} option (e.g. ${input(`--host=${options['address']}`)}) to specify the host of the dev server.\n` - ); - - options['host'] = options['address']; - } - - const engine = this.determineEngineFromCommandLine(options); - const host = options['host'] ? String(options['host']) : DEFAULT_ADDRESS; - const port = str2num(options['port'], DEFAULT_SERVER_PORT); - const [platform] = options['platform'] ? [String(options['platform'])] : inputs; - - return { - '--': separatedArgs ? separatedArgs : [], - host, - browser: options['browser'] ? String(options['browser']) : undefined, - browserOption: options['browseroption'] ? String(options['browseroption']) : undefined, - engine, - externalAddressRequired: !!options['externalAddressRequired'], - livereload: typeof options['livereload'] === 'boolean' ? Boolean(options['livereload']) : true, - open: !!options['open'], - platform, - port, - proxy: typeof options['proxy'] === 'boolean' ? Boolean(options['proxy']) : true, - project: options['project'] ? String(options['project']) : undefined, - publicHost: options['public-host'] ? String(options['public-host']) : undefined, - verbose: !!options['verbose'], - }; - } - - determineEngineFromCommandLine(options: CommandLineOptions): string { - if (options['engine']) { - return String(options['engine']); - } - - if (options['cordova']) { - return 'cordova'; - } - - return 'browser'; - } - - async beforeServe(options: T) { - const hook = new ServeBeforeHook(this.e); - - try { - await hook.run({ name: hook.name, serve: options }); - } catch (e: any) { - if (e instanceof BaseError) { - throw new FatalException(e.message); - } - - throw e; - } - } - - async run(options: T): Promise { - debug('serve options: %O', options); - - await this.beforeServe(options); - - const details = await this.serveProject(options); - - const localAddress = `${details.protocol}://${options.publicHost ? options.publicHost : 'localhost'}:${details.port}`; - const fmtExternalAddress = (host: string) => `${details.protocol}://${host}:${details.port}`; - - this.e.log.nl(); - this.e.log.info( - `Development server running!` + - `\nLocal: ${strong(localAddress)}` + - (details.externalNetworkInterfaces.length > 0 ? `\nExternal: ${details.externalNetworkInterfaces.map(v => strong(fmtExternalAddress(v.address))).join(', ')}` : '') + - `\n\n${chalk.yellow('Use Ctrl+C to quit this process')}` - ); - this.e.log.nl(); - - if (options.open) { - const openAddress = localAddress; - const url = this.modifyOpenUrl(openAddress, options); - - await openUrl(url, { app: options.browser }); - - this.e.log.info(`Browser window opened to ${strong(url)}!`); - this.e.log.nl(); - } - - emit('serve:ready', details); - debug('serve details: %O', details); - - this.scheduleAfterServe(options, details); - - return details; - } - - async afterServe(options: T, details: ServeDetails) { - const hook = new ServeAfterHook(this.e); - - try { - await hook.run({ name: hook.name, serve: lodash.assign({}, options, details) }); - } catch (e: any) { - if (e instanceof BaseError) { - throw new FatalException(e.message); - } - - throw e; - } - } - - scheduleAfterServe(options: T, details: ServeDetails) { - onBeforeExit(async () => this.afterServe(options, details)); - } - - getUsedPorts(options: T, details: ServeDetails): number[] { - return [details.port]; - } - - async selectExternalIP(options: T): Promise<[string, NetworkInterface[]]> { - let availableInterfaces: NetworkInterface[] = []; - let chosenIP = options.host; - - if (options.host === BIND_ALL_ADDRESS) { - // ignore link-local addresses - availableInterfaces = getExternalIPv4Interfaces().filter(i => !i.address.startsWith('169.254')); - - if (options.publicHost) { - chosenIP = options.publicHost; - } else { - if (availableInterfaces.length === 0) { - if (options.externalAddressRequired) { - throw new FatalException( - `No external network interfaces detected. In order to use the dev server externally you will need one.\n` + - `Are you connected to a local network?\n` - ); - } - } else if (availableInterfaces.length === 1) { - chosenIP = availableInterfaces[0].address; - } else if (availableInterfaces.length > 1) { - if (options.externalAddressRequired) { - if (this.e.flags.interactive) { - this.e.log.warn( - 'Multiple network interfaces detected!\n' + - `You will be prompted to select an external-facing IP for the dev server that your device or emulator can access. Make sure your device is on the same Wi-Fi network as your computer. Learn more about Live Reload in the docs${ancillary('[1]')}.\n\n` + - `To bypass this prompt, use the ${input('--public-host')} option (e.g. ${input(`--public-host=${availableInterfaces[0].address}`)}). You can alternatively bind the dev server to a specific IP (e.g. ${input(`--host=${availableInterfaces[0].address}`)}).\n\n` + - `${ancillary('[1]')}: ${strong('https://ion.link/livereload-docs')}\n` - ); - - const promptedIp = await this.e.prompt({ - type: 'list', - name: 'promptedIp', - message: 'Please select which IP to use:', - choices: availableInterfaces.map(i => ({ - name: `${i.address} ${weak(`(${i.device})`)}`, - value: i.address, - })), - }); - - chosenIP = promptedIp; - } else { - throw new FatalException( - `Multiple network interfaces detected!\n` + - `You must select an external-facing IP for the dev server that your device or emulator can access with the ${input('--public-host')} option.` - ); - } - } - } - } - } else if (options.externalAddressRequired && LOCAL_ADDRESSES.includes(options.host)) { - this.e.log.warn( - 'An external host may be required to serve for this target device/platform.\n' + - 'If you get connection issues on your device or emulator, try connecting the device to the same Wi-Fi network and selecting an accessible IP address for your computer on that network.\n\n' + - `You can use ${input('--external')} to run the dev server on all network interfaces, in which case an external address will be selected.\n` - ); - } - - return [chosenIP, availableInterfaces]; - } -} - -class ServeBeforeHook extends Hook { - readonly name = 'serve:before'; -} - -class ServeAfterHook extends Hook { - readonly name = 'serve:after'; -} - -export interface ServeCLIOptions { - readonly host: string; - readonly port: number; -} - -export interface ServeCLI { - emit(event: 'compile', chunks: number): boolean; - emit(event: 'ready'): boolean; - on(event: 'compile', handler: (chunks: number) => void): this; - on(event: 'ready', handler: () => void): this; - once(event: 'compile', handler: (chunks: number) => void): this; - once(event: 'ready', handler: () => void): this; -} - -export abstract class ServeCLI extends EventEmitter { - - /** - * The pretty name of this Serve CLI. - */ - abstract readonly name: string; - - /** - * The npm package of this Serve CLI. - */ - abstract readonly pkg: string; - - /** - * The bin program to use for this Serve CLI. - */ - abstract readonly program: string; - - /** - * The prefix to use for log statements. - */ - abstract readonly prefix: string; - - /** - * If specified, `package.json` is inspected for this script to use instead - * of `program`. - */ - abstract readonly script?: string; - - /** - * If true, the Serve CLI will not prompt to be installed. - */ - readonly global: boolean = false; - - private _resolvedProgram?: string; - - constructor(protected readonly e: ServeRunnerDeps) { - super(); - } - - get resolvedProgram() { - if (this._resolvedProgram) { - return this._resolvedProgram; - } - - return this.program; - } - - /** - * Build the arguments for starting this Serve CLI. Called by `this.start()`. - */ - protected abstract buildArgs(options: T): Promise; - - /** - * Build the environment variables to be passed to the Serve CLI. Called by `this.start()`; - */ - protected async buildEnvVars(options: T): Promise { - return process.env; - } - - /** - * Called whenever a line of stdout is received. - * - * If `false` is returned, the line is not emitted to the log. - * - * By default, the CLI is considered ready whenever stdout is emitted. This - * method should be overridden to more accurately portray readiness. - * - * @param line A line of stdout. - */ - protected stdoutFilter(line: string): boolean { - this.emit('ready'); - - return true; - } - - /** - * Called whenever a line of stderr is received. - * - * If `false` is returned, the line is not emitted to the log. - */ - protected stderrFilter(line: string): boolean { - return true; - } - - async resolveScript(): Promise { - if (typeof this.script === 'undefined') { - return; - } - - const [pkg] = await this.e.project.getPackageJson(undefined, { logErrors: false }); - - if (!pkg) { - return; - } - - return pkg.scripts && pkg.scripts[this.script]; - } - - async serve(options: T): Promise { - this._resolvedProgram = await this.resolveProgram(); - - await this.spawnWrapper(options); - - const interval = setInterval(() => { - this.e.log.info(`Waiting for connectivity with ${input(this.resolvedProgram)}...`); - }, 5000); - - debug('awaiting TCP connection to %s:%d', options.host, options.port); - await isHostConnectable(options.host, options.port); - clearInterval(interval); - } - - protected async spawnWrapper(options: T): Promise { - try { - return await this.spawn(options); - } catch (e: any) { - if (!(e instanceof ServeCLIProgramNotFoundException)) { - throw e; - } - - if (this.global) { - this.e.log.nl(); - throw new FatalException(`${input(this.pkg)} is required for this command to work properly.`); - } - - this.e.log.nl(); - this.e.log.info( - `Looks like ${input(this.pkg)} isn't installed in this project.\n` + - `This package is required for this command to work properly. The package provides a CLI utility, but the ${input(this.resolvedProgram)} binary was not found in your PATH.` - ); - - const installed = await this.promptToInstall(); - - if (!installed) { - this.e.log.nl(); - throw new FatalException(`${input(this.pkg)} is required for this command to work properly.`); - } - - return this.spawn(options); - } - } - - protected async spawn(options: T): Promise { - const args = await this.buildArgs(options); - const env = await this.buildEnvVars(options); - const p = await this.e.shell.spawn(this.resolvedProgram, args, { stdio: ['inherit', 'pipe', 'pipe'], cwd: this.e.project.directory, env: createProcessEnv(env) }); - - return new Promise((resolve, reject) => { - const errorHandler = (err: NodeJS.ErrnoException) => { - debug('received error for %s: %o', this.resolvedProgram, err); - - if (this.resolvedProgram === this.program && err.code === 'ENOENT') { - p.removeListener('close', closeHandler); // do not exit Ionic CLI, we can gracefully ask to install this CLI - reject(new ServeCLIProgramNotFoundException(`${strong(this.resolvedProgram)} command not found.`)); - } else { - reject(err); - } - }; - - const closeHandler = (code: number | null) => { - if (code !== null) { - debug('received unexpected close for %s (code: %d)', this.resolvedProgram, code); - - this.e.log.nl(); - this.e.log.error( - `${input(this.resolvedProgram)} has unexpectedly closed (exit code ${code}).\n` + - 'The Ionic CLI will exit. Please check any output above for error details.' - ); - - processExit(1); - } - }; - - p.on('error', errorHandler); - p.on('close', closeHandler); - - onBeforeExit(async () => { - p.removeListener('close', closeHandler); - - if (p.pid) { - await killProcessTree(p.pid); - } - }); - - const ws = this.createLoggerStream(); - - p.stdout?.pipe(split2()).pipe(this.createStreamFilter(line => this.stdoutFilter(line))).pipe(ws); - p.stderr?.pipe(split2()).pipe(this.createStreamFilter(line => this.stderrFilter(line))).pipe(ws); - - this.once('ready', () => { - resolve(); - }); - }); - } - - protected createLoggerStream(): NodeJS.WritableStream { - const log = this.e.log.clone(); - log.handlers = createDefaultLoggerHandlers(createPrefixedFormatter(weak(`[${this.resolvedProgram === this.program ? this.prefix : this.resolvedProgram}]`))); - return log.createWriteStream(LOGGER_LEVELS.INFO); - } - - protected async resolveProgram(): Promise { - if (typeof this.script !== 'undefined') { - debug(`Looking for ${ancillary(this.script)} npm script.`); - - if (await this.resolveScript()) { - debug(`Using ${ancillary(this.script)} npm script.`); - return this.e.config.get('npmClient'); - } - } - - return this.program; - } - - protected createStreamFilter(filter: (line: string) => boolean): stream.Transform { - return new stream.Transform({ - transform(chunk, enc, callback) { - const str = chunk.toString(); - - if (filter(str)) { - this.push(chunk); - } - - callback(); - }, - }); - } - - protected async promptToInstall(): Promise { - const { pkgManagerArgs } = await import('./utils/npm'); - const [manager, ...managerArgs] = await pkgManagerArgs(this.e.config.get('npmClient'), { command: 'install', pkg: this.pkg, saveDev: true, saveExact: true }); - - this.e.log.nl(); - - const confirm = await this.e.prompt({ - name: 'confirm', - message: `Install ${input(this.pkg)}?`, - type: 'confirm', - }); - - if (!confirm) { - this.e.log.warn(`Not installing--here's how to install manually: ${input(`${manager} ${managerArgs.join(' ')}`)}`); - return false; - } - - await this.e.shell.run(manager, managerArgs, { cwd: this.e.project.directory }); - - return true; - } -} - -abstract class PkgManagerServeCLI extends ServeCLI { - readonly abstract program: NpmClient; - readonly global = true; - readonly script = SERVE_SCRIPT; - - protected async resolveProgram(): Promise { - return this.program; - } - - protected async buildArgs(options: ServeOptions): Promise { - const { pkgManagerArgs } = await import('./utils/npm'); - - // The Ionic CLI decides the host/port of the dev server, so --host and - // --port are provided to the downstream npm script as a best-effort - // attempt. - const args: ParsedArgs = { - _: [], - host: options.host, - port: options.port.toString(), - }; - - const scriptArgs = [...unparseArgs(args), ...options['--'] || []]; - const [, ...pkgArgs] = await pkgManagerArgs(this.program, { command: 'run', script: this.script, scriptArgs }); - - return pkgArgs; - } -} - -export class NpmServeCLI extends PkgManagerServeCLI { - readonly name = 'npm CLI'; - readonly pkg = 'npm'; - readonly program = 'npm'; - readonly prefix = 'npm'; -} - -export class PnpmServeCLI extends PkgManagerServeCLI { - readonly name = 'pnpm CLI'; - readonly pkg = 'pnpm'; - readonly program = 'pnpm'; - readonly prefix = 'pnpm'; -} - -export class YarnServeCLI extends PkgManagerServeCLI { - readonly name = 'Yarn'; - readonly pkg = 'yarn'; - readonly program = 'yarn'; - readonly prefix = 'yarn'; -} diff --git a/packages/@ionic/cli/src/lib/session.ts b/packages/@ionic/cli/src/lib/session.ts deleted file mode 100644 index 70cd7b8a88..0000000000 --- a/packages/@ionic/cli/src/lib/session.ts +++ /dev/null @@ -1,256 +0,0 @@ -import { IClient, IConfig, ISession, IonicEnvironment } from '../definitions'; -import { isLoginResponse, isSuperAgentError } from '../guards'; - -import { input } from './color'; -import { FatalException, SessionException } from './errors'; -import { formatResponseError } from './http'; -import { openUrl } from './open'; - -export interface SessionDeps { - readonly config: IConfig; - readonly client: IClient; -} - -export class BaseSession { - constructor(readonly e: SessionDeps) {} - - async logout(): Promise { - const activeToken = this.e.config.get('tokens.user'); - if (activeToken) { - // invalidate the token - const { req } = await this.e.client.make('POST', '/logout'); - req.set('Authorization', `Bearer ${activeToken}`) - .send({}); - try { - await this.e.client.do(req); - } catch (e: any) {} - } - this.e.config.unset('org.id'); - this.e.config.unset('user.id'); - this.e.config.unset('user.email'); - this.e.config.unset('tokens.user'); - this.e.config.unset('tokens.refresh'); - this.e.config.unset('tokens.expiresInSeconds'); - this.e.config.unset('tokens.issuedOn'); - this.e.config.unset('tokens.flowName'); - this.e.config.set('git.setup', false); - - } - - isLoggedIn(): boolean { - return typeof this.e.config.get('tokens.user') === 'string'; - } - - getUser(): { id: number; } { - const userId = this.e.config.get('user.id'); - - if (!userId) { - throw new SessionException( - `Oops, sorry! You'll need to log in:\n ${input('ionic login')}\n\n` + - `You can create a new account by signing up:\n\n ${input('ionic signup')}\n` - ); - } - - return { id: userId }; - } -} - -export class ProSession extends BaseSession implements ISession { - - async getUserToken(): Promise { - let userToken = this.e.config.get('tokens.user'); - - if (!userToken) { - throw new SessionException( - `Oops, sorry! You'll need to log in:\n ${input('ionic login')}\n\n` + - `You can create a new account by signing up:\n\n ${input('ionic signup')}\n` - ); - } - - const tokenIssuedOn = this.e.config.get('tokens.issuedOn'); - const tokenExpirationSeconds = this.e.config.get('tokens.expiresInSeconds'); - const refreshToken = this.e.config.get('tokens.refresh'); - const flowName = this.e.config.get('tokens.flowName'); - - // if there is the possibility to refresh the token, try to do it - if (tokenIssuedOn && tokenExpirationSeconds && refreshToken && flowName) { - if (!this.isTokenValid(tokenIssuedOn, tokenExpirationSeconds)) { - userToken = await this.refreshLogin(refreshToken, flowName); - } - } - - // otherwise simply return the token - return userToken; - } - - private isTokenValid(tokenIssuedOn: string, tokenExpirationSeconds: number): boolean { - const tokenExpirationMilliSeconds = tokenExpirationSeconds * 1000; - // 15 minutes in milliseconds of margin - const marginExpiration = 15 * 60 * 1000; - const tokenValid = new Date() < new Date(new Date(tokenIssuedOn).getTime() + tokenExpirationMilliSeconds - marginExpiration); - return tokenValid; - } - - async login(email: string, password: string): Promise { - const { req } = await this.e.client.make('POST', '/login'); - req.send({ email, password, source: 'cli' }); - - try { - const res = await this.e.client.do(req); - - if (!isLoginResponse(res)) { - const data = res.data; - - if (hasTokenAttribute(data)) { - data.token = '*****'; - } - - throw new FatalException( - 'API request was successful, but the response format was unrecognized.\n' + - formatResponseError(req, res.meta.status, data) - ); - } - - const { token, user } = res.data; - - if (this.e.config.get('user.id') !== user.id) { // User changed - await this.logout(); - } - - this.e.config.set('user.id', user.id); - this.e.config.set('user.email', email); - this.e.config.set('tokens.user', token); - } catch (e: any) { - if (isSuperAgentError(e) && (e.response.status === 401 || e.response.status === 403)) { - throw new SessionException('Incorrect email or password.'); - } - - throw e; - } - } - - async ssoLogin(email?: string): Promise { - await this.webLogin(); - } - - async tokenLogin(token: string): Promise { - const { UserClient } = await import('./user'); - - const userClient = new UserClient(token, this.e); - - try { - const user = await userClient.loadSelf(); - const user_id = user.id; - - if (this.e.config.get('user.id') !== user_id) { // User changed - await this.logout(); - } - - this.e.config.set('user.id', user_id); - this.e.config.set('user.email', user.email); - this.e.config.set('tokens.user', token); - } catch (e: any) { - if (isSuperAgentError(e) && (e.response.status === 401 || e.response.status === 403)) { - throw new SessionException('Invalid auth token.'); - } - - throw e; - } - } - - async wizardLogin(): Promise { - const { OpenIDFlow } = await import('./oauth/openid'); - const wizardUrl = new URL(this.e.config.getOpenIDOAuthConfig().authorizationUrl); - wizardUrl.pathname = 'start'; - const flow = new OpenIDFlow({}, this.e, wizardUrl.href); - const token = await flow.run(); - - await this.tokenLogin(token.access_token); - - this.e.config.set('tokens.refresh', token.refresh_token); - this.e.config.set('tokens.expiresInSeconds', token.expires_in); - this.e.config.set('tokens.issuedOn', (new Date()).toJSON()); - this.e.config.set('tokens.flowName', flow.flowName); - return token.state; - } - - async webLogin(): Promise { - const { OpenIDFlow } = await import('./oauth/openid'); - const flow = new OpenIDFlow({}, this.e); - const token = await flow.run(); - - await this.tokenLogin(token.access_token); - - this.e.config.set('tokens.refresh', token.refresh_token); - this.e.config.set('tokens.expiresInSeconds', token.expires_in); - this.e.config.set('tokens.issuedOn', (new Date()).toJSON()); - this.e.config.set('tokens.flowName', flow.flowName); - } - - async refreshLogin(refreshToken: string, flowName: string): Promise { - let oauthflow; - // having a generic way to access the right refresh token flow - switch (flowName) { - case 'open_id': - const { OpenIDFlow } = await import('./oauth/openid'); - oauthflow = new OpenIDFlow({}, this.e); - break; - default: - oauthflow = undefined; - } - if (!oauthflow) { - throw new FatalException('Token cannot be refreshed'); - } - - const token = await oauthflow.exchangeRefreshToken(refreshToken); - await this.tokenLogin(token.access_token); - this.e.config.set('tokens.expiresInSeconds', token.expires_in); - this.e.config.set('tokens.issuedOn', (new Date()).toJSON()); - - return token.access_token; - } -} - -export async function promptToLogin(env: IonicEnvironment): Promise { - env.log.nl(); - env.log.msg( - `Log in to your Ionic account!\n` + - `If you don't have one yet, create yours by running: ${input(`ionic signup`)}\n` - ); - - const login = await env.prompt({ - type: 'confirm', - name: 'login', - message: 'Open the browser to log in to your Ionic account?', - default: true, - }); - - if (login) { - await env.session.webLogin(); - } -} - -export async function promptToSignup(env: IonicEnvironment): Promise { - env.log.nl(); - env.log.msg( - `Join the Ionic Community! 💙\n` + - `Connect with millions of developers on the Ionic Forum and get access to live events, news updates, and more.\n\n` - ); - - const create = await env.prompt({ - type: 'confirm', - name: 'create', - message: 'Create free Ionic account?', - default: false, - }); - - if (create) { - const dashUrl = env.config.getDashUrl(); - - await openUrl(`${dashUrl}/signup?source=cli`); - } -} - -function hasTokenAttribute(r: any): r is { token: string; } { - return r && typeof r === 'object' && typeof r.token === 'string'; -} diff --git a/packages/@ionic/cli/src/lib/shell.ts b/packages/@ionic/cli/src/lib/shell.ts deleted file mode 100644 index e4a60a01de..0000000000 --- a/packages/@ionic/cli/src/lib/shell.ts +++ /dev/null @@ -1,232 +0,0 @@ -import { LOGGER_LEVELS } from '@ionic/cli-framework-output'; -import { createProcessEnv, killProcessTree, onBeforeExit } from '@ionic/utils-process'; -import { ERROR_COMMAND_NOT_FOUND, Subprocess, SubprocessError, SubprocessOptions, WhichOptions, which } from '@ionic/utils-subprocess'; -import { TERMINAL_INFO } from '@ionic/utils-terminal'; -import chalk from 'chalk'; -import { ChildProcess, SpawnOptions } from 'child_process'; -import { debug as Debug } from 'debug'; -import * as path from 'path'; -import split2 from 'split2'; -import combineStreams from 'stream-combiner2'; - -import { ILogger, IShell, IShellOutputOptions, IShellRunOptions, IShellSpawnOptions } from '../definitions'; -import { isExitCodeException } from '../guards'; - -import { input, strong } from './color'; -import { FatalException } from './errors'; - -const debug = Debug('ionic:lib:shell'); - -export interface ShellDeps { - readonly log: ILogger; -} - -export interface ShellOptions { - readonly alterPath?: (p: string) => string; -} - -export class Shell implements IShell { - alterPath: (p: string) => string; - - constructor(protected readonly e: ShellDeps, options?: ShellOptions) { - this.alterPath = options && options.alterPath ? options.alterPath : (p: string) => p; - } - - async run(command: string, args: readonly string[], { stream, killOnExit = true, showCommand = true, showError = true, fatalOnNotFound = true, fatalOnError = true, truncateErrorOutput, ...crossSpawnOptions }: IShellRunOptions): Promise { - const proc = await this.createSubprocess(command, args, crossSpawnOptions); - - const fullCmd = proc.bashify(); - const truncatedCmd = fullCmd.length > 80 ? fullCmd.substring(0, 80) + '...' : fullCmd; - - if (showCommand && this.e.log.level >= LOGGER_LEVELS.INFO) { - this.e.log.rawmsg(`> ${input(fullCmd)}`); - } - - const ws = stream ? stream : this.e.log.createWriteStream(LOGGER_LEVELS.INFO, false); - - try { - const promise = proc.run(); - - if (promise.p.stdout) { - const s = combineStreams(split2(), ws); - - // TODO: https://github.com/angular/angular-cli/issues/10922 - s.on('error', (err: Error) => { - debug('Error in subprocess stdout pipe: %o', err); - }); - - promise.p.stdout?.pipe(s); - } - - if (promise.p.stderr) { - const s = combineStreams(split2(), ws); - - // TODO: https://github.com/angular/angular-cli/issues/10922 - s.on('error', (err: Error) => { - debug('Error in subprocess stderr pipe: %o', err); - }); - - promise.p.stderr?.pipe(s); - } - - if (killOnExit) { - onBeforeExit(async () => { - if (promise.p.pid) { - await killProcessTree(promise.p.pid); - } - }); - } - - await promise; - } catch (e: any) { - if (e instanceof SubprocessError && e.code === ERROR_COMMAND_NOT_FOUND) { - if (fatalOnNotFound) { - throw new FatalException(`Command not found: ${input(command)}`, 127); - } else { - throw e; - } - } - - if (!isExitCodeException(e)) { - throw e; - } - - let err = e.message || ''; - - if (truncateErrorOutput && err.length > truncateErrorOutput) { - err = `${strong('(truncated)')} ... ` + err.substring(err.length - truncateErrorOutput); - } - - const publicErrorMsg = ( - `An error occurred while running subprocess ${input(command)}.\n` + - `${input(truncatedCmd)} exited with exit code ${e.exitCode}.\n\n` + - `Re-running this command with the ${input('--verbose')} flag may provide more information.` - ); - - const privateErrorMsg = `Subprocess (${input(command)}) encountered an error (exit code ${e.exitCode}).`; - - if (fatalOnError) { - if (showError) { - throw new FatalException(publicErrorMsg, e.exitCode); - } else { - throw new FatalException(privateErrorMsg, e.exitCode); - } - } else { - if (showError) { - this.e.log.error(publicErrorMsg); - } - } - - throw e; - } - } - - async output(command: string, args: readonly string[], { fatalOnNotFound = true, fatalOnError = true, showError = true, showCommand = false, ...crossSpawnOptions }: IShellOutputOptions): Promise { - const proc = await this.createSubprocess(command, args, crossSpawnOptions); - const fullCmd = proc.bashify(); - const truncatedCmd = fullCmd.length > 80 ? fullCmd.substring(0, 80) + '...' : fullCmd; - - if (showCommand && this.e.log.level >= LOGGER_LEVELS.INFO) { - this.e.log.rawmsg(`> ${input(fullCmd)}`); - } - - try { - return await proc.output(); - } catch (e: any) { - if (e instanceof SubprocessError && e.code === ERROR_COMMAND_NOT_FOUND) { - if (fatalOnNotFound) { - throw new FatalException(`Command not found: ${input(command)}`, 127); - } else { - throw e; - } - } - - if (!isExitCodeException(e)) { - throw e; - } - - const errorMsg = `An error occurred while running ${input(truncatedCmd)} (exit code ${e.exitCode})\n`; - - if (fatalOnError) { - throw new FatalException(errorMsg, e.exitCode); - } else { - if (showError) { - this.e.log.error(errorMsg); - } - } - - return ''; - } - } - - /** - * When `child_process.spawn` isn't provided a full path to the command - * binary, it behaves differently on Windows than other platforms. For - * Windows, discover the full path to the binary, otherwise fallback to the - * command provided. - * - * @see https://github.com/ionic-team/ionic-cli/issues/3563#issuecomment-425232005 - */ - async resolveCommandPath(command: string, options: SpawnOptions): Promise { - if (TERMINAL_INFO.windows) { - try { - return await this.which(command, { PATH: options.env && options.env.PATH ? options.env.PATH : process.env.PATH }); - } catch (e: any) { - // ignore - } - } - - return command; - } - - async which(command: string, { PATH = process.env.PATH }: WhichOptions = {}): Promise { - return which(command, { PATH: this.alterPath(PATH || '') }); - } - - async spawn(command: string, args: readonly string[], { showCommand = true, ...crossSpawnOptions }: IShellSpawnOptions): Promise { - const proc = await this.createSubprocess(command, args, crossSpawnOptions); - const p = proc.spawn(); - - if (showCommand && this.e.log.level >= LOGGER_LEVELS.INFO) { - this.e.log.rawmsg(`> ${input(proc.bashify())}`); - } - - return p; - } - - async cmdinfo(command: string, args: readonly string[] = []): Promise { - const opts: IShellSpawnOptions = {}; - - const proc = await this.createSubprocess(command, args, opts); - - try { - const out = await proc.output(); - return out.split('\n').join(' ').trim(); - } catch (e: any) { - // no command info at this point - } - } - - async createSubprocess(command: string, args: readonly string[] = [], options: SubprocessOptions = {}): Promise { - this.prepareSpawnOptions(options); - const cmdpath = await this.resolveCommandPath(command, options); - const proc = new Subprocess(cmdpath, args, options); - - return proc; - } - - protected prepareSpawnOptions(options: IShellSpawnOptions) { - // Create a `process.env`-type object from all key/values of `process.env`, - // then `options.env`, then add several key/values. PATH is supplemented - // with the `node_modules\.bin` folder in the project directory so that we - // can run binaries inside a project. - options.env = createProcessEnv(process.env, options.env ?? {}, { - PATH: this.alterPath(process.env.PATH || ''), - FORCE_COLOR: chalk.level > 0 ? '1' : '0', - }); - } -} - -export function prependNodeModulesBinToPath(projectDir: string, p: string): string { - return path.resolve(projectDir, 'node_modules', '.bin') + path.delimiter + p; -} diff --git a/packages/@ionic/cli/src/lib/snapshot.ts b/packages/@ionic/cli/src/lib/snapshot.ts deleted file mode 100644 index b080316370..0000000000 --- a/packages/@ionic/cli/src/lib/snapshot.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { IClient, IPaginator, PaginateArgs, ResourceClientLoad, ResourceClientPaginate, Response, Snapshot } from '../definitions'; -import { isSnapshotListResponse, isSnapshotResponse } from '../guards'; - -import { ResourceClient, createFatalAPIFormat } from './http'; - -export interface SnapshotClientDeps { - readonly client: IClient; - readonly token: string; - readonly app: { id: string; }; -} - -export class SnapshotClient extends ResourceClient implements ResourceClientLoad, ResourceClientPaginate { - protected client: IClient; - protected token: string; - protected app: { id: string; }; - - constructor({ client, app, token }: SnapshotClientDeps) { - super(); - this.client = client; - this.token = token; - this.app = app; - } - - async load(id: string): Promise { - const { req } = await this.client.make('GET', `/apps/${this.app.id}/snapshots/${id}`); - this.applyAuthentication(req, this.token); - const res = await this.client.do(req); - - if (!isSnapshotResponse(res)) { - throw createFatalAPIFormat(req, res); - } - - return res.data; - } - - paginate(args: Partial>> = {}): IPaginator> { - return this.client.paginate({ - reqgen: async () => { - const { req } = await this.client.make('GET', `/apps/${this.app.id}/snapshots`); - this.applyAuthentication(req, this.token); - return { req }; - }, - guard: isSnapshotListResponse, - }); - } -} diff --git a/packages/@ionic/cli/src/lib/ssh-config.ts b/packages/@ionic/cli/src/lib/ssh-config.ts deleted file mode 100644 index edbeda7e33..0000000000 --- a/packages/@ionic/cli/src/lib/ssh-config.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { fileToString } from '@ionic/utils-fs'; -import os from 'os'; -import * as path from 'path'; -import SSHConfig from 'ssh-config'; - -export { SSHConfig }; - -export async function loadFromPath(p: string): Promise { - const s = await fileToString(p); - - return SSHConfig.parse(s); -} - -export function isDirective(entry: any): entry is SSHConfig.ConfigDirective { - return entry && entry.type === SSHConfig.DIRECTIVE; -} - -export function isHostDirective(entry: SSHConfig.Config): entry is SSHConfig.ConfigHostDirective { - return isDirective(entry) && entry.param === 'Host'; -} - -export function getConfigPath() { - return path.resolve(os.homedir(), '.ssh', 'config'); -} - -export function findHostSection(conf: SSHConfig.SSHConfig, host: string): SSHConfig.ConfigHostDirective | null { - return conf.find({ Host: host }); -} - -export function ensureHostAndKeyPath(conf: SSHConfig.SSHConfig, conn: { host: string, port?: number }, keyPath: string): void { - const section = ensureHostSection(conf, conn.host); - const index = conf.indexOf(section); - - ensureSectionLine(section, 'IdentityFile', keyPath); - - if (typeof conn.port === 'number' && conn.port !== 22) { - ensureSectionLine(section, 'Port', String(conn.port)); - } - - // massage the section for proper whitespace - - if (index === 0) { - section.before = ''; - } else { - const previousSection = conf[index - 1]; - - if (isHostDirective(previousSection)) { - const previousSectionLastEntry = previousSection.config[previousSection.config.length - 1]; - - if (previousSectionLastEntry) { - previousSectionLastEntry.after = '\n'; - } - } else { - previousSection.after = '\n'; - } - - section.before = '\n'; - } - - section.after = '\n'; - - if (!section.config) { - section.config = []; - } - - for (const entry of section.config) { - entry.before = ' '; - entry.after = '\n'; - } - - if (index !== conf.length - 1) { - const lastEntry = section.config[section.config.length - 1]; - lastEntry.after = '\n\n'; - } -} - -function ensureHostSection(conf: SSHConfig.SSHConfig, host: string): SSHConfig.ConfigHostDirective { - let section = findHostSection(conf, host); - - if (!section) { - conf.push(SSHConfig.parse(`\nHost ${host}\n`)[0]); - section = findHostSection(conf, host); - } - - if (!section) { - throw new Error(`Could not find/insert section for host: ${host}`); - } - - return section; -} - -function ensureSectionLine(section: SSHConfig.ConfigHostDirective, key: string, value: string): void { - const found = section.config.some(line => { - if (isDirective(line)) { - if (line.param === key) { - line.value = value; - return true; - } - } - - return false; - }); - - if (!found) { - section.config = section.config.concat(SSHConfig.parse(`${key} ${value}\n`)); - } -} diff --git a/packages/@ionic/cli/src/lib/ssh.ts b/packages/@ionic/cli/src/lib/ssh.ts deleted file mode 100644 index 5f89a3deff..0000000000 --- a/packages/@ionic/cli/src/lib/ssh.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { readFile, stat } from '@ionic/utils-fs'; -import * as os from 'os'; -import * as path from 'path'; - -import { IClient, IPaginator, PaginateArgs, PaginatorState, ResourceClientCreate, ResourceClientDelete, ResourceClientLoad, ResourceClientPaginate, Response, SSHKey } from '../definitions'; -import { isSSHKeyListResponse, isSSHKeyResponse } from '../guards'; - -import { ResourceClient, createFatalAPIFormat } from './http'; - -export const ERROR_SSH_MISSING_PRIVKEY = 'SSH_MISSING_PRIVKEY'; -export const ERROR_SSH_INVALID_PUBKEY = 'SSH_INVALID_PUBKEY'; -export const ERROR_SSH_INVALID_PRIVKEY = 'SSH_INVALID_PRIVKEY'; - -export async function getGeneratedPrivateKeyPath(userId = 0): Promise { - return path.resolve(os.homedir(), '.ssh', 'ionic', String(userId)); -} - -export async function parsePublicKeyFile(pubkeyPath: string): Promise<[string, string, string, string]> { - return parsePublicKey((await readFile(pubkeyPath, { encoding: 'utf8' })).trim()); -} - -/** - * @return [full pubkey, algorithm, public numbers, annotation] - */ -export function parsePublicKey(pubkey: string): [string, string, string, string] { - const r = /^(ssh-[A-z0-9]+)\s([A-z0-9+\/=]+)\s?(.+)?$/.exec(pubkey); - - if (!r) { - throw ERROR_SSH_INVALID_PUBKEY; - } - - if (!r[3]) { - r[3] = ''; - } - - r[1] = r[1].trim(); - r[2] = r[2].trim(); - r[3] = r[3].trim(); - - return [pubkey, r[1], r[2], r[3]]; -} - -export async function validatePrivateKey(keyPath: string): Promise { - try { - await stat(keyPath); - } catch (e: any) { - if (e.code === 'ENOENT') { - throw ERROR_SSH_MISSING_PRIVKEY; - } - - throw e; - } - - const f = await readFile(keyPath, { encoding: 'utf8' }); - const lines = f.split('\n'); - - if (!lines[0].match(/^\-{5}BEGIN [A-Z]+ PRIVATE KEY\-{5}$/)) { - throw ERROR_SSH_INVALID_PRIVKEY; - } -} - -export interface SSHKeyClientDeps { - readonly client: IClient; - readonly token: string; - readonly user: { id: number; }; -} - -export interface SSHKeyCreateDetails { - pubkey: string; -} - -export class SSHKeyClient extends ResourceClient implements ResourceClientLoad, ResourceClientDelete, ResourceClientCreate, ResourceClientPaginate { - protected client: IClient; - protected token: string; - protected user: { id: number; }; - - constructor({ client, token, user }: SSHKeyClientDeps) { - super(); - this.client = client; - this.token = token; - this.user = user; - } - - async create({ pubkey }: SSHKeyCreateDetails): Promise { - const { req } = await this.client.make('POST', `/users/${this.user.id}/sshkeys`); - this.applyAuthentication(req, this.token); - req.send({ pubkey }); - const res = await this.client.do(req); - - if (!isSSHKeyResponse(res)) { - throw createFatalAPIFormat(req, res); - } - - return res.data; - } - - async load(id: string): Promise { - const { req } = await this.client.make('GET', `/users/${this.user.id}/sshkeys/${id}`); - this.applyAuthentication(req, this.token); - const res = await this.client.do(req); - - if (!isSSHKeyResponse(res)) { - throw createFatalAPIFormat(req, res); - } - - return res.data; - } - - async delete(id: string): Promise { - const { req } = await this.client.make('DELETE', `/users/${this.user.id}/sshkeys/${id}`); - this.applyAuthentication(req, this.token); - await this.client.do(req); - } - - paginate(args: Partial>> = {}): IPaginator, PaginatorState> { - return this.client.paginate({ - reqgen: async () => { - const { req } = await this.client.make('GET', `/users/${this.user.id}/sshkeys`); - this.applyAuthentication(req, this.token); - return { req }; - }, - guard: isSSHKeyListResponse, - }); - } -} diff --git a/packages/@ionic/cli/src/lib/start.ts b/packages/@ionic/cli/src/lib/start.ts deleted file mode 100644 index cf0a68d043..0000000000 --- a/packages/@ionic/cli/src/lib/start.ts +++ /dev/null @@ -1,346 +0,0 @@ -import { readJson } from '@ionic/utils-fs'; -import { columnar } from '@ionic/utils-terminal'; -import * as lodash from 'lodash'; - -import { COLUMNAR_OPTIONS, PROJECT_TYPES, ANGULAR_STANDALONE } from '../constants'; -import { CommandLineOptions, IConfig, ILogger, ProjectType, StarterList, StarterManifest, StarterTemplate } from '../definitions'; -import { isStarterManifest } from '../guards'; - -import { input, strong, title } from './color'; -import { FatalException } from './errors'; -import { prettyProjectName } from './project'; -import { emoji } from './utils/emoji'; -import { createRequest } from './utils/http'; - -export const STARTER_BASE_URL = 'https://d2ql0qc7j8u4b2.cloudfront.net'; - -export interface BaseAppSchema { - projectId: string; - projectDir: string; - packageId?: string; - appflowId?: string; -} - -export interface NewAppSchema extends BaseAppSchema { - cloned: false; - name: string; - type: ProjectType; - template: string; - themeColor?: string; - appIcon?: Buffer; - splash?: Buffer; -} - -export interface ClonedAppSchema extends BaseAppSchema { - cloned: true; - url: string; -} - -export type AppSchema = NewAppSchema | ClonedAppSchema; - -export function verifyOptions(options: CommandLineOptions, { log }: { log: ILogger; }): void { - // If the action is list then lets just end here. - if (options['list']) { - const typeOption = options['type'] ? String(options['type']) : undefined; - - if (typeOption && !PROJECT_TYPES.includes(typeOption as ProjectType)) { - throw new FatalException( - `${input(typeOption)} is not a valid project type.\n` + - `Valid project types are: ${getStarterProjectTypes().map(type => input(type)).join(', ')}` - ); - } - - const headers = ['name', 'description'].map(h => strong(h)); - const starterTypes = typeOption ? [typeOption] : getStarterProjectTypes(); - - for (const starterType of starterTypes) { - const starters = STARTER_TEMPLATES.filter(template => template.projectType === starterType); - - log.rawmsg(`\n${strong(`Starters for ${prettyProjectName(starterType)}`)} (${input(`--type=${starterType}`)})\n\n`); - log.rawmsg(columnar(starters.map(({ name, description }) => [input(name), description || '']), { ...COLUMNAR_OPTIONS, headers })); - log.rawmsg('\n'); - } - - throw new FatalException('', 0); - } - - if (options['skip-deps']) { - log.warn(`The ${input('--skip-deps')} option has been deprecated. Please use ${input('--no-deps')}.`); - options['deps'] = false; - } - - if (options['skip-link']) { - log.warn(`The ${input('--skip-link')} option has been deprecated. Please use ${input('--no-link')}.`); - options['link'] = false; - } - - if (options['pro-id']) { - log.warn(`The ${input('--pro-id')} option has been deprecated. Please use ${input('--id')}.`); - options['id'] = options['pro-id']; - } - - if (options['id']) { - if (options['link'] === false) { - log.warn(`The ${input('--no-link')} option has no effect with ${input('--id')}. App must be linked.`); - } - - options['link'] = true; - - if (!options['git']) { - log.warn(`The ${input('--no-git')} option has no effect with ${input('--id')}. Git must be used.`); - } - - options['git'] = true; - } -} - -export async function readStarterManifest(p: string): Promise { - try { - const manifest = await readJson(p); - - if (!isStarterManifest(manifest)) { - throw new Error(`${p} is not a valid starter manifest.`); - } - - return manifest; - } catch (e: any) { - if (e.code === 'ENOENT') { - throw new Error(`${p} not found`); - } else if (e instanceof SyntaxError) { - throw new Error(`${p} is not valid JSON.`); - } - - throw e; - } -} - -export function getAdvertisement(): string { - const choices = [getAppflowAdvertisement, getAdvisoryAdvertisement, getEnterpriseAdvertisement]; - const idx = Math.floor(Math.random() * choices.length); - - return `${choices[idx]()}\n\n`; -} - -function getAppflowAdvertisement(): string { - return ` - ────────────────────────────────────────────────────────────── - - ${title('Ionic Appflow')}, the mobile DevOps solution by Ionic - - Continuously build, deploy, and ship apps ${emoji('🚀', '')} - Focus on building apps while we automate the rest ${emoji('🎁', '')} - - ${emoji(' 👉 ', 'Learn more:')} ${strong('https://ion.link/appflow')} ${emoji(' 👈', '')} - - ────────────────────────────────────────────────────────────── -`; -} - -function getAdvisoryAdvertisement(): string { - return ` - ────────────────────────────────────────────────────────────────────────────── - - ${title('Ionic Advisory')}, tailored solutions and expert services by Ionic - - Go to market faster ${emoji('🏆', '')} - Real-time troubleshooting and guidance ${emoji('💁', '')} - Custom training, best practices, code and architecture reviews ${emoji('🔎', '')} - Customized strategies for every phase of the development lifecycle ${emoji('🔮', '')} - - ${emoji(' 👉 ', 'Learn more:')} ${strong('https://ion.link/advisory')} ${emoji(' 👈', '')} - - ────────────────────────────────────────────────────────────────────────────── -`; -} - -function getEnterpriseAdvertisement(): string { - return ` - ────────────────────────────────────────────────────────────────────── - - ${title('Ionic Enterprise')}, platform and solutions for teams by Ionic - - Powerful library of native APIs ${emoji('⚡️', '')} - A supercharged platform for teams ${emoji('💪', '')} - - ${emoji(' 👉 ', 'Learn more:')} ${strong('https://ion.link/enterprise')} ${emoji(' 👈', '')} - - ────────────────────────────────────────────────────────────────────── -`; -} - -export async function getStarterList(config: IConfig, tag = 'latest'): Promise { - const { req } = await createRequest('GET', `${STARTER_BASE_URL}/${tag === 'latest' ? '' : `${tag}/`}starters.json`, config.getHTTPConfig()); - - const res = await req; - - // TODO: typecheck - - return res.body; -} - -export function getStarterProjectTypes(): string[] { - return lodash.uniq(STARTER_TEMPLATES.map(t => t.projectType)); -} - -export interface SupportedFramework { - name: string; - type: ProjectType; - description: string; -} - -export const SUPPORTED_FRAMEWORKS: readonly SupportedFramework[] = [ - { - name: 'Angular', - type: 'angular', - description: 'https://angular.io', - }, - { - name: 'React', - type: 'react', - description: 'https://reactjs.org', - }, - { - name: 'Vue', - type: 'vue', - description: 'https://vuejs.org', - }, -]; - -export const STARTER_TEMPLATES: StarterTemplate[] = [ - // Vue - { - name: 'tabs', - projectType: 'vue', - type: 'managed', - description: 'A starting project with a simple tabbed interface', - id: 'vue-vite-official-tabs', - }, - { - name: 'sidemenu', - projectType: 'vue', - type: 'managed', - description: 'A starting project with a side menu with navigation in the content area', - id: 'vue-vite-official-sidemenu', - }, - { - name: 'blank', - projectType: 'vue', - type: 'managed', - description: 'A blank starter project', - id: 'vue-vite-official-blank', - }, - { - name: 'list', - projectType: 'vue', - type: 'managed', - description: 'A starting project with a list', - id: 'vue-vite-official-list', - }, - // Angular - { - name: 'tabs', - projectType: 'angular', - type: 'managed', - description: 'A starting project with a simple tabbed interface', - id: 'angular-official-tabs', - }, - { - name: 'sidemenu', - projectType: 'angular', - type: 'managed', - description: 'A starting project with a side menu with navigation in the content area', - id: 'angular-official-sidemenu', - }, - { - name: 'blank', - projectType: 'angular', - type: 'managed', - description: 'A blank starter project', - id: 'angular-official-blank', - }, - { - name: 'list', - projectType: 'angular', - type: 'managed', - description: 'A starting project with a list', - id: 'angular-official-list', - }, - { - name: 'my-first-app', - projectType: 'angular', - type: 'repo', - description: 'A template for the "Build Your First App" tutorial', - repo: 'https://github.com/ionic-team/photo-gallery-capacitor-ng', - }, - { - name: 'tabs', - projectType: ANGULAR_STANDALONE, - type: 'managed', - description: 'A starting project with a simple tabbed interface', - id: 'angular-standalone-official-tabs', - }, - { - name: 'sidemenu', - projectType: ANGULAR_STANDALONE, - type: 'managed', - description: 'A starting project with a side menu with navigation in the content area', - id: 'angular-standalone-official-sidemenu', - }, - { - name: 'blank', - projectType: ANGULAR_STANDALONE, - type: 'managed', - description: 'A blank starter project', - id: 'angular-standalone-official-blank', - }, - { - name: 'list', - projectType: ANGULAR_STANDALONE, - type: 'managed', - description: 'A starting project with a list', - id: 'angular-standalone-official-list', - }, - { - name: 'my-first-app', - projectType: ANGULAR_STANDALONE, - type: 'repo', - description: 'A template for the "Build Your First App" tutorial', - repo: 'https://github.com/ionic-team/photo-gallery-capacitor-ng', - }, - // React - { - name: 'blank', - projectType: 'react', - type: 'managed', - description: 'A blank starter project', - id: 'react-vite-official-blank', - }, - { - name: 'list', - projectType: 'react', - type: 'managed', - description: 'A starting project with a list', - id: 'react-vite-official-list', - }, - { - name: 'my-first-app', - projectType: 'react', - type: 'repo', - description: 'A template for the "Build Your First App" tutorial', - repo: 'https://github.com/ionic-team/photo-gallery-capacitor-react', - }, - { - name: 'sidemenu', - projectType: 'react', - type: 'managed', - description: 'A starting project with a side menu with navigation in the content area', - id: 'react-vite-official-sidemenu', - }, - { - name: 'tabs', - projectType: 'react', - type: 'managed', - description: 'A starting project with a simple tabbed interface', - id: 'react-vite-official-tabs', - }, -]; diff --git a/packages/@ionic/cli/src/lib/telemetry.ts b/packages/@ionic/cli/src/lib/telemetry.ts deleted file mode 100644 index 3ae1703037..0000000000 --- a/packages/@ionic/cli/src/lib/telemetry.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { debug as Debug } from 'debug'; -import * as lodash from 'lodash'; - -import { IClient, IConfig, IProject, ISession, ITelemetry, InfoItem, IonicContext } from '../definitions'; - -import { sendMessage } from './helper'; -import { generateUUID } from './utils/uuid'; - -const debug = Debug('ionic:lib:telemetry'); -const GA_CODE = 'UA-44023830-30'; -let _gaTracker: import('leek') | undefined; - -export interface TelemetryDeps { - readonly client: IClient; - readonly config: IConfig; - readonly getInfo: () => Promise; - readonly ctx: IonicContext; - readonly project?: IProject; - readonly session: ISession; -} - -export class Telemetry implements ITelemetry { - protected readonly client: IClient; - protected readonly config: IConfig; - protected readonly getInfo: () => Promise; - protected readonly ctx: IonicContext; - protected readonly project?: IProject; - protected readonly session: ISession; - - constructor({ config, client, getInfo, ctx, project, session }: TelemetryDeps) { - this.client = client; - this.config = config; - this.getInfo = getInfo; - this.ctx = ctx; - this.project = project; - this.session = session; - } - - async sendCommand(command: string, args: string[]): Promise { - debug('Sending telemetry for command: %O %O', command, args); - await sendMessage({ config: this.config, ctx: this.ctx }, { type: 'telemetry', data: { command, args } }); - } -} - -async function getLeek({ config, version }: { config: IConfig; version: string; }): Promise { - if (!_gaTracker) { - const Leek = (await import('leek')).default; - let telemetryToken = config.get('tokens.telemetry'); - - if (!telemetryToken) { - telemetryToken = generateUUID(); - config.set('tokens.telemetry', telemetryToken); - debug(`setting telemetry token to ${telemetryToken}`); - } - - _gaTracker = new Leek({ - name: telemetryToken, - trackingCode: GA_CODE, - globalName: 'ionic', - version, - silent: !config.get('telemetry'), - }); - } - - return _gaTracker; -} - -export async function sendCommand({ config, client, getInfo, ctx, session, project }: TelemetryDeps, command: string, args: string[]) { - const messageList: string[] = []; - const name = 'command execution'; - const prettyArgs = args.map(a => a.includes(' ') ? `"${a}"` : a); - const message = messageList.concat([command], prettyArgs).join(' '); - - await Promise.all([ - (async () => { - const leek = await getLeek({ config, version: ctx.version }); - try { - await leek.track({ name, message }); - } catch (e: any) { - debug(`leek track error: ${e.stack ? e.stack : e}`); - } - })(), - (async () => { - const now = new Date().toISOString(); - const appflowId = project ? project.config.get('id') : undefined; - const info = await getInfo(); - const results = info.map(r => r.key ? { [r.key]: r.value } : undefined).filter(r => !!r); - const { req } = await client.make('POST', '/events/metrics'); - - const metric: { [key: string]: any; } = { - 'name': 'cli_command_metrics', - 'timestamp': now, - 'session_id': config.get('tokens.telemetry'), - 'source': 'cli', - 'value': { - 'command': command, - 'arguments': prettyArgs.join(' '), - 'app_id': appflowId, - 'backend': 'pro', // TODO: is this necessary? - ...lodash.extend({}, ...results), - }, - }; - - const isLoggedIn = session.isLoggedIn(); - - if (isLoggedIn) { - const token = await session.getUserToken(); - req.set('Authorization', `Bearer ${token}`); - } - - debug('metric: %o', metric); - - req.send({ - 'metrics': [metric], - 'sent_at': now, - }); - - try { - await client.do(req); - } catch (e: any) { - debug(`metric send error: ${e.stack ? e.stack : e}`); - } - })(), - ]); -} diff --git a/packages/@ionic/cli/src/lib/updates.ts b/packages/@ionic/cli/src/lib/updates.ts deleted file mode 100644 index 6d35334ce2..0000000000 --- a/packages/@ionic/cli/src/lib/updates.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { PackageJson } from '@ionic/cli-framework'; -import { readJson, writeJson } from '@ionic/utils-fs'; -import { stringWidth } from '@ionic/utils-terminal'; -import * as path from 'path'; -import * as semver from 'semver'; - -import { IConfig, IonicEnvironment } from '../definitions'; - -import { input, strong, success, weak, WARN } from './color'; -import { sendMessage } from './helper'; -import { pkgFromRegistry, pkgManagerArgs } from './utils/npm'; - -const UPDATE_CONFIG_FILE = 'update.json'; -const UPDATE_CHECK_INTERVAL = 60 * 60 * 24 * 1000; // 1 day -const UPDATE_NOTIFY_INTERVAL = 60 * 60 * 12 * 1000; // 12 hours -const PACKAGES = ['@ionic/cli', 'native-run', 'cordova-res']; - -export interface PersistedPackage { - name: string; - version: string; -} - -export interface UpdateConfig { - lastUpdate?: string; - lastNotify?: string; - packages: PersistedPackage[]; -} - -export async function readUpdateConfig(dir: string): Promise { - return readJson(path.resolve(dir, UPDATE_CONFIG_FILE)); -} - -export async function writeUpdateConfig(dir: string, config: UpdateConfig): Promise { - await writeJson(path.resolve(dir, UPDATE_CONFIG_FILE), config, { spaces: 2 }); -} - -export interface GetUpdateConfigDeps { - readonly config: IConfig; -} - -export async function getUpdateConfig({ config }: GetUpdateConfigDeps): Promise { - const dir = path.dirname(config.p); - - try { - return await readUpdateConfig(dir); - } catch (e: any) { - if (e.code !== 'ENOENT') { - process.stderr.write(`${e.stack ? e.stack : e}\n`); - } - - return { packages: [] }; - } -} - -export interface PersistPackageVersionsDeps { - readonly config: IConfig; -} - -export async function runUpdateCheck({ config }: PersistPackageVersionsDeps): Promise { - const dir = path.dirname(config.p); - - const pkgs: readonly PackageJson[] = - (await Promise.all(PACKAGES.map(pkg => pkgFromRegistry(config.get('npmClient'), { pkg })))) - .filter((pkg): pkg is PackageJson => typeof pkg !== 'undefined'); - - const updateConfig = await getUpdateConfig({ config }); - - const newUpdateConfig: UpdateConfig = { - ...updateConfig, - lastUpdate: new Date().toISOString(), - packages: pkgs.map(pkg => ({ - name: pkg.name, - version: pkg.version, - })), - }; - - await writeUpdateConfig(dir, newUpdateConfig); -} - -export const IONIC_CLOUD_CLI_MIGRATION = (() => -`${strong(WARN('Deprecated: Ionic Appflow functionality has moved to the new Ionic Cloud CLI'))}.\n`+ -`Existing functionality in the Ionic CLI is deprecated as of ${WARN('v6.18.0')}. `+ -`This functionality will be removed from the Ionic CLI in the next major version. `+ -`Please visit our simple guide to migrate to the Ionic Cloud CLI, available now.\n`+ -`${strong('https://ionic.io/docs/appflow/cli/migration/')}\n`)(); - -export async function runNotify(env: IonicEnvironment, pkg: PersistedPackage, latestVersion: string): Promise { - const dir = path.dirname(env.config.p); - const args = await pkgManagerArgs(env.config.get('npmClient'), { command: 'install', pkg: pkg.name, global: true }); - const lines = [ - `Ionic CLI update available: ${weak(pkg.version)} → ${success(latestVersion)}`, - `Run ${input(args.join(' '))} to update`, - ]; - - // TODO: Pull this into utils/format - - const padding = 3; - const longestLineLength = Math.max(...lines.map(line => stringWidth(line))); - const horizontalRule = ` ${'─'.repeat(longestLineLength + padding * 2)}`; - const output = ( - `\n${horizontalRule}\n\n` + - `${lines.map(line => ` ${' '.repeat((longestLineLength - stringWidth(line)) / 2 + padding)}${line}`).join('\n')}\n\n` + - `${horizontalRule}\n\n` - ); - - process.stderr.write(output); - - const updateConfig = await getUpdateConfig(env); - updateConfig.lastNotify = new Date().toISOString(); - await writeUpdateConfig(dir, updateConfig); -} - -export async function runUpdateNotify(env: IonicEnvironment, pkg: PackageJson): Promise { - const { name, version } = pkg; - const { lastUpdate, lastNotify, packages } = await getUpdateConfig(env); - const latestPkg = packages.find(pkg => pkg.name === name); - const latestVersion = latestPkg ? latestPkg.version : undefined; - - if ((!lastNotify || new Date(lastNotify).getTime() + UPDATE_NOTIFY_INTERVAL < new Date().getTime()) && latestVersion && semver.gt(latestVersion, version)) { - await runNotify(env, pkg, latestVersion); - } - - if (!lastUpdate || new Date(lastUpdate).getTime() + UPDATE_CHECK_INTERVAL < new Date().getTime()) { - await sendMessage(env, { type: 'update-check' }); - } -} diff --git a/packages/@ionic/cli/src/lib/user.ts b/packages/@ionic/cli/src/lib/user.ts deleted file mode 100644 index 861667b5c1..0000000000 --- a/packages/@ionic/cli/src/lib/user.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { GithubBranch, GithubRepo, IClient, IPaginator, ResourceClientLoad, ResourceClientRequestModifiers, Response, TokenPaginatorState, User } from '../definitions'; -import { isGithubBranchListResponse, isGithubRepoListResponse, isOAuthLoginResponse, isUserResponse } from '../guards'; - -import { ResourceClient, TokenPaginator, createFatalAPIFormat } from './http'; - -export interface UserClientDeps { - readonly client: IClient; -} - -export class UserClient extends ResourceClient implements ResourceClientLoad { - constructor(readonly token: string, readonly e: UserClientDeps) { - super(); - } - - async load(id: number, modifiers?: ResourceClientRequestModifiers): Promise { - const { req } = await this.e.client.make('GET', `/users/${id}`); - this.applyAuthentication(req, this.token); - this.applyModifiers(req, modifiers); - const res = await this.e.client.do(req); - - if (!isUserResponse(res)) { - throw createFatalAPIFormat(req, res); - } - - return res.data; - } - - async loadSelf(): Promise { - const { req } = await this.e.client.make('GET', '/users/self'); - this.applyAuthentication(req, this.token); - const res = await this.e.client.do(req); - - if (!isUserResponse(res)) { - throw createFatalAPIFormat(req, res); - } - - return res.data; - } - - async oAuthGithubLogin(id: number): Promise { - const { req } = await this.e.client.make('POST', `/users/${id}/oauth/github`); - this.applyAuthentication(req, this.token); - req.send({ source: 'cli' }); - - const res = await this.e.client.do(req); - - if (!isOAuthLoginResponse(res)) { - throw createFatalAPIFormat(req, res); - } - - return res.data.redirect_url; - } - - paginateGithubRepositories(id: number): IPaginator, TokenPaginatorState> { - return new TokenPaginator({ - client: this.e.client, - reqgen: async () => { - const { req } = await this.e.client.make('GET', `/users/${id}/oauth/github/repositories`); - req.set('Authorization', `Bearer ${this.token}`); - return { req }; - }, - guard: isGithubRepoListResponse, - }); - } - - paginateGithubBranches(userId: number, repoId: number): IPaginator, TokenPaginatorState> { - return new TokenPaginator({ - client: this.e.client, - reqgen: async () => { - const { req } = await this.e.client.make('GET', `/users/${userId}/oauth/github/repositories/${repoId}/branches`); - req.set('Authorization', `Bearer ${this.token}`); - return { req }; - }, - guard: isGithubBranchListResponse, - }); - } -} diff --git a/packages/@ionic/cli/src/lib/utils/__tests__/npm.ts b/packages/@ionic/cli/src/lib/utils/__tests__/npm.ts deleted file mode 100644 index b031a91c93..0000000000 --- a/packages/@ionic/cli/src/lib/utils/__tests__/npm.ts +++ /dev/null @@ -1,247 +0,0 @@ -describe('@ionic/cli', () => { - - describe('lib/utils/npm', () => { - - describe('pkgManagerArgs', () => { - - jest.resetModules(); - const { pkgManagerArgs } = require('../npm'); - - it('should be pkg install with default args', async () => { - const result = await pkgManagerArgs('npm', { command: 'install' }); - expect(result).toEqual(['npm', 'i']); - }); - - it('should be pkg install args for local package', async () => { - const result = await pkgManagerArgs('npm', { command: 'install', pkg: 'foo' }); - expect(result).toEqual(['npm', 'i', '--save', '-E', 'foo']); - }); - - it('should be pkg install args for local package install and save dev', async () => { - const result = await pkgManagerArgs('npm', { command: 'install', pkg: 'foo', saveDev: true }); - expect(result).toEqual(['npm', 'i', '-D', '-E', 'foo']); - }); - - it('should be pkg install args for local package uninstall', async () => { - const result = await pkgManagerArgs('npm', { command: 'uninstall', pkg: 'foo', saveDev: true }); - expect(result).toEqual(['npm', 'uninstall', '-D', 'foo']); - }); - - it('should be pkg install args for global package install', async () => { - const result = await pkgManagerArgs('npm', { command: 'install', pkg: 'foo', global: true }); - expect(result).toEqual(['npm', 'i', '-g', 'foo']); - }); - - it('should be pkg install args for global package install with bad options', async () => { - const result = await pkgManagerArgs('npm', { command: 'install', pkg: 'foo', global: true, saveDev: true }); - expect(result).toEqual(['npm', 'i', '-g', 'foo']); - }); - - it('should be dedupe args', async () => { - const result = await pkgManagerArgs('npm', { command: 'dedupe' }); - expect(result).toEqual(['npm', 'dedupe']); - }); - - it('should be rebuild args', async () => { - const result = await pkgManagerArgs('npm', { command: 'rebuild' }); - expect(result).toEqual(['npm', 'rebuild']); - }); - - it('should be bare run args', async () => { - const result = await pkgManagerArgs('npm', { command: 'run' }); - expect(result).toEqual(['npm', 'run']); - }); - - it('should be run args with script', async () => { - const result = await pkgManagerArgs('npm', { command: 'run', script: 'test' }); - expect(result).toEqual(['npm', 'run', 'test']); - }); - - it('should be run args with script and script args', async () => { - const result = await pkgManagerArgs('npm', { command: 'run', script: 'test', scriptArgs: ['-s'] }); - expect(result).toEqual(['npm', 'run', 'test', '--', '-s']); - }); - - it('should be info args', async () => { - const result = await pkgManagerArgs('npm', { command: 'info' }); - expect(result).toEqual(['npm', 'info']); - }); - - it('should be pkg info args', async () => { - const result = await pkgManagerArgs('npm', { command: 'info', pkg: '@ionic/cli' }); - expect(result).toEqual(['npm', 'info', '@ionic/cli']); - }); - - it('should be pkg info args with json flag', async () => { - const result = await pkgManagerArgs('npm', { command: 'info', pkg: '@ionic/cli', json: true }); - expect(result).toEqual(['npm', 'info', '@ionic/cli', '--json']); - }); - - it('should include lockFileOnly flag', async () => { - const result = await pkgManagerArgs('npm', { command: 'install', lockFileOnly: true }); - expect(result).toEqual(['npm', 'i', '--package-lock-only']); - }); - - describe('yarn', () => { - - jest.resetModules(); - const { pkgManagerArgs } = require('../npm'); - - it('should be pkg install with default args', async () => { - const result = await pkgManagerArgs('yarn', { command: 'install' }); - expect(result).toEqual(['yarn', 'install', '--non-interactive']); - }); - - it('should be pkg install args for local package', async () => { - const result = await pkgManagerArgs('yarn', { command: 'install', pkg: 'foo' }); - expect(result).toEqual(['yarn', 'add', '--exact', '--non-interactive', 'foo']); - }); - - it('should be pkg install args for local package install', async () => { - const result = await pkgManagerArgs('yarn', { command: 'install', pkg: 'foo', saveDev: true }); - expect(result).toEqual(['yarn', 'add', '--dev', '--exact', '--non-interactive', 'foo']); - }); - - it('should be pkg install args for local package uninstall', async () => { - const result = await pkgManagerArgs('yarn', { command: 'uninstall', pkg: 'foo', saveDev: true }); - expect(result).toEqual(['yarn', 'remove', '--dev', '--non-interactive', 'foo']); - }); - - it('should be pkg install args for global package install', async () => { - const result = await pkgManagerArgs('yarn', { command: 'install', pkg: 'foo', global: true }); - expect(result).toEqual(['yarn', 'global', 'add', '--non-interactive', 'foo']); - }); - - it('should be pkg install args for global package install with bad options', async () => { - const result = await pkgManagerArgs('yarn', { command: 'install', pkg: 'foo', global: true, saveDev: true }); - expect(result).toEqual(['yarn', 'global', 'add', '--non-interactive', 'foo']); - }); - - it('should be dedupe args', async () => { - const result = await pkgManagerArgs('yarn', { command: 'dedupe' }); - expect(result).toEqual([]); // yarn doesn't support dedupe - }); - - it('should be rebuild args', async () => { - const result = await pkgManagerArgs('yarn', { command: 'rebuild' }); - expect(result).toEqual(['yarn', 'install', '--non-interactive', '--force']); - }); - - it('should be run args', async () => { - const result = await pkgManagerArgs('yarn', { command: 'run' }); - expect(result).toEqual(['yarn', 'run', '--non-interactive']); - }); - - it('should be run args with script', async () => { - const result = await pkgManagerArgs('yarn', { command: 'run', script: 'test' }); - expect(result).toEqual(['yarn', 'run', '--non-interactive', 'test']); - }); - - it('should be run args with script and script args', async () => { - const result = await pkgManagerArgs('yarn', { command: 'run', script: 'test', scriptArgs: ['-s'] }); - expect(result).toEqual(['yarn', 'run', '--non-interactive', 'test', '-s']); - }); - - it('should be info args', async () => { - const result = await pkgManagerArgs('yarn', { command: 'info' }); - expect(result).toEqual(['yarn', 'info', '--non-interactive']); - }); - - it('should be pkg info args', async () => { - const result = await pkgManagerArgs('yarn', { command: 'info', pkg: '@ionic/cli' }); - expect(result).toEqual(['yarn', 'info', '--non-interactive', '@ionic/cli']); - }); - - it('should be pkg info args with json flag', async () => { - const result = await pkgManagerArgs('yarn', { command: 'info', pkg: '@ionic/cli', json: true }); - expect(result).toEqual(['yarn', 'info', '--non-interactive', '@ionic/cli', '--json']); - }); - - }); - - describe('pnpm', () => { - - jest.resetModules(); - const { pkgManagerArgs } = require('../npm'); - - it('should be pkg install with default args', async () => { - const result = await pkgManagerArgs('pnpm', { command: 'install' }); - expect(result).toEqual(['pnpm', 'install']); - }); - - it('should be pkg install args for local package', async () => { - const result = await pkgManagerArgs('pnpm', { command: 'install', pkg: 'foo' }); - expect(result).toEqual(['pnpm', 'add', '--save-exact', 'foo']); - }); - - it('should be pkg install args for local package install', async () => { - const result = await pkgManagerArgs('pnpm', { command: 'install', pkg: 'foo', saveDev: true }); - expect(result).toEqual(['pnpm', 'add', '--save-dev', '--save-exact', 'foo']); - }); - - it('should be pkg install args for local package uninstall', async () => { - const result = await pkgManagerArgs('pnpm', { command: 'uninstall', pkg: 'foo' }); - expect(result).toEqual(['pnpm', 'remove', 'foo']); - }); - - it('should be pkg install args for global package install', async () => { - const result = await pkgManagerArgs('pnpm', { command: 'install', pkg: 'foo', global: true }); - expect(result).toEqual(['pnpm', 'add', '--global', 'foo']); - }); - - it('should be pkg install args for global package install with bad options', async () => { - const result = await pkgManagerArgs('pnpm', { command: 'install', pkg: 'foo', global: true, saveDev: true }); - expect(result).toEqual(['pnpm', 'add', '--global', 'foo']); - }); - - it('should be dedupe args', async () => { - const result = await pkgManagerArgs('pnpm', { command: 'dedupe' }); - expect(result).toEqual([]); // pnpm doesn't support dedupe - }); - - it('should be rebuild args', async () => { - const result = await pkgManagerArgs('pnpm', { command: 'rebuild' }); - expect(result).toEqual(['pnpm', 'rebuild']); - }); - - it('should be run args', async () => { - const result = await pkgManagerArgs('pnpm', { command: 'run' }); - expect(result).toEqual(['pnpm', 'run']); - }); - - it('should be run args with script', async () => { - const result = await pkgManagerArgs('pnpm', { command: 'run', script: 'test' }); - expect(result).toEqual(['pnpm', 'run', 'test']); - }); - - it('should be run args with script and script args', async () => { - const result = await pkgManagerArgs('pnpm', { command: 'run', script: 'test', scriptArgs: ['-s'] }); - expect(result).toEqual(['pnpm', 'run', 'test', '--', '-s']); - }); - - it('should be info args', async () => { - const result = await pkgManagerArgs('pnpm', { command: 'info' }); - expect(result).toEqual(['pnpm', 'info']); - }); - - it('should be pkg info args', async () => { - const result = await pkgManagerArgs('pnpm', { command: 'info', pkg: '@ionic/cli' }); - expect(result).toEqual(['pnpm', 'info', '@ionic/cli']); - }); - - it('should be pkg info args with json flag', async () => { - const result = await pkgManagerArgs('pnpm', { command: 'info', pkg: '@ionic/cli', json: true }); - expect(result).toEqual(['pnpm', 'info', '@ionic/cli', '--json']); - }); - - it('should include lockFileOnly flag', async () => { - const result = await pkgManagerArgs('pnpm', { command: 'install', lockFileOnly: true }); - expect(result).toEqual(['pnpm', 'install', '--lockfile-only']); - }); - }); - - }); - - }); - -}); diff --git a/packages/@ionic/cli/src/lib/utils/__tests__/uuid.ts b/packages/@ionic/cli/src/lib/utils/__tests__/uuid.ts deleted file mode 100644 index 4790ce3478..0000000000 --- a/packages/@ionic/cli/src/lib/utils/__tests__/uuid.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { generateUUID } from '../uuid'; - -describe('@ionic/cli', () => { - - describe('lib/utils/uuid', () => { - - describe('generateUUID', () => { - - it('should be of standard format', () => { - const id = generateUUID(); - expect(id).toEqual(expect.stringMatching(/^[a-z0-9]{8}(-[a-z0-9]{4}){3}-[a-z0-9]{12}$/)); - }); - - }); - - }); - -}); diff --git a/packages/@ionic/cli/src/lib/utils/archive.ts b/packages/@ionic/cli/src/lib/utils/archive.ts deleted file mode 100644 index ef286d1a44..0000000000 --- a/packages/@ionic/cli/src/lib/utils/archive.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as tar from 'tar'; - -export { tar }; diff --git a/packages/@ionic/cli/src/lib/utils/color.ts b/packages/@ionic/cli/src/lib/utils/color.ts deleted file mode 100644 index 1b11614738..0000000000 --- a/packages/@ionic/cli/src/lib/utils/color.ts +++ /dev/null @@ -1,194 +0,0 @@ -export interface RGB { - b: number; - g: number; - r: number; -} - -export interface HSL { - h: number; - l: number; - s: number; -} - -function componentToHex(c: number) { - const hex = c.toString(16); - return hex.length === 1 ? `0${hex}` : hex; -} - -function expandHex(hex: string): string { - const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; - hex = hex.replace(shorthandRegex, (_m, r, g, b) => { - return r + r + g + g + b + b; - }); - - return `#${hex.replace('#', '')}`; -} - -function hexToRGB(hex: string): RGB { - hex = expandHex(hex); - hex = hex.replace('#', ''); - const intValue: number = parseInt(hex, 16); - - return { - r: (intValue >> 16) & 255, - g: (intValue >> 8) & 255, - b: intValue & 255, - }; -} - -function hslToRGB({ h, s, l }: HSL): RGB { - h = h / 360; - s = s / 100; - l = l / 100; - if (s === 0) { - l = Math.round(l * 255); - return { - r: l, - g: l, - b: l, - }; - } - - const hue2rgb = (p: number, q: number, t: number) => { - if (t < 0) { t += 1; } - if (t > 1) { t -= 1; } - if (t < 1 / 6) { return p + (q - p) * 6 * t; } - if (t < 1 / 2) { return q; } - if (t < 2 / 3) { return p + (q - p) * (2 / 3 - t) * 6; } - return p; - }; - - const q = l < 0.5 ? l * (1 + s) : l + s - l * s; - const p = 2 * l - q; - const r = hue2rgb(p, q, h + (1 / 3)); - const g = hue2rgb(p, q, h); - const b = hue2rgb(p, q, h - (1 / 3)); - - return { - r: Math.round(r * 255), - g: Math.round(g * 255), - b: Math.round(b * 255), - }; -} - -function mixColors(color: Color, mixColor: Color, weight = .5): RGB { - const colorRGB: RGB = color.rgb; - const mixColorRGB: RGB = mixColor.rgb; - const mixColorWeight = 1 - weight; - - return { - r: Math.round(weight * mixColorRGB.r + mixColorWeight * colorRGB.r), - g: Math.round(weight * mixColorRGB.g + mixColorWeight * colorRGB.g), - b: Math.round(weight * mixColorRGB.b + mixColorWeight * colorRGB.b), - }; -} - -function rgbToHex({ r, g, b }: RGB) { - return '#' + componentToHex(r) + componentToHex(g) + componentToHex(b); -} - -function rgbToHSL({ r, g, b }: RGB): HSL { - r = Math.max(Math.min(r / 255, 1), 0); - g = Math.max(Math.min(g / 255, 1), 0); - b = Math.max(Math.min(b / 255, 1), 0); - const max = Math.max(r, g, b); - const min = Math.min(r, g, b); - const l = Math.min(1, Math.max(0, (max + min) / 2)); - let d; - let h; - let s; - - if (max !== min) { - d = max - min; - s = l > 0.5 ? d / (2 - max - min) : d / (max + min); - if (max === r) { - h = (g - b) / d + (g < b ? 6 : 0); - } else if (max === g) { - h = (b - r) / d + 2; - } else { - h = (r - g) / d + 4; - } - h = h / 6; - } else { - h = s = 0; - } - return { - h: Math.round(h * 360), - s: Math.round(s * 100), - l: Math.round(l * 100), - }; -} - -function rgbToYIQ({ r, g, b }: RGB): number { - return ((r * 299) + (g * 587) + (b * 114)) / 1000; -} - -export class Color { - readonly hex: string; - readonly hsl: HSL; - readonly rgb: RGB; - readonly yiq: number; - - constructor(value: string | RGB | HSL) { - if (typeof(value) === 'string' && /rgb\(/.test(value)) { - const matches = /rgb\((\d{1,3}), ?(\d{1,3}), ?(\d{1,3})\)/.exec(value); - if (matches) { - value = { r: parseInt(matches[0], 10), g: parseInt(matches[1], 10), b: parseInt(matches[2], 10) }; - } - } else if (typeof(value) === 'string' && /hsl\(/.test(value)) { - const matches = /hsl\((\d{1,3}), ?(\d{1,3}%), ?(\d{1,3}%)\)/.exec(value); - if (matches) { - value = { h: parseInt(matches[0], 10), s: parseInt(matches[1], 10), l: parseInt(matches[2], 10) }; - } - } - - if (typeof(value) === 'string') { - value = value.replace(/\s/g, ''); - this.hex = expandHex(value); - this.rgb = hexToRGB(this.hex); - this.hsl = rgbToHSL(this.rgb); - } else if ('r' in value && 'g' in value && 'b' in value) { - this.rgb = value; - this.hex = rgbToHex(this.rgb); - this.hsl = rgbToHSL(this.rgb); - } else if ('h' in value && 's' in value && 'l' in value) { - this.hsl = value; - this.rgb = hslToRGB(this.hsl); - this.hex = rgbToHex(this.rgb); - } else { - throw new Error('Invalid color'); - } - - this.yiq = rgbToYIQ(this.rgb); - } - - static isColor(value: string): boolean { - if (/rgb\((\d{1,3}), ?(\d{1,3}), ?(\d{1,3})\)/.test(value)) { - return true; - } - - return /(^#[0-9a-fA-F]+)/.test(value.trim()); - } - - contrast = (threshold = 128): Color => { - return new Color((this.yiq && this.yiq >= threshold ? '#000' : '#fff')); - } - - mix = (from: string | RGB | HSL | Color, amount = .5): Color => { - const base: Color = from instanceof Color ? from : new Color(from); - return new Color(mixColors(this, base, amount)); - } - - shade = (weight = .12): Color => { - return this.mix({ r: 0, g: 0, b: 0 }, weight); - } - - tint = (weight = .1): Color => { - return this.mix({ r: 255, g: 255, b: 255 }, weight); - } - - toList(): string { - const { r, g, b } = this.rgb; - return `${r},${g},${b}`; - } -} diff --git a/packages/@ionic/cli/src/lib/utils/emoji.ts b/packages/@ionic/cli/src/lib/utils/emoji.ts deleted file mode 100644 index 44a4ed4954..0000000000 --- a/packages/@ionic/cli/src/lib/utils/emoji.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { TERMINAL_INFO } from '@ionic/utils-terminal'; - -export function emoji(x: string, fallback: string): string { - if (TERMINAL_INFO.windows) { - return fallback; - } - - return x; -} diff --git a/packages/@ionic/cli/src/lib/utils/file.ts b/packages/@ionic/cli/src/lib/utils/file.ts deleted file mode 100644 index a98c1ffc68..0000000000 --- a/packages/@ionic/cli/src/lib/utils/file.ts +++ /dev/null @@ -1,22 +0,0 @@ -class FileUtils { - private filenameReservedRegex = (/[<>:"\/\\|?*\x00-\x1F]/g); - private filenameReservedRegexWindows = (/^(con|prn|aux|nul|com[0-9]|lpt[0-9])$/i); - - isValidFileName(fileName: any): boolean { - if (!fileName || fileName.length > 255) { - return false; - } - - if (this.filenameReservedRegex.test(fileName) || this.filenameReservedRegexWindows.test(fileName)) { - return false; - } - - if (/^\.\.?$/.test(fileName)) { - return false; - } - - return true; - } -} - -export const fileUtils = new FileUtils(); diff --git a/packages/@ionic/cli/src/lib/utils/http.ts b/packages/@ionic/cli/src/lib/utils/http.ts deleted file mode 100644 index 2498d5f7e5..0000000000 --- a/packages/@ionic/cli/src/lib/utils/http.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { conform } from '@ionic/utils-array'; -import { readFile } from '@ionic/utils-fs'; -import { debug as Debug } from 'debug'; - -import superagentProxy from './superagent-proxy'; - -import { CreateRequestOptions, HttpMethod } from '../../definitions'; - -export type SuperAgentRequest = import('superagent').SuperAgentRequest; - -const debug = Debug('ionic:lib:utils:http'); - -export const PROXY_ENVIRONMENT_VARIABLES: readonly string[] = ['IONIC_HTTP_PROXY', 'HTTPS_PROXY', 'HTTP_PROXY', 'PROXY', 'https_proxy', 'http_proxy', 'proxy']; - -function getGlobalProxy(): { envvar: string; envval: string; } | undefined { - for (const envvar of PROXY_ENVIRONMENT_VARIABLES) { - const envval = process.env[envvar]; - - if (envval) { - return { envval, envvar }; - } - } -} - -export async function createRequest(method: HttpMethod, url: string, { userAgent, proxy, ssl }: CreateRequestOptions): Promise<{ req: SuperAgentRequest; }> { - const superagent = (await import('superagent')).default; - - if (!proxy) { - const gproxy = getGlobalProxy(); - - if (gproxy) { - proxy = gproxy.envval; - } - } - - const req = superagent(method, url) as SuperAgentRequest & { proxy?: (uri: string) => void }; - - req - .set('User-Agent', userAgent) - .redirects(25); - - if (proxy) { - superagentProxy(superagent); - - if (req.proxy) { - req.proxy(proxy); - } else { - debug(`Cannot install proxy--req.proxy not defined`); - } - } - - if (ssl) { - const cafiles = conform(ssl.cafile); - const certfiles = conform(ssl.certfile); - const keyfiles = conform(ssl.keyfile); - - if (cafiles.length > 0) { - req.ca(await Promise.all(cafiles.map(p => readFile(p, { encoding: 'utf8' })))); - } - - if (certfiles.length > 0) { - req.cert(await Promise.all(certfiles.map(p => readFile(p, { encoding: 'utf8' })))); - } - - if (keyfiles.length > 0) { - req.key(await Promise.all(keyfiles.map(p => readFile(p, { encoding: 'utf8' })))); - } - } - - return { req }; -} - -/** - * Initiate a request, downloading the contents to a writable stream. - * - * @param req The request to download to the writable stream. - * @param ws Must be a dedicated writable stream that calls the 'close' event. - */ -export async function download(req: SuperAgentRequest, ws: NodeJS.WritableStream, { progress }: { progress?: (loaded: number, total: number) => void; }): Promise { - return new Promise((resolve, reject) => { - req - .on('response', res => { - if (res.status !== 200) { - reject(new Error( - `Encountered bad status code (${res.status}) for ${req.url}\n` + - `This could mean the server is experiencing difficulties right now--please try again later.` - )); - } - - if (progress) { - let loaded = 0; - const total = Number(res.header['content-length']); - res.on('data', chunk => { - loaded += chunk.length; - progress(loaded, total); - }); - } - }) - .on('error', err => { - if (err.code === 'ECONNABORTED') { - reject(new Error(`Timeout of ${err.timeout}ms reached for ${req.url}`)); - } else { - reject(err); - } - }); - - ws.on('close', resolve); - - req.pipe(ws); - }); -} diff --git a/packages/@ionic/cli/src/lib/utils/logger.ts b/packages/@ionic/cli/src/lib/utils/logger.ts deleted file mode 100644 index 0fae1f193f..0000000000 --- a/packages/@ionic/cli/src/lib/utils/logger.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { DEFAULT_COLORS } from '@ionic/cli-framework'; -import { CreateTaggedFormatterOptions, DEFAULT_LOGGER_HANDLERS, LOGGER_LEVELS, Logger as BaseLogger, LoggerFormatter, LoggerLevelWeight, createPrefixedFormatter, createTaggedFormatter } from '@ionic/cli-framework-output'; -import chalk from 'chalk'; - -import { ILogger } from '../../definitions'; -import { weak } from '../color'; - -export class Logger extends BaseLogger implements ILogger { - ok(msg: string): void { - this.log({ ...this.createRecord(`${weak('[')}${chalk.bold.green('OK')}${weak(']')} ${msg}`), format: false }); - } - - rawmsg(msg: string): void { - this.log({ ...this.createRecord(msg), format: false }); - } -} - -export function createFormatter(options: CreateTaggedFormatterOptions = {}): LoggerFormatter { - const prefix = process.argv.includes('--log-timestamps') ? () => `${weak('[' + new Date().toISOString() + ']')}` : ''; - - return createTaggedFormatter({ colors: DEFAULT_COLORS, prefix, titleize: true, wrap: true, ...options }); -} - -export function createDefaultLoggerHandlers(formatter = createFormatter()) { - return new Set([...DEFAULT_LOGGER_HANDLERS].map(handler => handler.clone({ formatter }))); -} - -export function createPrefixedWriteStream(log: ILogger, prefix: string, level: LoggerLevelWeight = LOGGER_LEVELS.INFO): NodeJS.WritableStream { - const l = log.clone(); - l.handlers = createDefaultLoggerHandlers(createPrefixedFormatter(prefix)); - - return l.createWriteStream(level); -} diff --git a/packages/@ionic/cli/src/lib/utils/npm.ts b/packages/@ionic/cli/src/lib/utils/npm.ts deleted file mode 100644 index c717b1924d..0000000000 --- a/packages/@ionic/cli/src/lib/utils/npm.ts +++ /dev/null @@ -1,190 +0,0 @@ -import { PackageJson } from '@ionic/cli-framework'; -import { Subprocess } from '@ionic/utils-subprocess'; - -import { NpmClient } from '../../definitions'; - -interface PkgManagerVocabulary { - // commands - install: string; - bareInstall: string; - uninstall: string; - run: string; - dedupe: string; - rebuild: string; - - // flags - global: string; - save: string; - saveDev: string; - saveExact: string; - nonInteractive: string; - lockFileOnly: string; -} - -export type PkgManagerCommand = 'dedupe' | 'rebuild' | 'install' | 'uninstall' | 'run' | 'info'; - -export interface PkgManagerOptions { - command: PkgManagerCommand; - pkg?: string | string[]; - script?: string; - scriptArgs?: string[]; - global?: boolean; - save?: boolean; - saveDev?: boolean; - saveExact?: boolean; - json?: boolean; - lockFileOnly?: boolean; -} - -/** - * Resolves pkg manager intent with command args. - * - * TODO: this is a weird function and should be split up - * - * @return Promise If the args is an empty array, it means the pkg manager doesn't have that command. - */ -export async function pkgManagerArgs(npmClient: NpmClient, options: PkgManagerOptions): Promise { - let vocab: PkgManagerVocabulary; - - const cmd = options.command; - - if (cmd === 'dedupe') { - delete options.pkg; - } - - if (cmd === 'dedupe' || cmd === 'rebuild') { - delete options.global; - delete options.save; - delete options.saveDev; - } - - if (cmd === 'dedupe' || cmd === 'rebuild' || cmd === 'uninstall') { - delete options.saveExact; - } - - if (cmd === 'install' || cmd === 'uninstall') { - if (options.global) { // Turn off all save flags for global context - options.save = false; - options.saveDev = false; - options.saveExact = false; - } else if (options.pkg && typeof options.save === 'undefined' && typeof options.saveDev === 'undefined') { // Prefer save flag - options.save = true; - } - - if (cmd === 'install' && options.pkg && typeof options.saveExact === 'undefined') { // For single package installs, prefer to save exact versions - options.saveExact = true; - } - } - - const installerArgs: string[] = []; - - switch (npmClient) { - case 'npm': - vocab = { run: 'run', install: 'i', bareInstall: 'i', uninstall: 'uninstall', dedupe: 'dedupe', rebuild: 'rebuild', global: '-g', save: '--save', saveDev: '-D', saveExact: '-E', nonInteractive: '', lockFileOnly: '--package-lock-only' }; - break; - case 'yarn': - vocab = { run: 'run', install: 'add', bareInstall: 'install', uninstall: 'remove', dedupe: '', rebuild: 'install', global: '', save: '', saveDev: '--dev', saveExact: '--exact', nonInteractive: '--non-interactive', lockFileOnly: '' }; - if (options.global) { // yarn installs packages globally under the 'global' prefix, instead of having a flag - installerArgs.push('global'); - } - break; - case 'pnpm': - vocab = { run: 'run', install: 'add', bareInstall: 'install', uninstall: 'remove', dedupe: '', rebuild: 'rebuild', global: '--global', save: '', saveDev: '--save-dev', saveExact: '--save-exact', nonInteractive: '', lockFileOnly: '--lockfile-only' }; - break; - default: - throw new Error(`unknown installer: ${npmClient}`); - } - - if (cmd === 'install') { - if (options.pkg) { - installerArgs.push(vocab.install); - } else { - installerArgs.push(vocab.bareInstall); - } - if (options.lockFileOnly) { - installerArgs.push(vocab.lockFileOnly) - } - } else if (cmd === 'uninstall') { - installerArgs.push(vocab.uninstall); - } else if (cmd === 'dedupe') { - if (vocab.dedupe) { - installerArgs.push(vocab.dedupe); - } else { - return []; - } - } else if (cmd === 'rebuild') { - installerArgs.push(vocab.rebuild); - } else { - installerArgs.push(cmd); - } - - if (options.global && vocab.global) { - installerArgs.push(vocab.global); - } - - if (options.save && vocab.save) { - installerArgs.push(vocab.save); - } - - if (options.saveDev && vocab.saveDev) { - installerArgs.push(vocab.saveDev); - } - - if (options.saveExact && vocab.saveExact) { - installerArgs.push(vocab.saveExact); - } - - if (vocab.nonInteractive) { // Some CLIs offer a flag that disables all interactivity, which we want to opt-into - installerArgs.push(vocab.nonInteractive); - } - - if (options.pkg) { - if (typeof options.pkg === 'string'){ - installerArgs.push(options.pkg); - } - if (Array.isArray(options.pkg)){ - installerArgs.push(...options.pkg) - } - } - - if (cmd === 'run' && options.script) { - installerArgs.push(options.script); - } - - if (npmClient === 'yarn') { - if (cmd === 'rebuild') { - installerArgs.push('--force'); - } - } - - if (cmd === 'run' && options.script && options.scriptArgs && options.scriptArgs.length > 0) { - if (npmClient === 'npm' || npmClient === 'pnpm') { - installerArgs.push('--'); - } - - for (const arg of options.scriptArgs) { - installerArgs.push(arg); - } - } - - if (options.json) { - installerArgs.push('--json'); - } - - return [npmClient, ...installerArgs]; -} - -/** - * @return Promise - */ -export async function pkgFromRegistry(npmClient: NpmClient, options: Partial): Promise { - const [ manager, ...managerArgs ] = await pkgManagerArgs(npmClient, { command: 'info', json: true, ...options }); - - const cmd = new Subprocess(manager, managerArgs); - const result = await cmd.output(); - - if (result) { - const json = JSON.parse(result); - return manager === 'yarn' ? json.data : json; - } -} diff --git a/packages/@ionic/cli/src/lib/utils/superagent-proxy.ts b/packages/@ionic/cli/src/lib/utils/superagent-proxy.ts deleted file mode 100644 index d2aa6fe691..0000000000 --- a/packages/@ionic/cli/src/lib/utils/superagent-proxy.ts +++ /dev/null @@ -1,131 +0,0 @@ -/** - * (The MIT License) - * - * Copyright (c) 2013 Nathan Rajlich - * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - * THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - * - * This is a fork of the original source, upgraded to proxy-agent v6 and TypeScript. - * Original source: https://github.com/TooTallNate/superagent-proxy/blob/ef2cc112926b574547cf2f765ae3dfd80bdeb9a0/index.js - * - */ -import { ProxyAgent } from 'proxy-agent'; -import { debug as Debug } from 'debug'; -import { SuperAgent, SuperAgentRequest } from 'superagent'; - -const debug = Debug('superagent-proxy'); - -/** - * Adds a `.proxy(uri)` function to the "superagent" module's Request class. - * - * ``` js - * var request = require('superagent'); - * require('superagent-proxy')(request); - * - * request - * .get(uri) - * .proxy(uri) - * .end(fn); - * ``` - * - * Or, you can pass in a `superagent.Request` instance, and it's like calling the - * `.proxy(uri)` function on it, but without extending the prototype: - * - * ``` js - * var request = require('superagent'); - * var proxy = require('superagent-proxy'); - * - * proxy(request.get(uri), uri).end(fn); - * ``` - * - * @param {Object} superagent The `superagent` exports object - * @api public - */ - -export default function setup(superagent: any, uri?: string): SuperAgent & { proxy: (uri?: string) => SuperAgent } { - const Request = superagent.Request; - if (Request) { - // the superagent exports object - extent Request with "proxy" - Request.prototype.proxy = proxy; - return superagent; - } else { - // assume it's a `superagent.Request` instance - return proxy.call(superagent, uri); - } -} - -/** - * Sets the proxy server to use for this HTTP(s) request. - * - * @param {String} uri proxy url - * @api public - */ - -function proxy(this: any, uri?: string) { - debug('Request#proxy(%o)', uri); - // we need to observe the `url` field from now on... Superagent sometimes - // re-uses the `req` instance but changes its `url` field (i.e. in the case of - // a redirect), so when that happens we need to potentially re-set the proxy - // agent - setupUrl(this); - - // attempt to get a proxying `http.Agent` instance - const agent = new ProxyAgent(uri !== undefined ? { getProxyForUrl: () => uri } : {}); - - // if we have an `http.Agent` instance then call the .agent() function - if (agent) this.agent(agent); - - // store the proxy URI in case of changes to the `url` prop in the future - this._proxyUri = uri; - - return this; -} - -/** - * Sets up a get/set descriptor for the `url` property of the provided `req` - * Request instance. This is so that we can re-run the "proxy agent" logic when - * the `url` field is changed, i.e. during a 302 Redirect scenario. - * - * @api private - */ - -function setupUrl(req: any): void { - var desc = Object.getOwnPropertyDescriptor(req, 'url'); - if (desc?.get == getUrl && desc?.set == setUrl) return; // already patched - - // save current value - req._url = req.url; - - if (desc) { - desc.get = getUrl; - desc.set = setUrl; - delete desc.value; - delete desc.writable; - - Object.defineProperty(req, 'url', desc); - debug('patched superagent Request "url" property for changes'); - } -} - -/** - * `url` property getter. - * - * @api protected - */ - -function getUrl(this: any): string { - return this._url; -} - -/** - * `url` property setter. - * - * @api protected - */ - -function setUrl(this: any, v: string): void { - debug('set `.url`: %o', v); - this._url = v; - proxy.call(this, this._proxyUri); -} diff --git a/packages/@ionic/cli/src/lib/utils/uuid.ts b/packages/@ionic/cli/src/lib/utils/uuid.ts deleted file mode 100644 index 80cd6bc6cc..0000000000 --- a/packages/@ionic/cli/src/lib/utils/uuid.ts +++ /dev/null @@ -1,12 +0,0 @@ -export function generateUUID(): string { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { - const r = Math.random() * 16 | 0; - const v = c === 'x' ? r : (r & 0x3 | 0x8); - - return v.toString(16); - }); -} - -export function shortid(): string { - return generateUUID().substring(0, 8); -} diff --git a/packages/@ionic/cli/tsconfig.json b/packages/@ionic/cli/tsconfig.json deleted file mode 100644 index 85d3dba781..0000000000 --- a/packages/@ionic/cli/tsconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./", - "types": [ - "node" - ] - }, - "include": [ - "../../../types/leek.d.ts", - "../../../types/ssh-config.d.ts", - "../../../types/stream-combiner2.d.ts", - "src/**/*.ts" - ], - "exclude": [ - "node_modules", - "src/**/__tests__/*.ts" - ] -} diff --git a/packages/@ionic/discover/.gitignore b/packages/@ionic/discover/.gitignore deleted file mode 100644 index de4d1f007d..0000000000 --- a/packages/@ionic/discover/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -dist -node_modules diff --git a/packages/@ionic/discover/.npmrc b/packages/@ionic/discover/.npmrc deleted file mode 100644 index 43c97e719a..0000000000 --- a/packages/@ionic/discover/.npmrc +++ /dev/null @@ -1 +0,0 @@ -package-lock=false diff --git a/packages/@ionic/discover/CHANGELOG.md b/packages/@ionic/discover/CHANGELOG.md deleted file mode 100644 index 51de1d984c..0000000000 --- a/packages/@ionic/discover/CHANGELOG.md +++ /dev/null @@ -1,312 +0,0 @@ -# Change Log - -All notable changes to this project will be documented in this file. -See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -## [3.1.8](https://github.com/ionic-team/ionic-cli/compare/@ionic/discover@3.1.7...@ionic/discover@3.1.8) (2023-12-19) - - -### Bug Fixes - -* **cli:** resolve vm2 security vulnerability ([#5070](https://github.com/ionic-team/ionic-cli/issues/5070)) ([4050419](https://github.com/ionic-team/ionic-cli/commit/4050419bef70fb92e58b0a83cd4b68b48090e596)) - - - - - -## [3.1.7](https://github.com/ionic-team/ionic-cli/compare/@ionic/discover@3.1.6...@ionic/discover@3.1.7) (2023-11-07) - -**Note:** Version bump only for package @ionic/discover - - - - - -## [3.1.6](https://github.com/ionic-team/ionic-cli/compare/@ionic/discover@3.1.5...@ionic/discover@3.1.6) (2023-03-29) - -**Note:** Version bump only for package @ionic/discover - - - - - -## [3.1.5](https://github.com/ionic-team/ionic-cli/compare/@ionic/discover@3.1.4...@ionic/discover@3.1.5) (2020-08-28) - -**Note:** Version bump only for package @ionic/discover - - - - - -## [3.1.4](https://github.com/ionic-team/ionic-cli/compare/@ionic/discover@3.1.3...@ionic/discover@3.1.4) (2020-08-25) - -**Note:** Version bump only for package @ionic/discover - - - - - -## [3.1.3](https://github.com/ionic-team/ionic-cli/compare/@ionic/discover@3.1.2...@ionic/discover@3.1.3) (2020-05-12) - - -### Bug Fixes - -* pin tslib to avoid "Cannot set property pathExists" error ([689e1f0](https://github.com/ionic-team/ionic-cli/commit/689e1f038b907356ef855a067a76d4822e7072a8)) - - - - - -## [3.1.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/discover@3.1.1...@ionic/discover@3.1.2) (2020-05-06) - -**Note:** Version bump only for package @ionic/discover - - - - - -## [3.1.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/discover@3.1.0...@ionic/discover@3.1.1) (2020-03-03) - -**Note:** Version bump only for package @ionic/discover - - - - - -# 3.1.0 (2020-02-11) - - -### Features - -* **start:** add new list starter option ([#4315](https://github.com/ionic-team/ionic-cli/issues/4315)) ([1df44c1](https://github.com/ionic-team/ionic-cli/commit/1df44c1591f37b89f2b672857740edd6cb2aea67)) - - - - - -## [3.0.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/discover@3.0.1...@ionic/discover@3.0.2) (2020-02-10) - -**Note:** Version bump only for package @ionic/discover - - - - - -## [3.0.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/discover@3.0.0...@ionic/discover@3.0.1) (2020-02-03) - -**Note:** Version bump only for package @ionic/discover - - - - - -# [3.0.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/discover@2.0.8...@ionic/discover@3.0.0) (2020-01-25) - - -### chore - -* require Node 10 ([5a47874](https://github.com/ionic-team/ionic-cli/commit/5a478746c074207b6dc96aa8771f04a606deb1ef)) - - -### BREAKING CHANGES - -* A minimum of Node.js 10.3.0 is required. - - - - - -## [2.0.8](https://github.com/ionic-team/ionic-cli/compare/@ionic/discover@2.0.7...@ionic/discover@2.0.8) (2020-01-15) - -**Note:** Version bump only for package @ionic/discover - - - - - -## [2.0.7](https://github.com/ionic-team/ionic-cli/compare/@ionic/discover@2.0.6...@ionic/discover@2.0.7) (2019-12-05) - -**Note:** Version bump only for package @ionic/discover - - - - - -## [2.0.6](https://github.com/ionic-team/ionic-cli/compare/@ionic/discover@2.0.5...@ionic/discover@2.0.6) (2019-09-18) - -**Note:** Version bump only for package @ionic/discover - - - - - -## [2.0.5](https://github.com/ionic-team/ionic-cli/compare/@ionic/discover@2.0.4...@ionic/discover@2.0.5) (2019-08-23) - -**Note:** Version bump only for package @ionic/discover - - - - - -## [2.0.4](https://github.com/ionic-team/ionic-cli/compare/@ionic/discover@2.0.3...@ionic/discover@2.0.4) (2019-08-14) - -**Note:** Version bump only for package @ionic/discover - - - - - -## [2.0.3](https://github.com/ionic-team/ionic-cli/compare/@ionic/discover@2.0.2...@ionic/discover@2.0.3) (2019-08-07) - -**Note:** Version bump only for package @ionic/discover - - - - - -## [2.0.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/discover@2.0.1...@ionic/discover@2.0.2) (2019-06-18) - -**Note:** Version bump only for package @ionic/discover - - - - - -## [2.0.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/discover@2.0.0...@ionic/discover@2.0.1) (2019-06-05) - -**Note:** Version bump only for package @ionic/discover - - - - - -# [2.0.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/discover@1.0.13...@ionic/discover@2.0.0) (2019-05-29) - - -### chore - -* require Node 8 ([5670e68](https://github.com/ionic-team/ionic-cli/commit/5670e68)) - - -### BREAKING CHANGES - -* A minimum of Node.js 8.9.4 is required. - - - - - - -## [1.0.13](https://github.com/ionic-team/ionic-cli/compare/@ionic/discover@1.0.12...@ionic/discover@1.0.13) (2019-02-27) - - - - -**Note:** Version bump only for package @ionic/discover - - -## [1.0.12](https://github.com/ionic-team/ionic-cli/compare/@ionic/discover@1.0.11...@ionic/discover@1.0.12) (2019-02-15) - - - - -**Note:** Version bump only for package @ionic/discover - - -## [1.0.11](https://github.com/ionic-team/ionic-cli/compare/@ionic/discover@1.0.10...@ionic/discover@1.0.11) (2019-01-23) - - - - -**Note:** Version bump only for package @ionic/discover - - -## [1.0.10](https://github.com/ionic-team/ionic-cli/compare/@ionic/discover@1.0.9...@ionic/discover@1.0.10) (2019-01-07) - - - - -**Note:** Version bump only for package @ionic/discover - - -## [1.0.9](https://github.com/ionic-team/ionic-cli/compare/@ionic/discover@1.0.8...@ionic/discover@1.0.9) (2018-12-19) - - - - -**Note:** Version bump only for package @ionic/discover - - -## [1.0.8](https://github.com/ionic-team/ionic-cli/compare/@ionic/discover@1.0.7...@ionic/discover@1.0.8) (2018-11-20) - - -### Bug Fixes - -* **discover:** `new Buffer` deprecation warnings ([302b68c](https://github.com/ionic-team/ionic-cli/commit/302b68c)) - - - - - -## [1.0.7](https://github.com/ionic-team/ionic-cli/compare/@ionic/discover@1.0.6...@ionic/discover@1.0.7) (2018-10-31) - - - - -**Note:** Version bump only for package @ionic/discover - - -## [1.0.6](https://github.com/ionic-team/ionic-cli/compare/@ionic/discover@1.0.5...@ionic/discover@1.0.6) (2018-10-05) - - - - -**Note:** Version bump only for package @ionic/discover - - -## [1.0.5](https://github.com/ionic-team/ionic-cli/compare/@ionic/discover@1.0.4...@ionic/discover@1.0.5) (2018-10-03) - - - - -**Note:** Version bump only for package @ionic/discover - - -## [1.0.4](https://github.com/ionic-team/ionic-cli/compare/@ionic/discover@1.0.3...@ionic/discover@1.0.4) (2018-08-15) - - - - -**Note:** Version bump only for package @ionic/discover - - -## [1.0.3](https://github.com/ionic-team/ionic-cli/compare/@ionic/discover@1.0.2...@ionic/discover@1.0.3) (2018-08-06) - - - - -**Note:** Version bump only for package @ionic/discover - - -## [1.0.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/discover@1.0.1...@ionic/discover@1.0.2) (2018-08-02) - - - - -**Note:** Version bump only for package @ionic/discover - - -## [1.0.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/discover@1.0.0...@ionic/discover@1.0.1) (2018-07-30) - - - - -**Note:** Version bump only for package @ionic/discover - - -# [1.0.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/discover@1.0.0-rc.13...@ionic/discover@1.0.0) (2018-07-25) - - - - -**Note:** Version bump only for package @ionic/discover diff --git a/packages/@ionic/discover/LICENSE b/packages/@ionic/discover/LICENSE deleted file mode 100644 index 7c5808ced6..0000000000 --- a/packages/@ionic/discover/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2017 Drifty Co - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/packages/@ionic/discover/README.md b/packages/@ionic/discover/README.md deleted file mode 100644 index c6f9764873..0000000000 --- a/packages/@ionic/discover/README.md +++ /dev/null @@ -1,51 +0,0 @@ -# Ionic Discover - -Simple UDP based protocol for service discovery implemented in pure JS. It is -not mDNS or bonjour, but it tries to accomplish the same thing. - -## Spec - -It uses a JSON based textual format: - -```ts -const message = { - t: now, - id: 'unique', - name: this.name, - host: os.hostname(), - ip: iface.address, - port: number1, - commPort: number2 -}; - -return 'ION_DP' + JSON.stringify(message); -``` - -| key | description -|------------|------------- -| `t` | unix timestamp in second -| `id` | unique id for this session -| `name` | name of the announced service -| `host` | hostname of the machine announcing the service -| `ip` | ipv4 address -| `port` | tcp port of the announced service -| `commPort` | optional websocket port of the communication server - -## Installation - -``` -npm install @ionic/discover -``` - -## Usage - -```ts -import { Publisher } from '@ionic/discover'; - -const namespace = 'your-service'; -const serviceName = 'Ionic thing!'; -const tcpPort = 8100; -const service = new Publisher(namespace, serviceName, tcpPort); - -await service.start(); -``` diff --git a/packages/@ionic/discover/jest.config.js b/packages/@ionic/discover/jest.config.js deleted file mode 100644 index eace8d6d9d..0000000000 --- a/packages/@ionic/discover/jest.config.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('../../../jest.config.base'); diff --git a/packages/@ionic/discover/lint-staged.config.js b/packages/@ionic/discover/lint-staged.config.js deleted file mode 100644 index 5815a72f0d..0000000000 --- a/packages/@ionic/discover/lint-staged.config.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('../../../lint-staged.config.base'); diff --git a/packages/@ionic/discover/package.json b/packages/@ionic/discover/package.json deleted file mode 100644 index fc684c5114..0000000000 --- a/packages/@ionic/discover/package.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "name": "@ionic/discover", - "version": "3.1.8", - "description": "Simple UDP based protocol for service discovery implemented in pure JS.", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "homepage": "https://ionicframework.com/", - "author": "Ionic Team (https://ionic.io)", - "license": "MIT", - "files": [ - "dist/", - "LICENSE", - "README.md" - ], - "repository": { - "type": "git", - "url": "https://github.com/ionic-team/ionic-cli.git" - }, - "bugs": { - "url": "https://github.com/ionic-team/ionic-cli/issues" - }, - "scripts": { - "clean": "rimraf dist", - "lint": "true", - "build": "npm run clean && tsc", - "watch": "tsc -w --preserveWatchOutput", - "test": "jest --maxWorkers=4", - "prepublishOnly": "npm run build" - }, - "dependencies": { - "debug": "^4.0.0", - "netmask": "^2.0.2", - "tslib": "^2.0.1", - "ws": "^7.0.0" - }, - "devDependencies": { - "@types/debug": "^4.1.1", - "@types/jest": "^26.0.10", - "@types/netmask": "^2.0.5", - "@types/node": "~16.0.0", - "@types/ws": "^7.2.0", - "jest": "^26.4.2", - "jest-cli": "^26.0.1", - "lint-staged": "^10.0.2", - "rimraf": "^3.0.0", - "ts-jest": "~26.3.0", - "typescript": "~4.8.0" - } -} diff --git a/packages/@ionic/discover/src/__tests__/publisher.ts b/packages/@ionic/discover/src/__tests__/publisher.ts deleted file mode 100644 index cba103f4b8..0000000000 --- a/packages/@ionic/discover/src/__tests__/publisher.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { Publisher, prepareInterfaces } from '../publisher'; -import { NetworkInterfaceInfo } from 'os'; - -const os = require('os'); - -describe('publisher', () => { - it('create', () => { - const p = new Publisher('devapp', 'conference-app', 8100); - p.on('error', () => {}); - expect((p as any).client).toBeUndefined(); - expect(p.namespace).toEqual('devapp'); - expect(p.name).toEqual('conference-app'); - expect((p as any).interval).toEqual(2000); - expect(p.path).toEqual('/'); - expect(p.port).toEqual(8100); - expect(p.id.length).toBeGreaterThan(0); - expect(p.running).toBeFalsy(); - }); - - it('start', async () => { - const p = new Publisher('devapp', 'conference-app', 8100); - p.on('error', () => {}); - await p.start(); - expect((p as any).timer).toBeDefined(); - p.stop(); - expect((p as any).timer).toBeUndefined(); - }, 1000); - - it('start/stop', async () => { - const p = new Publisher('devapp', 'conference-app', 8100); - p.on('error', () => {}); - await p.start(); - expect(p.running).toBeTruthy(); - expect((p as any).client).toBeDefined(); - - p.stop(); - expect(p.running).toBeFalsy(); - expect((p as any).client).toBeUndefined(); - - }, 1000); - - it('buildMessage', () => { - const p = new Publisher('devapp', 'conference-app', 8100); - p.on('error', () => {}); - (p as any).path = '/?devapp=true'; - const message = (p as any).buildMessage('192.168.0.1'); - expect(message.t).toEqual(expect.any(Number)); - const expected = { - t: message.t, - id: p.id, - nspace: 'devapp', - name: 'conference-app', - host: os.hostname(), - ip: '192.168.0.1', - port: 8100, - path: '/?devapp=true' - }; - expect(message).toEqual(expected); - }); - -}); - -it('prepareInterfaces', () => { - const iface1: NetworkInterfaceInfo = { - cidr: '192.168.0.2/24', - address: '192.168.0.2', - netmask: '255.255.0.0', - family: 'IPv4', - mac: '45:45:45:45:45:45', - internal: false, - }; - const iface2: NetworkInterfaceInfo = { ...iface1, address: '193.168.0.3', internal: true }; - const iface3: NetworkInterfaceInfo = { ...iface1, address: '194.168.0.4', family: 'IPv6', scopeid: 0 }; - const iface4: NetworkInterfaceInfo = { ...iface1, address: '192.168.0.22' }; - const iface5: NetworkInterfaceInfo = { ...iface1, address: '195.168.0.5' }; - - const output = prepareInterfaces({ - 'en0': [iface1, iface2, iface3], - 'lo': [iface4, iface5] - }); - - expect(output).toEqual([{ - address: '192.168.0.2', - broadcast: '192.168.255.255' - }, { - address: '193.168.0.3', - broadcast: '193.168.255.255' - }, { - address: '195.168.0.5', - broadcast: '195.168.255.255' - }]); -}); - diff --git a/packages/@ionic/discover/src/comm.ts b/packages/@ionic/discover/src/comm.ts deleted file mode 100644 index 5fb3af0776..0000000000 --- a/packages/@ionic/discover/src/comm.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { debug as Debug } from 'debug'; -import * as events from 'events'; -import * as util from 'util'; -import WebSocket from 'ws'; - - -const debug = Debug('ionic:discover:comm'); - -const PREFIX = 'ION_CS'; - -export interface CommServerConnectionPayload { - event: 'connect'; - device: string; -} - -type Payload = CommServerConnectionPayload; - -export interface CommServer { - on(event: 'error', listener: (err: Error) => void): this; - on(event: 'connect', listener: (data: CommServerConnectionPayload) => void): this; -} - -export class CommServer extends events.EventEmitter { - protected server?: WebSocket.Server; - - constructor( - public namespace: string, - - /** - * Unique identifier of the publisher. - */ - public id: string, - - /** - * Port of communication server. - */ - public port: number - ) { - super(); - } - - get clients(): Set { - return this.server ? this.server.clients : new Set(); - } - - start(): Promise { - if (this.server) { - throw new Error('server already initialized'); - } - - const server = this.server = new WebSocket.Server({ clientTracking: true, host: '0.0.0.0', port: this.port }); - - return new Promise((resolve, reject) => { - server.on('error', err => { - this.emit('error', err); - }); - - server.on('connection', ws => { - debug(`Connection established. ${server.clients.size} clients.`); - - ws.on('message', data => { - debug(`Received %O`, data.toString()); - - const message = this.parseData(data); - - if (message) { - this.emit(message.event, message); - } - }); - - ws.on('close', () => { - debug(`Connection closed. ${server.clients.size} clients.`); - }); - }); - - server.on('listening', () => { - debug('Comm server listening: %O', { host: server.options.host, port: this.port }); - resolve(); - }); - }); - } - - private parseData(data: WebSocket.Data): Payload | undefined { - try { - const msg = data.toString(); - const msgprefix = msg.substring(0, PREFIX.length); - - if (msgprefix !== PREFIX) { - throw new Error(`Invalid prefix for message: ${msgprefix}`); - } - - const payload = JSON.parse(msg.substring(PREFIX.length)); - - if (!isPayload(payload)) { - throw new Error(`Invalid payload: ${util.inspect(payload)}`); - } - - return payload; - } catch (e: any) { - debug(e); - } - } - - stop(): Promise { - if (!this.server) { - throw new Error('server not initialized'); - } - - const server = this.server; - - return new Promise(resolve => { - server.close(() => resolve()); - }); - } -} - -function isPayload(payload: any): payload is Payload { - return payload && typeof payload.event === 'string'; -} diff --git a/packages/@ionic/discover/src/index.ts b/packages/@ionic/discover/src/index.ts deleted file mode 100644 index d84d97a512..0000000000 --- a/packages/@ionic/discover/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { CommServer } from './comm'; -export { Publisher, computeBroadcastAddress, newSilentPublisher } from './publisher'; diff --git a/packages/@ionic/discover/src/publisher.ts b/packages/@ionic/discover/src/publisher.ts deleted file mode 100644 index d36f2b112d..0000000000 --- a/packages/@ionic/discover/src/publisher.ts +++ /dev/null @@ -1,199 +0,0 @@ -import { debug as Debug } from 'debug'; -import * as dgram from 'dgram'; -import * as events from 'events'; -import { Netmask } from 'netmask'; -import * as os from 'os'; - -const debug = Debug('ionic:discover:publisher'); - -const PREFIX = 'ION_DP'; -const PORT = 41234; - -export interface Interface { - address: string; - broadcast: string; -} - -export interface PublisherMessage { - t: number; - id: string; - nspace: string; - name: string; - host: string; - ip: string; - port: number; - commPort?: number; - path: string; -} - -export interface IPublisherEventEmitter { - on(event: 'error', listener: (err: Error) => void): this; -} - -export class Publisher extends events.EventEmitter implements IPublisherEventEmitter { - readonly id: string; - readonly path = '/'; - - running = false; - interfaces?: Interface[]; - - protected timer?: NodeJS.Timer; - protected interval = 2000; - protected client?: dgram.Socket; - - constructor( - public namespace: string, - public name: string, - public port: number, - public commPort?: number - ) { - super(); - - if (name.indexOf(':') >= 0) { - throw new Error('name should not contain ":"'); - } - - this.id = Math.random().toString(10).substring(2, 8); - } - - start(): Promise { - return new Promise((resolve, reject) => { - if (this.running) { - return resolve(); - } - - this.running = true; - - if (!this.interfaces) { - this.interfaces = this.getInterfaces(); - } - - const client = this.client = dgram.createSocket('udp4'); - - client.on('error', err => { - this.emit('error', err); - }); - - client.on('listening', () => { - client.setBroadcast(true); - this.timer = setInterval(() => this.sayHello(), this.interval); - this.sayHello(); - debug('Publisher starting'); - resolve(); - }); - - client.bind(); - }); - } - - stop() { - if (!this.running) { - return; - } - - this.running = false; - - if (this.timer) { - clearInterval(this.timer); - this.timer = undefined; - } - - if (this.client) { - this.client.close(); - this.client = undefined; - } - } - - protected buildMessage(ip: string): PublisherMessage { - return { - t: Date.now(), - id: this.id, - nspace: this.namespace, - name: this.name, - host: os.hostname(), - ip, - port: this.port, - commPort: this.commPort, - path: this.path, - }; - } - - protected getInterfaces(): Interface[] | undefined { - return prepareInterfaces(os.networkInterfaces()); - } - - protected sayHello() { - if (!this.interfaces) { - throw new Error('No network interfaces set--was the service started?'); - } - - if (!this.client) { - throw new Error('Client not initialized--was the service started?'); - } - - try { - for (const iface of this.interfaces) { - const message = this.buildMessage(iface.address); - const serialized = PREFIX + JSON.stringify(message); - const buf = Buffer.from(serialized); - - debug(`Broadcasting %O to ${iface.broadcast}`, serialized); - - this.client.send(buf, 0, buf.length, PORT, iface.broadcast, err => { - if (err) { - this.emit('error', err); - } - }); - } - } catch (e: any) { - this.emit('error', e); - } - } -} - -export function prepareInterfaces(interfaces: NodeJS.Dict): Interface[] | undefined { - const set = new Set(); - const values = Object.values(interfaces); - const flatValues = values.reduce((prev, current) => prev?.concat(current ? current : [])); - if (flatValues) { - return flatValues - .filter((iface: os.NetworkInterfaceInfo) => iface.family === 'IPv4') - .map((iface: os.NetworkInterfaceInfo) => { - return { - address: iface.address, - broadcast: computeBroadcastAddress(iface.address, iface.netmask), - }; - }) - .filter((iface: any) => { - if (!set.has(iface.broadcast)) { - set.add(iface.broadcast); - - return true; - } - - return false; - }); - } -} - -export function newSilentPublisher(namespace: string, name: string, port: number): Publisher { - name = `${name}@${port}`; - const service = new Publisher(namespace, name, port); - - service.on('error', () => { - // do not log - }); - - service.start().catch(() => { - // do not log - }); - - return service; -} - -export function computeBroadcastAddress(address: string, netmask: string): string { - const ip = address + '/' + netmask; - const block = new Netmask(ip); - - return block.broadcast; -} diff --git a/packages/@ionic/discover/tsconfig.json b/packages/@ionic/discover/tsconfig.json deleted file mode 100644 index 46cccd1031..0000000000 --- a/packages/@ionic/discover/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "types": [ - "node" - ] - }, - "include": [ - "src/**/*.ts" - ], - "exclude": [ - "node_modules", - "src/**/__tests__/*.ts" - ] -} diff --git a/packages/@ionic/utils-array/.gitignore b/packages/@ionic/utils-array/.gitignore deleted file mode 100644 index de4d1f007d..0000000000 --- a/packages/@ionic/utils-array/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -dist -node_modules diff --git a/packages/@ionic/utils-array/.npmrc b/packages/@ionic/utils-array/.npmrc deleted file mode 100644 index 43c97e719a..0000000000 --- a/packages/@ionic/utils-array/.npmrc +++ /dev/null @@ -1 +0,0 @@ -package-lock=false diff --git a/packages/@ionic/utils-array/CHANGELOG.md b/packages/@ionic/utils-array/CHANGELOG.md deleted file mode 100644 index 18837bc5c5..0000000000 --- a/packages/@ionic/utils-array/CHANGELOG.md +++ /dev/null @@ -1,185 +0,0 @@ -# Change Log - -All notable changes to this project will be documented in this file. -See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -## [2.1.6](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-array@2.1.5...@ionic/utils-array@2.1.6) (2023-03-29) - -**Note:** Version bump only for package @ionic/utils-array - - - - - -## [2.1.5](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-array@2.1.4...@ionic/utils-array@2.1.5) (2020-08-28) - -**Note:** Version bump only for package @ionic/utils-array - - - - - -## [2.1.4](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-array@2.1.3...@ionic/utils-array@2.1.4) (2020-08-25) - -**Note:** Version bump only for package @ionic/utils-array - - - - - -## [2.1.3](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-array@2.1.2...@ionic/utils-array@2.1.3) (2020-05-12) - - -### Bug Fixes - -* pin tslib to avoid "Cannot set property pathExists" error ([689e1f0](https://github.com/ionic-team/ionic-cli/commit/689e1f038b907356ef855a067a76d4822e7072a8)) - - - - - -## [2.1.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-array@2.1.1...@ionic/utils-array@2.1.2) (2020-05-06) - -**Note:** Version bump only for package @ionic/utils-array - - - - - -## [2.1.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-array@2.1.0...@ionic/utils-array@2.1.1) (2020-03-03) - -**Note:** Version bump only for package @ionic/utils-array - - - - - -# 2.1.0 (2020-02-11) - - -### Features - -* **start:** add new list starter option ([#4315](https://github.com/ionic-team/ionic-cli/issues/4315)) ([1df44c1](https://github.com/ionic-team/ionic-cli/commit/1df44c1591f37b89f2b672857740edd6cb2aea67)) - - - - - -## [2.0.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-array@2.0.1...@ionic/utils-array@2.0.2) (2020-02-10) - -**Note:** Version bump only for package @ionic/utils-array - - - - - -## [2.0.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-array@2.0.0...@ionic/utils-array@2.0.1) (2020-02-03) - -**Note:** Version bump only for package @ionic/utils-array - - - - - -# [2.0.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-array@1.2.2...@ionic/utils-array@2.0.0) (2020-01-25) - - -### chore - -* require Node 10 ([5a47874](https://github.com/ionic-team/ionic-cli/commit/5a478746c074207b6dc96aa8771f04a606deb1ef)) - - -### BREAKING CHANGES - -* A minimum of Node.js 10.3.0 is required. - - - - - -## [1.2.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-array@1.2.1...@ionic/utils-array@1.2.2) (2019-12-05) - -**Note:** Version bump only for package @ionic/utils-array - - - - - -## [1.2.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-array@1.2.0...@ionic/utils-array@1.2.1) (2019-09-18) - -**Note:** Version bump only for package @ionic/utils-array - - - - - -# [1.2.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-array@1.1.1...@ionic/utils-array@1.2.0) (2019-08-28) - - -### Features - -* **replace:** add replace item in array by index function ([011ddf7](https://github.com/ionic-team/ionic-cli/commit/011ddf7)) -* **splice:** add non-mutating splice function ([758d287](https://github.com/ionic-team/ionic-cli/commit/758d287)) - - - - - -## [1.1.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-array@1.1.0...@ionic/utils-array@1.1.1) (2019-08-23) - -**Note:** Version bump only for package @ionic/utils-array - - - - - -# [1.1.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-array@1.0.2...@ionic/utils-array@1.1.0) (2019-08-14) - - -### Features - -* add new `move` function ([ba8da3b](https://github.com/ionic-team/ionic-cli/commit/ba8da3b)) - - - - - -## [1.0.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-array@1.0.1...@ionic/utils-array@1.0.2) (2019-08-07) - -**Note:** Version bump only for package @ionic/utils-array - - - - - -## [1.0.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-array@1.0.0...@ionic/utils-array@1.0.1) (2019-06-05) - -**Note:** Version bump only for package @ionic/utils-array - - - - - -# [1.0.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-array@0.0.1...@ionic/utils-array@1.0.0) (2019-05-29) - - -### chore - -* require Node 8 ([5670e68](https://github.com/ionic-team/ionic-cli/commit/5670e68)) - - -### BREAKING CHANGES - -* A minimum of Node.js 8.9.4 is required. - - - - - - -## 0.0.1 (2019-02-27) - - - - -**Note:** Version bump only for package @ionic/utils-array diff --git a/packages/@ionic/utils-array/LICENSE b/packages/@ionic/utils-array/LICENSE deleted file mode 100644 index 7c5808ced6..0000000000 --- a/packages/@ionic/utils-array/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2017 Drifty Co - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/packages/@ionic/utils-array/README.md b/packages/@ionic/utils-array/README.md deleted file mode 100644 index f43b97c120..0000000000 --- a/packages/@ionic/utils-array/README.md +++ /dev/null @@ -1 +0,0 @@ -# @ionic/utils-array diff --git a/packages/@ionic/utils-array/jest.config.js b/packages/@ionic/utils-array/jest.config.js deleted file mode 100644 index eace8d6d9d..0000000000 --- a/packages/@ionic/utils-array/jest.config.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('../../../jest.config.base'); diff --git a/packages/@ionic/utils-array/lint-staged.config.js b/packages/@ionic/utils-array/lint-staged.config.js deleted file mode 100644 index 5815a72f0d..0000000000 --- a/packages/@ionic/utils-array/lint-staged.config.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('../../../lint-staged.config.base'); diff --git a/packages/@ionic/utils-array/package.json b/packages/@ionic/utils-array/package.json deleted file mode 100644 index a7e8552d04..0000000000 --- a/packages/@ionic/utils-array/package.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "name": "@ionic/utils-array", - "version": "2.1.6", - "description": "Array utils", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "homepage": "https://ionicframework.com/", - "author": "Ionic Team (https://ionic.io)", - "license": "MIT", - "engines": { - "node": ">=16.0.0" - }, - "files": [ - "dist/", - "LICENSE", - "README.md" - ], - "repository": { - "type": "git", - "url": "https://github.com/ionic-team/ionic-cli.git" - }, - "bugs": { - "url": "https://github.com/ionic-team/ionic-cli/issues" - }, - "scripts": { - "clean": "rimraf dist", - "lint": "true", - "build": "npm run clean && tsc", - "watch": "tsc -w --preserveWatchOutput", - "test": "jest --maxWorkers=4", - "prepublishOnly": "npm run build" - }, - "dependencies": { - "debug": "^4.0.0", - "tslib": "^2.0.1" - }, - "devDependencies": { - "@types/debug": "^4.1.1", - "@types/jest": "^26.0.10", - "@types/node": "~16.0.0", - "jest": "^26.4.2", - "jest-cli": "^26.0.1", - "lint-staged": "^10.0.2", - "rimraf": "^3.0.0", - "ts-jest": "~26.3.0", - "typescript": "~4.8.0" - } -} diff --git a/packages/@ionic/utils-array/src/__tests__/index.ts b/packages/@ionic/utils-array/src/__tests__/index.ts deleted file mode 100644 index 69b0c46f99..0000000000 --- a/packages/@ionic/utils-array/src/__tests__/index.ts +++ /dev/null @@ -1,328 +0,0 @@ -import { concurrentFilter, conform, filter, reduce, map, splice, move, replace } from '../'; - -describe('@ionic/utils-array', () => { - - describe('conform', () => { - - it('should conform undefined to empty array', () => { - const result = conform(undefined); - expect(result).toEqual([]); - }); - - it('should conform a number to a single-item array with that number', () => { - const result = conform(5); - expect(result).toEqual([5]); - }); - - it('should conform empty string to a single-item array with empty string', () => { - const result = conform(''); - expect(result).toEqual(['']); - }); - - it('should conform zero to a single-item array with zero', () => { - const result = conform(0); - expect(result).toEqual([0]); - }); - - it('should conform a string to a single-item array with that string', () => { - const result = conform('foo'); - expect(result).toEqual(['foo']); - }); - - it('should do nothing to an empty array', () => { - const result = conform([]); - expect(result).toEqual([]); - }); - - it('should do nothing to an array of numbers', () => { - const result = conform([1, 2, 3]); - expect(result).toEqual([1, 2, 3]); - }); - - it('should do nothing to an array of strings', () => { - const result = conform(['foo', 'bar', 'baz']); - expect(result).toEqual(['foo', 'bar', 'baz']); - }); - - }); - - describe('concurrentFilter', () => { - - it('should return new empty array', async () => { - const initial: number[] = []; - const result = await concurrentFilter(initial, async () => true); - expect(result).not.toBe(initial); - expect(result).toEqual([]); - }); - - it('should return new array', async () => { - const initial = [1, 2, 3]; - const result = await concurrentFilter(initial, async () => true); - expect(result).not.toBe(initial); - expect(result).toEqual([1, 2, 3]); - }); - - it('should filter out everything', async () => { - const initial = [1, 2, 3]; - const result = await concurrentFilter(initial, async () => false); - expect(result).toEqual([]); - }); - - it('should filter out conditionally', async () => { - const initial = [1, 2, 3]; - const result = await concurrentFilter(initial, async v => v % 2 === 0); - expect(result).toEqual([2]); - }); - - }); - - describe('filter', () => { - - it('should return new empty array', async () => { - const initial: number[] = []; - const result = await filter(initial, async () => true); - expect(result).not.toBe(initial); - expect(result).toEqual([]); - }); - - it('should return new array', async () => { - const initial = [1, 2, 3]; - const result = await filter(initial, async () => true); - expect(result).not.toBe(initial); - expect(result).toEqual([1, 2, 3]); - }); - - it('should filter out everything', async () => { - const initial = [1, 2, 3]; - const result = await filter(initial, async () => false); - expect(result).toEqual([]); - }); - - it('should filter out conditionally', async () => { - const initial = [1, 2, 3]; - const result = await filter(initial, async v => v % 2 === 0); - expect(result).toEqual([2]); - }); - - it('should filter out conditionally using index', async () => { - const initial = [1, 2, 3]; - const result = await filter(initial, async (v, i) => i % 2 === 0); - expect(result).toEqual([1, 3]); - }); - - it('should pass array into callback with each iteration', async () => { - const initial = [1, 2, 3]; - const result = await filter(initial, async (v, i, arr) => { expect(arr).toBe(initial); return false; }); - expect(result).toEqual([]); - }); - - }); - - describe('map', () => { - - it('should return new empty array', async () => { - const initial: number[] = []; - const result = await map(initial, async () => {}); - expect(result).not.toBe(initial); - expect(result).toEqual([]); - }); - - it('should return new array', async () => { - const initial = [1, 2, 3]; - const result = await map(initial, async () => 0); - expect(result).not.toBe(initial); - expect(result).toEqual([0, 0, 0]); - }); - - it('should map using current value', async () => { - const result = await map([1, 2, 3], async v => v + 1); - expect(result).toEqual([2, 3, 4]); - }); - - it('should map to different data type', async () => { - const result = await map([1, 2, 3], async () => ''); - expect(result).toEqual(['', '', '']); - }); - - it('should map using current index', async () => { - const result = await map([1, 2, 3], async (v, i) => String.fromCharCode(i + 65 + 32)); - expect(result).toEqual(['a', 'b', 'c']); - }); - - it('should pass array into callback with each iteration', async () => { - const initial = [1, 2, 3]; - const result = await map(initial, async (v, i, arr) => { expect(arr).toBe(initial); return v; }); - expect(result).toEqual([1, 2, 3]); - }); - - }); - - describe('reduce', () => { - - it('should throw type error with empty array and no initial value', async () => { - await expect(reduce([], async () => {})).rejects.toThrow('Reduce of empty array with no initial value'); - }); - - it('should reduce to initial value with empty array', async () => { - const result = await reduce([], async value => value, 10); - expect(result).toEqual(10); - }); - - it('should reduce to undefined if callback returns nothing', async () => { - const cb = async () => undefined; - const result = await reduce([1, 2, 3], cb as any, 0); - expect(result).toBeUndefined(); - }); - - it('should reduce to undefined if callback returns nothing and no initial value supplied', async () => { - const result = await reduce([1, 2, 3], async () => {}); - expect(result).toBeUndefined(); - }); - - it('should reduce via returned values', async () => { - const result = await reduce(['a', 'b', 'c'], async (acc, v) => acc + v); - expect(result).toEqual('abc'); - }); - - it('should reduce via returned values with initial value', async () => { - const result = await reduce(['a', 'b', 'c'], async (acc, v) => acc + v, 'answer: '); - expect(result).toEqual('answer: abc'); - }); - - it('should reduce via returned values with initial value using current index', async () => { - const result = await reduce(['a', 'b', 'c'], async (acc, v, i) => acc + i, 0); - expect(result).toEqual(3); - }); - - it('should reduce via returned values using accumulator and current index', async () => { - const result = await reduce([1, 2, 3], async (acc, v, i) => String(acc) + String(v + i)); - expect(result).toEqual('135'); - }); - - it('should reduce via accumulator', async () => { - const result = await reduce([1, 2, 3], async (acc, v) => { acc.push('call: ' + v); return acc; }, []); - expect(result).toEqual(['call: 1', 'call: 2', 'call: 3']); - }); - - it('should pass array into callback with each iteration', async () => { - const initial = [1, 2, 3]; - const result = await reduce(initial, async (acc, v, i, arr) => { expect(arr).toBe(initial); return acc; }, []); - expect(result).toEqual([]); - }); - - }); - - describe('splice', () => { - - const array = ['a', 'b', 'c']; - - it('should delete all items with start equal to zero and without deleteCount', () => { - const result = splice(array, 0); - expect(result).toEqual([]); - expect(result).not.toBe(array); - }); - - it('should leave array unchanged with start equal to zero and with deleteCount equal to zero', () => { - const result = splice(array, 0, 0); - expect(result).toEqual(array); - expect(result).not.toBe(array); - }); - - it('should delete one from index position 1', () => { - const result = splice(array, 1, 1); - expect(result).toEqual(['a', 'c']); - expect(result).not.toBe(array); - }); - - it('should delete two from index position 0 and then add an item', () => { - const result = splice(array, 0, 2, 'z'); - expect(result).toEqual(['z', 'c']); - expect(result).not.toBe(array); - }); - - }); - - describe('move', () => { - - const array = ['a', 'b', 'c']; - - it('should move first element to last element', () => { - const result = move(array, 0, 2); - expect(result).toEqual(['b', 'c', 'a']); - }); - - it('should move last element to first element', () => { - const result = move(array, 2, 0); - expect(result).toEqual(['c', 'a', 'b']); - }); - - it('should not move element with equal indexes', () => { - const result = move(array, 1, 1); - expect(result).toEqual(['a', 'b', 'c']); - expect(result).not.toBe(array); - }); - - describe('out of bounds', () => { - it('should leave array unchanged for from index greater than array length', () => { - const result = move(array, 5, 0); - expect(result).toEqual(['a', 'b', 'c']); - expect(result).not.toBe(array); - }); - - it('should leave array unchanged for to index greater than array length', () => { - const result = move(array, 0, 5); - expect(result).toEqual(['a', 'b', 'c']); - expect(result).not.toBe(array); - }); - - it('should leave array unchanged for from index less than zero', () => { - const result = move(array, -1, 0); - expect(result).toEqual(['a', 'b', 'c']); - expect(result).not.toBe(array); - }); - - it('should leave array unchanged for to index less than zero', () => { - const result = move(array, 0, -1); - expect(result).toEqual(['a', 'b', 'c']); - expect(result).not.toBe(array); - }); - - }); - - }); - - describe('replace', () => { - - const array = ['a', 'b', 'c']; - - it('should replace first element with z', () => { - const result = replace(array, 0, 'z'); - expect(result).toEqual(['z', 'b', 'c']); - expect(result).not.toBe(array); - }); - - it('should replace last element with z', () => { - const result = replace(array, 2, 'z'); - expect(result).toEqual(['a', 'b', 'z']); - expect(result).not.toBe(array); - }); - - describe('out of bounds', () => { - - it('should leave array unchanged for index less than zero', () => { - const result = replace(array, -1, 'z'); - expect(result).toEqual(['a', 'b', 'c']); - expect(result).not.toBe(array); - }); - - it('should leave array unchanged for index greater than array index', () => { - const result = replace(array, 5, 'z'); - expect(result).toEqual(['a', 'b', 'c']); - expect(result).not.toBe(array); - }); - - }); - - }); - -}); diff --git a/packages/@ionic/utils-array/src/index.ts b/packages/@ionic/utils-array/src/index.ts deleted file mode 100644 index 47a4173e49..0000000000 --- a/packages/@ionic/utils-array/src/index.ts +++ /dev/null @@ -1,122 +0,0 @@ -export function conform(t?: T | T[]): T[] { - if (typeof t === 'undefined') { - return []; - } - - if (!Array.isArray(t)) { - return [t]; - } - - return t; -} - -export async function concurrentFilter(array: T[], callback: (currentValue: T) => Promise): Promise { - const mapper = async (v: T): Promise<[T, boolean]> => [v, await callback(v)]; - const mapped = await Promise.all(array.map(mapper)); - - return mapped - .filter(([ , f ]) => f) - .map(([ v ]) => v); -} - -export async function filter(array: T[], callback: (currentValue: T, currentIndex: number, array: readonly T[]) => Promise): Promise; -export async function filter(array: readonly T[], callback: (currentValue: T, currentIndex: number, array: readonly T[]) => Promise): Promise; -export async function filter(array: T[] | readonly T[], callback: (currentValue: T, currentIndex: number, array: readonly T[]) => Promise): Promise { - const initial: T[] = []; - - return reduce(array, async (acc, v, i, arr) => { - if (await callback(v, i, arr)) { - acc.push(v); - } - - return acc; - }, initial); -} - -export async function map(array: T[], callback: (currentValue: T, currentIndex: number, array: readonly T[]) => Promise): Promise; -export async function map(array: T[], callback: (currentValue: T, currentIndex: number, array: readonly T[]) => Promise): Promise; -export async function map(array: readonly T[], callback: (currentValue: T, currentIndex: number, array: readonly T[]) => Promise): Promise; -export async function map(array: readonly T[], callback: (currentValue: T, currentIndex: number, array: readonly T[]) => Promise): Promise; -export async function map(array: T[] | readonly T[], callback: (currentValue: T, currentIndex: number, array: readonly T[]) => Promise): Promise { - const initial: U[] = []; - - return reduce(array, async (acc, v, i, arr) => { - acc.push(await callback(v, i, arr)); - - return acc; - }, initial); -} - -export async function reduce(array: T[], callback: (accumulator: T, currentValue: T, currentIndex: number, array: readonly T[]) => Promise): Promise; -export async function reduce(array: T[], callback: (accumulator: T, currentValue: T, currentIndex: number, array: readonly T[]) => Promise, initialValue: T): Promise; -export async function reduce(array: T[], callback: (accumulator: R, currentValue: T, currentIndex: number, array: readonly T[]) => Promise): Promise; -export async function reduce(array: T[], callback: (accumulator: U, currentValue: T, currentIndex: number, array: readonly T[]) => Promise, initialValue: U): Promise; -export async function reduce(array: readonly T[], callback: (accumulator: T, currentValue: T, currentIndex: number, array: readonly T[]) => Promise): Promise; -export async function reduce(array: readonly T[], callback: (accumulator: T, currentValue: T, currentIndex: number, array: readonly T[]) => Promise, initialValue: T): Promise; -export async function reduce(array: readonly T[], callback: (accumulator: R, currentValue: T, currentIndex: number, array: readonly T[]) => Promise): Promise; -export async function reduce(array: readonly T[], callback: (accumulator: U, currentValue: T, currentIndex: number, array: readonly T[]) => Promise, initialValue: U): Promise; -export async function reduce(array: T[] | readonly T[], callback: (accumulator: T | U, currentValue: T, currentIndex: number, array: readonly T[]) => Promise, initialValue?: T | U): Promise { - const hadInitialValue = typeof initialValue === 'undefined'; - const startingIndex = hadInitialValue ? 1 : 0; - - if (typeof initialValue === 'undefined') { - if (array.length === 0) { - throw new TypeError('Reduce of empty array with no initial value'); - } - - initialValue = array[0]; - } - - let value = initialValue; - - for (let i = startingIndex; i < array.length; i++) { - const v = await callback(value, array[i], i, array); - value = v; - } - - return value; -} - -/** - * Splice an array. - * - * This function will return a new array with the standard splice behavior - * applied. Unlike the standard array splice, the array of removed items is not - * returned. - */ -export function splice(array: readonly T[], start: number, deleteCount = array.length - start, ...items: T[]): T[] { - const result = [...array]; - result.splice(start, deleteCount, ...items); - return result; -} - -/** - * Move an item in an array by index. - * - * This function will return a new array with the item in the `fromIndex` - * position moved to the `toIndex` position. If `fromIndex` or `toIndex` are - * out of bounds, the array items remain unmoved. - */ -export function move(array: readonly T[], fromIndex: number, toIndex: number): T[] { - const element = array[fromIndex]; - - if (fromIndex < 0 || toIndex < 0 || fromIndex >= array.length || toIndex >= array.length) { - return [...array]; - } - - return splice(splice(array, fromIndex, 1), toIndex, 0, element); -} - -/** - * Replace an item in an array by index. - * - * This function will return a new array with the item in the `index` position - * replaced with `item`. If `index` is out of bounds, the item is not replaced. - */ -export function replace(array: readonly T[], index: number, item: T): T[] { - if (index < 0 || index > array.length) { - return [...array]; - } - - return splice(array, index, 1, item); -} diff --git a/packages/@ionic/utils-array/tsconfig.json b/packages/@ionic/utils-array/tsconfig.json deleted file mode 100644 index 46cccd1031..0000000000 --- a/packages/@ionic/utils-array/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "types": [ - "node" - ] - }, - "include": [ - "src/**/*.ts" - ], - "exclude": [ - "node_modules", - "src/**/__tests__/*.ts" - ] -} diff --git a/packages/@ionic/utils-fs/.gitignore b/packages/@ionic/utils-fs/.gitignore deleted file mode 100644 index de4d1f007d..0000000000 --- a/packages/@ionic/utils-fs/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -dist -node_modules diff --git a/packages/@ionic/utils-fs/.npmrc b/packages/@ionic/utils-fs/.npmrc deleted file mode 100644 index 43c97e719a..0000000000 --- a/packages/@ionic/utils-fs/.npmrc +++ /dev/null @@ -1 +0,0 @@ -package-lock=false diff --git a/packages/@ionic/utils-fs/CHANGELOG.md b/packages/@ionic/utils-fs/CHANGELOG.md deleted file mode 100644 index 872cb72c57..0000000000 --- a/packages/@ionic/utils-fs/CHANGELOG.md +++ /dev/null @@ -1,318 +0,0 @@ -# Change Log - -All notable changes to this project will be documented in this file. -See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -## [3.1.7](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-fs@3.1.6...@ionic/utils-fs@3.1.7) (2023-03-29) - -**Note:** Version bump only for package @ionic/utils-fs - - - - - -## [3.1.6](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-fs@3.1.5...@ionic/utils-fs@3.1.6) (2022-06-16) - -**Note:** Version bump only for package @ionic/utils-fs - - - - - -## [3.1.5](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-fs@3.1.4...@ionic/utils-fs@3.1.5) (2020-08-28) - -**Note:** Version bump only for package @ionic/utils-fs - - - - - -## [3.1.4](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-fs@3.1.3...@ionic/utils-fs@3.1.4) (2020-08-25) - -**Note:** Version bump only for package @ionic/utils-fs - - - - - -## [3.1.3](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-fs@3.1.2...@ionic/utils-fs@3.1.3) (2020-05-12) - - -### Bug Fixes - -* pin tslib to avoid "Cannot set property pathExists" error ([689e1f0](https://github.com/ionic-team/ionic-cli/commit/689e1f038b907356ef855a067a76d4822e7072a8)) - - - - - -## [3.1.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-fs@3.1.1...@ionic/utils-fs@3.1.2) (2020-05-06) - -**Note:** Version bump only for package @ionic/utils-fs - - - - - -## [3.1.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-fs@3.1.0...@ionic/utils-fs@3.1.1) (2020-03-03) - -**Note:** Version bump only for package @ionic/utils-fs - - - - - -# 3.1.0 (2020-02-11) - - -### Features - -* **start:** add new list starter option ([#4315](https://github.com/ionic-team/ionic-cli/issues/4315)) ([1df44c1](https://github.com/ionic-team/ionic-cli/commit/1df44c1591f37b89f2b672857740edd6cb2aea67)) - - - - - -## [3.0.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-fs@3.0.1...@ionic/utils-fs@3.0.2) (2020-02-10) - -**Note:** Version bump only for package @ionic/utils-fs - - - - - -## [3.0.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-fs@3.0.0...@ionic/utils-fs@3.0.1) (2020-02-03) - -**Note:** Version bump only for package @ionic/utils-fs - - - - - -# [3.0.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-fs@2.0.9...@ionic/utils-fs@3.0.0) (2020-01-25) - - -### chore - -* require Node 10 ([5a47874](https://github.com/ionic-team/ionic-cli/commit/5a478746c074207b6dc96aa8771f04a606deb1ef)) - - -### BREAKING CHANGES - -* A minimum of Node.js 10.3.0 is required. - - - - - -## [2.0.9](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-fs@2.0.8...@ionic/utils-fs@2.0.9) (2019-12-05) - -**Note:** Version bump only for package @ionic/utils-fs - - - - - -## [2.0.8](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-fs@2.0.7...@ionic/utils-fs@2.0.8) (2019-10-14) - - -### Bug Fixes - -* **fileToString:** handle ENOTDIR errors ([fab07d8](https://github.com/ionic-team/ionic-cli/commit/fab07d8c6e3d6756d633ada4c81165d11c70eb8f)) - - - - - -## [2.0.7](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-fs@2.0.6...@ionic/utils-fs@2.0.7) (2019-09-18) - -**Note:** Version bump only for package @ionic/utils-fs - - - - - -## [2.0.6](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-fs@2.0.5...@ionic/utils-fs@2.0.6) (2019-08-23) - -**Note:** Version bump only for package @ionic/utils-fs - - - - - -## [2.0.5](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-fs@2.0.4...@ionic/utils-fs@2.0.5) (2019-08-14) - -**Note:** Version bump only for package @ionic/utils-fs - - - - - -## [2.0.4](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-fs@2.0.3...@ionic/utils-fs@2.0.4) (2019-08-07) - -**Note:** Version bump only for package @ionic/utils-fs - - - - - -## [2.0.3](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-fs@2.0.2...@ionic/utils-fs@2.0.3) (2019-06-28) - -**Note:** Version bump only for package @ionic/utils-fs - - - - - -## [2.0.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-fs@2.0.1...@ionic/utils-fs@2.0.2) (2019-06-10) - -**Note:** Version bump only for package @ionic/utils-fs - - - - - -## [2.0.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-fs@2.0.0...@ionic/utils-fs@2.0.1) (2019-06-05) - -**Note:** Version bump only for package @ionic/utils-fs - - - - - -# [2.0.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-fs@1.2.0...@ionic/utils-fs@2.0.0) (2019-05-29) - - -### chore - -* require Node 8 ([5670e68](https://github.com/ionic-team/ionic-cli/commit/5670e68)) - - -### BREAKING CHANGES - -* A minimum of Node.js 8.9.4 is required. - - - - - - -# [1.2.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-fs@1.1.1...@ionic/utils-fs@1.2.0) (2019-03-06) - - -### Features - -* **fs:** add `isExecutableFile` utility ([53d9626](https://github.com/ionic-team/ionic-cli/commit/53d9626)) -* **fs:** add `pathReadable`, `pathWritable`, and `pathExecutable` ([5412791](https://github.com/ionic-team/ionic-cli/commit/5412791)) - - - - - -## [1.1.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-fs@1.1.0...@ionic/utils-fs@1.1.1) (2019-02-27) - - - - -**Note:** Version bump only for package @ionic/utils-fs - - -# [1.1.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-fs@1.0.0...@ionic/utils-fs@1.1.0) (2019-02-15) - - -### Bug Fixes - -* **walk:** stat symlinks themselves while walking ([92f2cee](https://github.com/ionic-team/ionic-cli/commit/92f2cee)) - - -### Features - -* **fs:** `onError` option for `readdirp` and `getFileTree` ([90bcfcd](https://github.com/ionic-team/ionic-cli/commit/90bcfcd)) -* **fs:** `onFileNode` and `onDirectoryNode` options for `getFileTree` ([1858e96](https://github.com/ionic-team/ionic-cli/commit/1858e96)) -* **fs:** add `getFileTree` for creating file tree object structures ([c14b46b](https://github.com/ionic-team/ionic-cli/commit/c14b46b)) - - - - - -# [1.0.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-fs@0.0.7...@ionic/utils-fs@1.0.0) (2019-01-08) - - -### Bug Fixes - -* rimraf is a full dependency ([962ad94](https://github.com/ionic-team/ionic-cli/commit/962ad94)) - - -### Features - -* switch to fs-extra ([c17d8d8](https://github.com/ionic-team/ionic-cli/commit/c17d8d8)) - - -### BREAKING CHANGES - -* `readDir`, `readDirp`, `readDirSafe` were renamed to be -consistent with fs and fs-extra. `readJsonFile` and `writeJsonFile` have -been renamed to `readJson` and `writeJson` and use the fs-extra version -directly. `isDir` was removed (use stat). `copyFile` and `copyDirectory` -were removed (use `copy`). `removeDirectory` was removed (use `remove`). - - - - - -## [0.0.7](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-fs@0.0.6...@ionic/utils-fs@0.0.7) (2019-01-07) - - - - -**Note:** Version bump only for package @ionic/utils-fs - - -## [0.0.6](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-fs@0.0.5...@ionic/utils-fs@0.0.6) (2018-12-19) - - - - -**Note:** Version bump only for package @ionic/utils-fs - - -## [0.0.5](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-fs@0.0.4...@ionic/utils-fs@0.0.5) (2018-11-20) - - - - -**Note:** Version bump only for package @ionic/utils-fs - - -## [0.0.4](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-fs@0.0.3...@ionic/utils-fs@0.0.4) (2018-10-31) - - - - -**Note:** Version bump only for package @ionic/utils-fs - - -## [0.0.3](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-fs@0.0.2...@ionic/utils-fs@0.0.3) (2018-10-05) - - - - -**Note:** Version bump only for package @ionic/utils-fs - - -## [0.0.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-fs@0.0.1...@ionic/utils-fs@0.0.2) (2018-10-03) - - - - -**Note:** Version bump only for package @ionic/utils-fs - - -## 0.0.1 (2018-09-05) - - - - -**Note:** Version bump only for package @ionic/utils-fs - -# Change Log diff --git a/packages/@ionic/utils-fs/LICENSE b/packages/@ionic/utils-fs/LICENSE deleted file mode 100644 index 7c5808ced6..0000000000 --- a/packages/@ionic/utils-fs/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2017 Drifty Co - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/packages/@ionic/utils-fs/README.md b/packages/@ionic/utils-fs/README.md deleted file mode 100644 index 8370453ed0..0000000000 --- a/packages/@ionic/utils-fs/README.md +++ /dev/null @@ -1 +0,0 @@ -# @ionic/utils-fs diff --git a/packages/@ionic/utils-fs/jest.config.js b/packages/@ionic/utils-fs/jest.config.js deleted file mode 100644 index eace8d6d9d..0000000000 --- a/packages/@ionic/utils-fs/jest.config.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('../../../jest.config.base'); diff --git a/packages/@ionic/utils-fs/lint-staged.config.js b/packages/@ionic/utils-fs/lint-staged.config.js deleted file mode 100644 index 5815a72f0d..0000000000 --- a/packages/@ionic/utils-fs/lint-staged.config.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('../../../lint-staged.config.base'); diff --git a/packages/@ionic/utils-fs/package.json b/packages/@ionic/utils-fs/package.json deleted file mode 100644 index 1ae20fc91f..0000000000 --- a/packages/@ionic/utils-fs/package.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "name": "@ionic/utils-fs", - "version": "3.1.7", - "description": "Filesystem utils for Node", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "homepage": "https://ionicframework.com/", - "author": "Ionic Team (https://ionic.io)", - "license": "MIT", - "engines": { - "node": ">=16.0.0" - }, - "files": [ - "dist/", - "LICENSE", - "README.md" - ], - "repository": { - "type": "git", - "url": "https://github.com/ionic-team/ionic-cli.git" - }, - "bugs": { - "url": "https://github.com/ionic-team/ionic-cli/issues" - }, - "scripts": { - "clean": "rimraf dist", - "lint": "true", - "build": "npm run clean && tsc", - "watch": "tsc -w --preserveWatchOutput", - "test": "jest --maxWorkers=4", - "prepublishOnly": "npm run build" - }, - "dependencies": { - "@types/fs-extra": "^8.0.0", - "debug": "^4.0.0", - "fs-extra": "^9.0.0", - "tslib": "^2.0.1" - }, - "devDependencies": { - "@types/debug": "^4.1.1", - "@types/jest": "^26.0.10", - "@types/node": "~16.0.0", - "jest": "^26.4.2", - "jest-cli": "^26.0.1", - "lint-staged": "^10.0.2", - "rimraf": "^3.0.0", - "ts-jest": "~26.3.0", - "typescript": "~4.8.0" - } -} diff --git a/packages/@ionic/utils-fs/src/__tests__/index.ts b/packages/@ionic/utils-fs/src/__tests__/index.ts deleted file mode 100644 index d2b20f2c0c..0000000000 --- a/packages/@ionic/utils-fs/src/__tests__/index.ts +++ /dev/null @@ -1,257 +0,0 @@ -import * as path from 'path'; - -describe('@ionic/cli-framework', () => { - - describe('utils/fs', () => { - - describe('findBaseDirectory', () => { - - describe('posix', () => { - - const mock_path_posix = path.posix; - jest.resetModules(); - jest.mock('path', () => mock_path_posix); - - const fslib = require('../'); - const safefslib = require('../safe'); - - it('should get undefined with empty input', async () => { - const result = await fslib.findBaseDirectory('', ''); - expect(result).toEqual(undefined); - }); - - it('should return undefined if marker file not found', async () => { - jest.spyOn(safefslib, 'readdir').mockImplementation(async () => ['bar']); - const result = await fslib.findBaseDirectory('/some/dir', 'foo'); - expect(result).toEqual(undefined); - }); - - it('should return path when marker file found in cwd', async () => { - jest.spyOn(safefslib, 'readdir').mockImplementation(async () => ['foo']); - const result = await fslib.findBaseDirectory('/some/dir', 'foo'); - expect(result).toEqual('/some/dir'); - }); - - it('should return path when marker file found in one directory back', async () => { - jest.spyOn(safefslib, 'readdir') - .mockImplementationOnce(async () => ['garbage']) - .mockImplementationOnce(async () => ['dir', 'foo']); - const result = await fslib.findBaseDirectory('/some/dir', 'foo'); - expect(result).toEqual('/some'); - }); - - it('should return path when marker file found in really nested path', async () => { - jest.spyOn(safefslib, 'readdir') - .mockImplementationOnce(async () => ['']) - .mockImplementationOnce(async () => ['nested']) - .mockImplementationOnce(async () => ['really']) - .mockImplementationOnce(async () => ['is']) - .mockImplementationOnce(async () => ['that']) - .mockImplementationOnce(async () => ['dir', 'foo']); - const result = await fslib.findBaseDirectory('/some/dir/that/is/really/nested', 'foo'); - expect(result).toEqual('/some'); - }); - - }); - - describe('windows', () => { - - const mock_path_win32 = path.win32; - jest.resetModules(); - jest.mock('path', () => mock_path_win32); - - const fslib = require('../'); - const safefslib = require('../safe'); - - it('should get undefined with empty input', async () => { - const result = await fslib.findBaseDirectory('', ''); - expect(result).toEqual(undefined); - }); - - it('should return undefined if marker file not found', async () => { - jest.spyOn(safefslib, 'readdir').mockImplementation(async () => ['bar']); - const result = await fslib.findBaseDirectory('C:\\some\\dir', 'foo'); - expect(result).toEqual(undefined); - }); - - it('should return path when marker file found in cwd', async () => { - jest.spyOn(safefslib, 'readdir').mockImplementation(async () => ['foo']); - const result = await fslib.findBaseDirectory('C:\\some\\dir', 'foo'); - expect(result).toEqual('C:\\some\\dir'); - }); - - it('should return path when marker file found in one directory back', async () => { - jest.spyOn(safefslib, 'readdir') - .mockImplementationOnce(async () => ['garbage']) - .mockImplementationOnce(async () => ['dir', 'foo']); - const result = await fslib.findBaseDirectory('C:\\some\\dir', 'foo'); - expect(result).toEqual('C:\\some'); - }); - - it('should return path when marker file found in really nested path', async () => { - jest.spyOn(safefslib, 'readdir') - .mockImplementationOnce(async () => ['']) - .mockImplementationOnce(async () => ['nested']) - .mockImplementationOnce(async () => ['really']) - .mockImplementationOnce(async () => ['is']) - .mockImplementationOnce(async () => ['that']) - .mockImplementationOnce(async () => ['dir', 'foo']); - const result = await fslib.findBaseDirectory('C:\\some\\dir\\\\that\\is\\really\\nested', 'foo'); - expect(result).toEqual('C:\\some'); - }); - - }); - - }); - - describe('compilePaths', () => { - - describe('posix', () => { - - const mock_path_posix = path.posix; - jest.resetModules(); - jest.mock('path', () => mock_path_posix); - - const fslib = require('../'); - - it('should not accept a malformed path', () => { - expect(() => fslib.compilePaths('.')).toThrowError('. is not an absolute path'); - }); - - it('should compile an array of paths working backwards from a base directory', () => { - const result = fslib.compilePaths('/some/dir'); - expect(result).toEqual(['/some/dir', '/some', '/']); - }); - - it('should work for the root directory', () => { - const result = fslib.compilePaths('/'); - expect(result).toEqual(['/']); - }); - - }); - - describe('windows', () => { - - const mock_path_win32 = path.win32; - jest.resetModules(); - jest.mock('path', () => mock_path_win32); - - const fslib = require('../'); - - it('should not accept a malformed path', () => { - expect(() => fslib.compilePaths('.')).toThrowError('. is not an absolute path'); - }); - - it('should compile an array of paths working backwards from a base directory', () => { - const result = fslib.compilePaths('C:\\some\\dir'); - expect(result).toEqual(['C:\\some\\dir', 'C:\\some', 'C:\\']); - }); - - it('should work for the root directory', () => { - const result = fslib.compilePaths('C:\\'); - expect(result).toEqual(['C:\\']); - }); - - }); - - }); - - describe('Walker', () => { - - jest.resetModules(); - const path = require('path'); - const lstatSpy = jest.fn(); - const readdirSpy = jest.fn(); - jest.mock('fs-extra', () => ({ lstat: lstatSpy, readdir: readdirSpy })); - const fslib = require('../'); - - beforeEach(() => { - lstatSpy.mockReset(); - readdirSpy.mockReset(); - }); - - it('should emit data once with path for single file', async done => { - lstatSpy.mockImplementation((p: string, cb: any) => { cb(null, { isDirectory: () => false }); }); - const dataSpy = jest.fn(); - const walker = new fslib.Walker('root'); - walker.on('data', dataSpy); - walker.on('end', () => { - expect(lstatSpy).toHaveBeenCalledTimes(1); - expect(dataSpy).toHaveBeenCalledTimes(1); - expect(dataSpy).toHaveBeenCalledWith({ path: 'root', stats: expect.any(Object) }); - done(); - }); - }); - - it('should emit data for each child', async done => { - const root = 'root'; - const children = ['a', 'b', 'c']; - lstatSpy.mockImplementation((p: string, cb: any) => { cb(null, { isDirectory: () => p === root }); }); - readdirSpy.mockImplementation((p: string, cb: any) => { cb(null, children); }); - const dataSpy = jest.fn(); - const walker = new fslib.Walker(root); - walker.on('data', dataSpy); - walker.on('end', () => { - expect(lstatSpy).toHaveBeenCalledTimes(4); - expect(readdirSpy).toHaveBeenCalledTimes(1); - expect(dataSpy).toHaveBeenCalledTimes(4); - expect(dataSpy).toHaveBeenCalledWith({ path: root, stats: expect.any(Object) }); - for (const child of children.map(c => path.join(root, c))) { - expect(dataSpy).toHaveBeenCalledWith({ path: child, stats: expect.any(Object) }); - } - done(); - }); - }); - - it('should emit data for each child recursively', async done => { - const root = 'root'; // directory - const children = ['foo', 'bar']; // directories - const fooChildren = ['a', 'b', 'c']; // directories - const barChildren = ['1', '2', '3']; // files - lstatSpy.mockImplementation((p: string, cb: any) => { cb(null, { isDirectory: () => p === root || children.map(c => path.join(root, c)).includes(p) || fooChildren.map(c => path.join(root, 'foo', c)).includes(p) }); }); - readdirSpy.mockImplementation((p: string, cb: any) => { cb(null, p === root ? children : (p === path.join(root, 'foo') ? fooChildren : (p === path.join(root, 'bar') ? barChildren : []))) }); - const dataSpy = jest.fn(); - const walker = new fslib.Walker(root); - walker.on('data', dataSpy); - walker.on('end', () => { - expect(lstatSpy).toHaveBeenCalledTimes(9); - expect(readdirSpy).toHaveBeenCalledTimes(6); - expect(dataSpy).toHaveBeenCalledTimes(9); - expect(dataSpy).toHaveBeenCalledWith({ path: root, stats: expect.any(Object) }); - for (const child of children.map(c => path.join(root, c))) { - expect(dataSpy).toHaveBeenCalledWith({ path: child, stats: expect.any(Object) }); - } - for (const child of fooChildren.map(c => path.join(root, 'foo', c))) { - expect(dataSpy).toHaveBeenCalledWith({ path: child, stats: expect.any(Object) }); - } - for (const child of barChildren.map(c => path.join(root, 'bar', c))) { - expect(dataSpy).toHaveBeenCalledWith({ path: child, stats: expect.any(Object) }); - } - done(); - }); - }); - - it('should emit data for each child except for filtered paths', async done => { - const root = 'root'; - const children = ['a', 'b', 'c']; - lstatSpy.mockImplementation((p: string, cb: any) => { cb(null, { isDirectory: () => p === root }); }); - readdirSpy.mockImplementation((p: string, cb: any) => { cb(null, children); }); - const dataSpy = jest.fn(); - const walker = new fslib.Walker(root, { pathFilter: (p: string) => p !== 'b' }); - walker.on('data', dataSpy); - walker.on('end', () => { - expect(lstatSpy).toHaveBeenCalledTimes(3); - expect(readdirSpy).toHaveBeenCalledTimes(1); - expect(dataSpy).toHaveBeenCalledTimes(3); - expect(dataSpy).toHaveBeenCalledWith({ path: root, stats: expect.any(Object) }); - expect(dataSpy).toHaveBeenCalledWith({ path: path.join(root, 'a'), stats: expect.any(Object) }); - expect(dataSpy).toHaveBeenCalledWith({ path: path.join(root, 'c'), stats: expect.any(Object) }); - done(); - }); - }); - - }); - - }); - -}); diff --git a/packages/@ionic/utils-fs/src/index.ts b/packages/@ionic/utils-fs/src/index.ts deleted file mode 100644 index 8cf6076b3a..0000000000 --- a/packages/@ionic/utils-fs/src/index.ts +++ /dev/null @@ -1,403 +0,0 @@ -import * as fs from 'fs-extra'; -import * as os from 'os'; -import * as path from 'path'; -import * as stream from 'stream'; - -import * as safe from './safe'; - -export * from 'fs-extra'; - -export { stat as statSafe, readdir as readdirSafe } from './safe'; - -export interface ReaddirPOptions { - /** - * Filter out items from the walk process from the final result. - * - * @return `true` to keep, otherwise the item is filtered out - */ - readonly filter?: (item: WalkerItem) => boolean; - - /** - * Called whenever an error occurs during the walk process. - * - * If excluded, the function will throw an error when first encountered. - */ - readonly onError?: (err: Error) => void; - readonly walkerOptions?: WalkerOptions; -} - -export async function readdirp(dir: string, { filter, onError, walkerOptions }: ReaddirPOptions = {}): Promise { - return new Promise((resolve, reject) => { - const items: string[] = []; - - let rs: NodeJS.ReadableStream = walk(dir, walkerOptions); - - if (filter) { - rs = rs.pipe(new stream.Transform({ - objectMode: true, - transform(obj: WalkerItem, enc, cb) { - if (!filter || filter(obj)) { - this.push(obj); - } - - cb(); - }, - })); - } - - rs - .on('error', (err: Error) => onError ? onError(err) : reject(err)) - .on('data', (item: WalkerItem) => items.push(item.path)) - .on('end', () => resolve(items)); - }); -} - -export const enum FileType { - FILE = 'file', - DIRECTORY = 'directory', -} - -export interface RegularFileNode { - path: string; - type: FileType.FILE; - parent: FileNode; -} - -export interface DirectoryNode { - path: string; - type: FileType.DIRECTORY; - parent?: FileNode; - children: FileNode[]; -} - -export type FileNode = RegularFileNode | DirectoryNode; - -export interface GetFileTreeOptions { - /** - * Called whenever an error occurs during the walk process. - * - * If excluded, the function will throw an error when first encountered. - */ - readonly onError?: (err: Error) => void; - - /** - * Called whenever a file node is added to the tree. - * - * File nodes can be supplemented by returning a new object from this - * function. - */ - readonly onFileNode?: (node: RegularFileNode) => RegularFileNode & RE; - - /** - * Called whenever a directory node is added to the tree. - * - * Directory nodes can be supplemented by returning a new object from this - * function. - */ - readonly onDirectoryNode?: (node: DirectoryNode) => DirectoryNode & DE; - readonly walkerOptions?: WalkerOptions; -} - -/** - * Compile and return a file tree structure. - * - * This function walks a directory structure recursively, building a nested - * object structure in memory that represents it. When finished, the root - * directory node is returned. - * - * @param dir The root directory from which to compile the file tree - */ -export async function getFileTree(dir: string, { onError, onFileNode = n => n as RegularFileNode & RE, onDirectoryNode = n => n as DirectoryNode & DE, walkerOptions }: GetFileTreeOptions = {}): Promise { - const fileMap = new Map([]); - - const getOrCreateParent = (item: WalkerItem): DirectoryNode & DE => { - const parentPath = path.dirname(item.path); - const parent = fileMap.get(parentPath); - - if (parent && parent.type === FileType.DIRECTORY) { - return parent; - } - - return onDirectoryNode({ path: parentPath, type: FileType.DIRECTORY, children: [] }); - }; - - const createFileNode = (item: WalkerItem, parent: DirectoryNode & DE): RegularFileNode & RE | DirectoryNode & DE => { - const node = { path: item.path, parent }; - - return item.stats.isDirectory() ? - onDirectoryNode({ ...node, type: FileType.DIRECTORY, children: [] }) : - onFileNode({ ...node, type: FileType.FILE }); - }; - - return new Promise((resolve, reject) => { - dir = path.resolve(dir); - const rs = walk(dir, walkerOptions); - - rs - .on('error', err => onError ? onError(err) : reject(err)) - .on('data', item => { - const parent = getOrCreateParent(item); - const node = createFileNode(item, parent); - - parent.children.push(node); - fileMap.set(item.path, node); - fileMap.set(parent.path, parent); - }) - .on('end', () => { - const root = fileMap.get(dir); - - if (!root) { - return reject(new Error('No root node found after walking directory structure.')); - } - - delete root.parent; - resolve(root); - }); - }); -} - -export async function fileToString(filePath: string): Promise { - try { - return await fs.readFile(filePath, { encoding: 'utf8' }); - } catch (e: any) { - if (e.code === 'ENOENT' || e.code === 'ENOTDIR') { - return ''; - } - - throw e; - } -} - -export async function getFileChecksum(filePath: string): Promise { - const crypto = await import('crypto'); - - return new Promise((resolve, reject) => { - const hash = crypto.createHash('md5'); - const input = fs.createReadStream(filePath); - - input.on('error', (err: Error) => { - reject(err); - }); - - hash.once('readable', () => { - const fullChecksum = (hash.read() as Buffer).toString('hex'); - resolve(fullChecksum); - }); - - input.pipe(hash); - }); -} - -/** - * Return true and cached checksums for a file by its path. - * - * Cached checksums are stored as `.md5` files next to the original file. If - * the cache file is missing, the cached checksum is undefined. - * - * @param p The file path - * @return Promise<[true checksum, cached checksum or undefined if cache file missing]> - */ -export async function getFileChecksums(p: string): Promise<[string, string | undefined]> { - return Promise.all([ - getFileChecksum(p), - (async () => { - try { - const md5 = await fs.readFile(`${p}.md5`, { encoding: 'utf8' }); - return md5.trim(); - } catch (e: any) { - if (e.code !== 'ENOENT') { - throw e; - } - } - })(), - ]); -} - -/** - * Store a cache file containing the source file's md5 checksum hash. - * - * @param p The file path - * @param checksum The checksum. If excluded, the checksum is computed - */ -export async function cacheFileChecksum(p: string, checksum?: string): Promise { - const md5 = await getFileChecksum(p); - await fs.writeFile(`${p}.md5`, md5, { encoding: 'utf8' }); -} - -export function writeStreamToFile(stream: NodeJS.ReadableStream, destination: string): Promise { - return new Promise((resolve, reject) => { - const dest = fs.createWriteStream(destination); - stream.pipe(dest); - dest.on('error', reject); - dest.on('finish', resolve); - }); -} - -export async function pathAccessible(filePath: string, mode: number): Promise { - try { - await fs.access(filePath, mode); - } catch (e: any) { - return false; - } - - return true; -} - -export async function pathExists(filePath: string): Promise { - return pathAccessible(filePath, fs.constants.F_OK); -} - -export async function pathReadable(filePath: string): Promise { - return pathAccessible(filePath, fs.constants.R_OK); -} - -export async function pathWritable(filePath: string): Promise { - return pathAccessible(filePath, fs.constants.W_OK); -} - -export async function pathExecutable(filePath: string): Promise { - return pathAccessible(filePath, fs.constants.X_OK); -} - -export async function isExecutableFile(filePath: string): Promise { - const [ stats, executable ] = await (Promise.all([ - safe.stat(filePath), - pathExecutable(filePath), - ])); - - return !!stats && (stats.isFile() || stats.isSymbolicLink()) && executable; -} - -/** - * Find the base directory based on the path given and a marker file to look for. - */ -export async function findBaseDirectory(dir: string, file: string): Promise { - if (!dir || !file) { - return; - } - - for (const d of compilePaths(dir)) { - const results = await safe.readdir(d); - - if (results.includes(file)) { - return d; - } - } -} - -/** - * Generate a random file path within the computer's temporary directory. - * - * @param prefix Optionally provide a filename prefix. - */ -export function tmpfilepath(prefix?: string): string { - const rn = Math.random().toString(16).substring(2, 8); - const p = path.resolve(os.tmpdir(), prefix ? `${prefix}-${rn}` : rn); - - return p; -} - -/** - * Given an absolute system path, compile an array of paths working backwards - * one directory at a time, always ending in the root directory. - * - * For example, `'/some/dir'` => `['/some/dir', '/some', '/']` - * - * @param filePath Absolute system base path. - */ -export function compilePaths(filePath: string): string[] { - filePath = path.normalize(filePath); - - if (!path.isAbsolute(filePath)) { - throw new Error(`${filePath} is not an absolute path`); - } - - const parsed = path.parse(filePath); - - if (filePath === parsed.root) { - return [filePath]; - } - - return filePath - .slice(parsed.root.length) - .split(path.sep) - .map((segment, i, array) => parsed.root + path.join(...array.slice(0, array.length - i))) - .concat(parsed.root); -} - -export interface WalkerItem { - path: string; - stats: fs.Stats; -} - -export interface WalkerOptions { - /** - * Filter out file paths during walk. - * - * As the file tree is walked, this function can be used to exclude files and - * directories from the final result. - * - * It can also be used to tune performance. If a subdirectory is excluded, it - * is not walked. - * - * @param p The file path. - * @return `true` to include file path, otherwise it is excluded - */ - readonly pathFilter?: (p: string) => boolean; -} - -export interface Walker extends stream.Readable { - on(event: 'data', callback: (item: WalkerItem) => void): this; - on(event: string, callback: (...args: any[]) => any): this; -} - -export class Walker extends stream.Readable { - readonly paths: string[] = [this.p]; - - constructor(readonly p: string, readonly options: WalkerOptions = {}) { - super({ objectMode: true }); - } - - _read() { - const p = this.paths.shift(); - const { pathFilter } = this.options; - - if (!p) { - this.push(null); - return; - } - - fs.lstat(p, (err, stats) => { - if (err) { - this.emit('error', err); - return; - } - - const item: WalkerItem = { path: p, stats }; - - if (stats.isDirectory()) { - fs.readdir(p, (err, contents) => { - if (err) { - this.emit('error', err); - return; - } - - let paths = contents.map(file => path.join(p, file)); - - if (pathFilter) { - paths = paths.filter(p => pathFilter(p.substring(this.p.length + 1))); - } - - this.paths.push(...paths); - this.push(item); - }); - } else { - this.push(item); - } - }); - } -} - -export function walk(p: string, options: WalkerOptions = {}): Walker { - return new Walker(p, options); -} diff --git a/packages/@ionic/utils-fs/src/safe.ts b/packages/@ionic/utils-fs/src/safe.ts deleted file mode 100644 index 54b6e6db5b..0000000000 --- a/packages/@ionic/utils-fs/src/safe.ts +++ /dev/null @@ -1,17 +0,0 @@ -import * as fs from 'fs-extra'; - -export async function stat(p: string): Promise { - try { - return await fs.stat(p); - } catch (e: any) { - // ignore - } -} - -export async function readdir(dir: string): Promise { - try { - return await fs.readdir(dir); - } catch (e: any) { - return []; - } -} diff --git a/packages/@ionic/utils-fs/tsconfig.json b/packages/@ionic/utils-fs/tsconfig.json deleted file mode 100644 index 46cccd1031..0000000000 --- a/packages/@ionic/utils-fs/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "types": [ - "node" - ] - }, - "include": [ - "src/**/*.ts" - ], - "exclude": [ - "node_modules", - "src/**/__tests__/*.ts" - ] -} diff --git a/packages/@ionic/utils-network/.gitignore b/packages/@ionic/utils-network/.gitignore deleted file mode 100644 index de4d1f007d..0000000000 --- a/packages/@ionic/utils-network/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -dist -node_modules diff --git a/packages/@ionic/utils-network/.npmrc b/packages/@ionic/utils-network/.npmrc deleted file mode 100644 index 43c97e719a..0000000000 --- a/packages/@ionic/utils-network/.npmrc +++ /dev/null @@ -1 +0,0 @@ -package-lock=false diff --git a/packages/@ionic/utils-network/CHANGELOG.md b/packages/@ionic/utils-network/CHANGELOG.md deleted file mode 100644 index 40478269c6..0000000000 --- a/packages/@ionic/utils-network/CHANGELOG.md +++ /dev/null @@ -1,239 +0,0 @@ -# Change Log - -All notable changes to this project will be documented in this file. -See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -## [2.1.7](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-network@2.1.6...@ionic/utils-network@2.1.7) (2023-12-19) - - -### Bug Fixes - -* **cli:** resolve vm2 security vulnerability ([#5070](https://github.com/ionic-team/ionic-cli/issues/5070)) ([4050419](https://github.com/ionic-team/ionic-cli/commit/4050419bef70fb92e58b0a83cd4b68b48090e596)) - - - - - -## [2.1.6](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-network@2.1.5...@ionic/utils-network@2.1.6) (2023-03-29) - -**Note:** Version bump only for package @ionic/utils-network - - - - - -## [2.1.5](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-network@2.1.4...@ionic/utils-network@2.1.5) (2020-08-28) - -**Note:** Version bump only for package @ionic/utils-network - - - - - -## [2.1.4](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-network@2.1.3...@ionic/utils-network@2.1.4) (2020-08-25) - -**Note:** Version bump only for package @ionic/utils-network - - - - - -## [2.1.3](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-network@2.1.2...@ionic/utils-network@2.1.3) (2020-05-12) - - -### Bug Fixes - -* pin tslib to avoid "Cannot set property pathExists" error ([689e1f0](https://github.com/ionic-team/ionic-cli/commit/689e1f038b907356ef855a067a76d4822e7072a8)) - - - - - -## [2.1.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-network@2.1.1...@ionic/utils-network@2.1.2) (2020-05-06) - -**Note:** Version bump only for package @ionic/utils-network - - - - - -## [2.1.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-network@2.1.0...@ionic/utils-network@2.1.1) (2020-03-03) - -**Note:** Version bump only for package @ionic/utils-network - - - - - -# 2.1.0 (2020-02-11) - - -### Features - -* **start:** add new list starter option ([#4315](https://github.com/ionic-team/ionic-cli/issues/4315)) ([1df44c1](https://github.com/ionic-team/ionic-cli/commit/1df44c1591f37b89f2b672857740edd6cb2aea67)) - - - - - -## [2.0.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-network@2.0.1...@ionic/utils-network@2.0.2) (2020-02-10) - -**Note:** Version bump only for package @ionic/utils-network - - - - - -## [2.0.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-network@2.0.0...@ionic/utils-network@2.0.1) (2020-02-03) - -**Note:** Version bump only for package @ionic/utils-network - - - - - -# [2.0.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-network@1.0.6...@ionic/utils-network@2.0.0) (2020-01-25) - - -### chore - -* require Node 10 ([5a47874](https://github.com/ionic-team/ionic-cli/commit/5a478746c074207b6dc96aa8771f04a606deb1ef)) - - -### BREAKING CHANGES - -* A minimum of Node.js 10.3.0 is required. - - - - - -## [1.0.6](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-network@1.0.5...@ionic/utils-network@1.0.6) (2019-12-05) - -**Note:** Version bump only for package @ionic/utils-network - - - - - -## [1.0.5](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-network@1.0.4...@ionic/utils-network@1.0.5) (2019-09-18) - -**Note:** Version bump only for package @ionic/utils-network - - - - - -## [1.0.4](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-network@1.0.3...@ionic/utils-network@1.0.4) (2019-08-23) - -**Note:** Version bump only for package @ionic/utils-network - - - - - -## [1.0.3](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-network@1.0.2...@ionic/utils-network@1.0.3) (2019-08-14) - -**Note:** Version bump only for package @ionic/utils-network - - - - - -## [1.0.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-network@1.0.1...@ionic/utils-network@1.0.2) (2019-08-07) - -**Note:** Version bump only for package @ionic/utils-network - - - - - -## [1.0.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-network@1.0.0...@ionic/utils-network@1.0.1) (2019-06-05) - -**Note:** Version bump only for package @ionic/utils-network - - - - - -# [1.0.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-network@0.0.8...@ionic/utils-network@1.0.0) (2019-05-29) - - -### chore - -* require Node 8 ([5670e68](https://github.com/ionic-team/ionic-cli/commit/5670e68)) - - -### BREAKING CHANGES - -* A minimum of Node.js 8.9.4 is required. - - - - - - -## [0.0.8](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-network@0.0.7...@ionic/utils-network@0.0.8) (2019-02-27) - - - - -**Note:** Version bump only for package @ionic/utils-network - - -## [0.0.7](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-network@0.0.6...@ionic/utils-network@0.0.7) (2019-02-15) - - - - -**Note:** Version bump only for package @ionic/utils-network - - -## [0.0.6](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-network@0.0.5...@ionic/utils-network@0.0.6) (2019-01-07) - - - - -**Note:** Version bump only for package @ionic/utils-network - - -## [0.0.5](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-network@0.0.4...@ionic/utils-network@0.0.5) (2018-12-19) - - - - -**Note:** Version bump only for package @ionic/utils-network - - -## [0.0.4](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-network@0.0.3...@ionic/utils-network@0.0.4) (2018-10-31) - - - - -**Note:** Version bump only for package @ionic/utils-network - - -## [0.0.3](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-network@0.0.2...@ionic/utils-network@0.0.3) (2018-10-05) - - - - -**Note:** Version bump only for package @ionic/utils-network - - -## [0.0.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-network@0.0.1...@ionic/utils-network@0.0.2) (2018-10-03) - - - - -**Note:** Version bump only for package @ionic/utils-network - - -## 0.0.1 (2018-09-05) - - - - -**Note:** Version bump only for package @ionic/utils-network - -# Change Log diff --git a/packages/@ionic/utils-network/LICENSE b/packages/@ionic/utils-network/LICENSE deleted file mode 100644 index 7c5808ced6..0000000000 --- a/packages/@ionic/utils-network/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2017 Drifty Co - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/packages/@ionic/utils-network/README.md b/packages/@ionic/utils-network/README.md deleted file mode 100644 index 87b5b758b8..0000000000 --- a/packages/@ionic/utils-network/README.md +++ /dev/null @@ -1 +0,0 @@ -# @ionic/utils-network diff --git a/packages/@ionic/utils-network/jest.config.js b/packages/@ionic/utils-network/jest.config.js deleted file mode 100644 index eace8d6d9d..0000000000 --- a/packages/@ionic/utils-network/jest.config.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('../../../jest.config.base'); diff --git a/packages/@ionic/utils-network/lint-staged.config.js b/packages/@ionic/utils-network/lint-staged.config.js deleted file mode 100644 index 5815a72f0d..0000000000 --- a/packages/@ionic/utils-network/lint-staged.config.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('../../../lint-staged.config.base'); diff --git a/packages/@ionic/utils-network/package.json b/packages/@ionic/utils-network/package.json deleted file mode 100644 index e417450863..0000000000 --- a/packages/@ionic/utils-network/package.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "name": "@ionic/utils-network", - "version": "2.1.7", - "description": "Network utils for Node", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "homepage": "https://ionicframework.com/", - "author": "Ionic Team (https://ionic.io)", - "license": "MIT", - "engines": { - "node": ">=16.0.0" - }, - "files": [ - "dist/", - "LICENSE", - "README.md" - ], - "repository": { - "type": "git", - "url": "https://github.com/ionic-team/ionic-cli.git" - }, - "bugs": { - "url": "https://github.com/ionic-team/ionic-cli/issues" - }, - "scripts": { - "clean": "rimraf dist", - "lint": "true", - "build": "npm run clean && tsc", - "watch": "tsc -w --preserveWatchOutput", - "test": "jest --maxWorkers=4", - "prepublishOnly": "npm run build" - }, - "dependencies": { - "debug": "^4.0.0", - "tslib": "^2.0.1" - }, - "devDependencies": { - "@types/debug": "^4.1.1", - "@types/jest": "^26.0.10", - "@types/node": "~16.0.0", - "jest": "^26.4.2", - "jest-cli": "^26.0.1", - "lint-staged": "^10.0.2", - "rimraf": "^3.0.0", - "ts-jest": "~26.3.0", - "typescript": "~4.8.0" - } -} diff --git a/packages/@ionic/utils-network/src/__tests__/index.ts b/packages/@ionic/utils-network/src/__tests__/index.ts deleted file mode 100644 index 4fed64770f..0000000000 --- a/packages/@ionic/utils-network/src/__tests__/index.ts +++ /dev/null @@ -1,50 +0,0 @@ -import osSpy from 'os'; -import { getExternalIPv4Interfaces } from '../'; - -describe('@ionic/utils-network', () => { - - describe('getExternalIPv4Interfaces', () => { - - const networkInterfaces1 = {}; - - const networkInterfaces2 = { - lo: [ - { internal: true, family: 'IPv4' }, - ], - eth0: [ - { internal: false, family: 'IPv6' }, - ], - }; - - const networkInterfaces3 = { - lo: [ - ...networkInterfaces2.lo, - ], - eth0: [ - { internal: false, family: 'IPv4' }, - ...networkInterfaces2.eth0, - ], - }; - - it('should return empty array if no network interfaces', () => { - spyOn(osSpy, 'networkInterfaces').and.callFake(() => networkInterfaces1); - const result = getExternalIPv4Interfaces(); - expect(result).toEqual([]); - }); - - it('should return empty array if unsuitable network interfaces found', () => { - spyOn(osSpy, 'networkInterfaces').and.callFake(() => networkInterfaces2); - const result = getExternalIPv4Interfaces(); - expect(result).toEqual([]); - }); - - it('should find the suitable network interface', () => { - spyOn(osSpy, 'networkInterfaces').and.callFake(() => networkInterfaces3); - const result = getExternalIPv4Interfaces(); - expect(result.length).toEqual(1); - expect(result[0].device).toEqual('eth0'); - }); - - }); - -}); diff --git a/packages/@ionic/utils-network/src/index.ts b/packages/@ionic/utils-network/src/index.ts deleted file mode 100644 index 4211b03f51..0000000000 --- a/packages/@ionic/utils-network/src/index.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { debug as Debug } from 'debug'; -import * as net from 'net'; -import os from 'os'; - -const debug = Debug('ionic:utils-network'); - -export const DEFAULT_ADDRESSES: readonly string[] = getDefaultAddresses(); - -export type NetworkInterface = { device: string; } & os.NetworkInterfaceInfo; - -function getDefaultAddresses(): string[] { - const addresses: string[] = ['0.0.0.0']; - - try { - const networkInterfaces = os.networkInterfaces(); - - for (const device of Object.keys(networkInterfaces)) { - const networkInterface = networkInterfaces[device]; - if (networkInterface) { - addresses.push(...networkInterface.map(i => i.address)); - } - } - } catch (e: any) { - // swallow - } - - return addresses; -} - -export function getExternalIPv4Interfaces(): NetworkInterface[] { - const networkInterfaces = os.networkInterfaces(); - const devices: NetworkInterface[] = []; - - for (const device of Object.keys(networkInterfaces)) { - const networkInterface = networkInterfaces[device]; - if (networkInterface) { - for (const networkAddress of networkInterface) { - if (!networkAddress.internal && networkAddress.family === 'IPv4') { - devices.push({ device, ...networkAddress }); - } - } - } - } - - return devices; -} - -/** - * Attempts to locate a port number starting from `port` and incrementing by 1. - * - * This function looks through all internal network interfaces, attempting - * host/port combinations until it finds an available port on all interfaces. - * - * @param port The port at which to start checking. - */ -export async function findClosestOpenPort(port: number): Promise { - async function t(portToCheck: number): Promise { - if (await isPortAvailable(portToCheck)) { - return portToCheck; - } - - return t(portToCheck + 1); - } - - return t(port); -} - -/** - * Checks whether a port is open or closed. - * - * This function looks through all internal network interfaces, checking - * whether all host/port combinations are open. If one or more is not, the port - * is not available. - */ -export async function isPortAvailable(port: number): Promise { - let available = true; - - for (const address of DEFAULT_ADDRESSES) { - try { - debug('checking for open port on %s:%d', address, port); - available = await isPortAvailableForHost(address, port); - - if (!available) { - return false; - } - } catch (e: any) { - debug('error while checking %s:%d: %o', address, port, e); - } - } - - return available; -} - -export function isPortAvailableForHost(host: string, port: number): Promise { - return new Promise((resolve, reject) => { - const tester = net.createServer() - .once('error', (err: any) => { - if (err.code === 'EADDRINUSE') { - resolve(false); // host/port in use - } else { - reject(err); - } - }) - .once('listening', () => { - tester.once('close', () => { - resolve(true); // found available host/port - }) - .close(); - }) - .on('error', (err: any) => { - reject(err); - }) - .listen(port, host); - }); -} - -/** - * Continuously attempt TCP connections. - * - * By default, this function will only ever resolve once a host is connectable. - * This behavior can be changed with the `timeout` option, which resolves with - * `false` if the timeout is reached. - * - * @param host The host to connect to. - * @param port The port to connect to. - * @param options.timeout Optionally define a timeout, in milliseconds. - */ -export async function isHostConnectable(host: string, port: number, { timeout }: { timeout?: number; } = {}): Promise { - const tryConnect = async () => { - return new Promise((resolve, reject) => { - if (host === '0.0.0.0') { - host = '127.0.0.1'; - } - - const sock = net.connect({ port, host }); - - sock.on('connect', () => { - sock.destroy(); - resolve(true); - }); - - sock.on('error', err => { - reject(err); - }); - }); - }; - - return new Promise(async resolve => { - let timer: NodeJS.Timer | undefined; - let resolved = false; - - if (timeout) { - timer = setTimeout(() => { - debug('Timeout of %dms reached while waiting for host connectivity', timeout); - resolve(false); - resolved = true; - }, timeout); - - timer.unref(); - } - - while (!resolved) { - try { - await tryConnect(); - - if (timer) { - clearTimeout(timer); - } - - resolve(true); - resolved = true; - } catch (e: any) { - // try again - } - } - }); -} diff --git a/packages/@ionic/utils-network/tsconfig.json b/packages/@ionic/utils-network/tsconfig.json deleted file mode 100644 index 46cccd1031..0000000000 --- a/packages/@ionic/utils-network/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "types": [ - "node" - ] - }, - "include": [ - "src/**/*.ts" - ], - "exclude": [ - "node_modules", - "src/**/__tests__/*.ts" - ] -} diff --git a/packages/@ionic/utils-object/.gitignore b/packages/@ionic/utils-object/.gitignore deleted file mode 100644 index de4d1f007d..0000000000 --- a/packages/@ionic/utils-object/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -dist -node_modules diff --git a/packages/@ionic/utils-object/.npmrc b/packages/@ionic/utils-object/.npmrc deleted file mode 100644 index 43c97e719a..0000000000 --- a/packages/@ionic/utils-object/.npmrc +++ /dev/null @@ -1 +0,0 @@ -package-lock=false diff --git a/packages/@ionic/utils-object/CHANGELOG.md b/packages/@ionic/utils-object/CHANGELOG.md deleted file mode 100644 index c93d5093ca..0000000000 --- a/packages/@ionic/utils-object/CHANGELOG.md +++ /dev/null @@ -1,170 +0,0 @@ -# Change Log - -All notable changes to this project will be documented in this file. -See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -## [2.1.6](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-object@2.1.5...@ionic/utils-object@2.1.6) (2023-03-29) - -**Note:** Version bump only for package @ionic/utils-object - - - - - -## [2.1.5](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-object@2.1.4...@ionic/utils-object@2.1.5) (2020-08-28) - -**Note:** Version bump only for package @ionic/utils-object - - - - - -## [2.1.4](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-object@2.1.3...@ionic/utils-object@2.1.4) (2020-08-25) - -**Note:** Version bump only for package @ionic/utils-object - - - - - -## [2.1.3](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-object@2.1.2...@ionic/utils-object@2.1.3) (2020-05-12) - - -### Bug Fixes - -* pin tslib to avoid "Cannot set property pathExists" error ([689e1f0](https://github.com/ionic-team/ionic-cli/commit/689e1f038b907356ef855a067a76d4822e7072a8)) - - - - - -## [2.1.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-object@2.1.1...@ionic/utils-object@2.1.2) (2020-05-06) - -**Note:** Version bump only for package @ionic/utils-object - - - - - -## [2.1.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-object@2.1.0...@ionic/utils-object@2.1.1) (2020-03-03) - -**Note:** Version bump only for package @ionic/utils-object - - - - - -# 2.1.0 (2020-02-11) - - -### Features - -* **start:** add new list starter option ([#4315](https://github.com/ionic-team/ionic-cli/issues/4315)) ([1df44c1](https://github.com/ionic-team/ionic-cli/commit/1df44c1591f37b89f2b672857740edd6cb2aea67)) - - - - - -## [2.0.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-object@2.0.1...@ionic/utils-object@2.0.2) (2020-02-10) - -**Note:** Version bump only for package @ionic/utils-object - - - - - -## [2.0.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-object@2.0.0...@ionic/utils-object@2.0.1) (2020-02-03) - -**Note:** Version bump only for package @ionic/utils-object - - - - - -# [2.0.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-object@1.0.6...@ionic/utils-object@2.0.0) (2020-01-25) - - -### chore - -* require Node 10 ([5a47874](https://github.com/ionic-team/ionic-cli/commit/5a478746c074207b6dc96aa8771f04a606deb1ef)) - - -### BREAKING CHANGES - -* A minimum of Node.js 10.3.0 is required. - - - - - -## [1.0.6](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-object@1.0.5...@ionic/utils-object@1.0.6) (2019-12-05) - -**Note:** Version bump only for package @ionic/utils-object - - - - - -## [1.0.5](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-object@1.0.4...@ionic/utils-object@1.0.5) (2019-09-18) - -**Note:** Version bump only for package @ionic/utils-object - - - - - -## [1.0.4](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-object@1.0.3...@ionic/utils-object@1.0.4) (2019-08-23) - -**Note:** Version bump only for package @ionic/utils-object - - - - - -## [1.0.3](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-object@1.0.2...@ionic/utils-object@1.0.3) (2019-08-14) - -**Note:** Version bump only for package @ionic/utils-object - - - - - -## [1.0.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-object@1.0.1...@ionic/utils-object@1.0.2) (2019-08-07) - -**Note:** Version bump only for package @ionic/utils-object - - - - - -## [1.0.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-object@1.0.0...@ionic/utils-object@1.0.1) (2019-06-05) - -**Note:** Version bump only for package @ionic/utils-object - - - - - -# [1.0.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-object@0.0.1...@ionic/utils-object@1.0.0) (2019-05-29) - - -### chore - -* require Node 8 ([5670e68](https://github.com/ionic-team/ionic-cli/commit/5670e68)) - - -### BREAKING CHANGES - -* A minimum of Node.js 8.9.4 is required. - - - - - - -## 0.0.1 (2019-02-27) - - - - -**Note:** Version bump only for package @ionic/utils-object diff --git a/packages/@ionic/utils-object/LICENSE b/packages/@ionic/utils-object/LICENSE deleted file mode 100644 index 7c5808ced6..0000000000 --- a/packages/@ionic/utils-object/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2017 Drifty Co - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/packages/@ionic/utils-object/README.md b/packages/@ionic/utils-object/README.md deleted file mode 100644 index 08999f963d..0000000000 --- a/packages/@ionic/utils-object/README.md +++ /dev/null @@ -1 +0,0 @@ -# @ionic/utils-object diff --git a/packages/@ionic/utils-object/jest.config.js b/packages/@ionic/utils-object/jest.config.js deleted file mode 100644 index eace8d6d9d..0000000000 --- a/packages/@ionic/utils-object/jest.config.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('../../../jest.config.base'); diff --git a/packages/@ionic/utils-object/lint-staged.config.js b/packages/@ionic/utils-object/lint-staged.config.js deleted file mode 100644 index 5815a72f0d..0000000000 --- a/packages/@ionic/utils-object/lint-staged.config.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('../../../lint-staged.config.base'); diff --git a/packages/@ionic/utils-object/package.json b/packages/@ionic/utils-object/package.json deleted file mode 100644 index 433ce322f5..0000000000 --- a/packages/@ionic/utils-object/package.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "name": "@ionic/utils-object", - "version": "2.1.6", - "description": "Object utils", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "homepage": "https://ionicframework.com/", - "author": "Ionic Team (https://ionic.io)", - "license": "MIT", - "engines": { - "node": ">=16.0.0" - }, - "files": [ - "dist/", - "LICENSE", - "README.md" - ], - "repository": { - "type": "git", - "url": "https://github.com/ionic-team/ionic-cli.git" - }, - "bugs": { - "url": "https://github.com/ionic-team/ionic-cli/issues" - }, - "scripts": { - "clean": "rimraf dist", - "lint": "true", - "build": "npm run clean && tsc", - "watch": "tsc -w --preserveWatchOutput", - "test": "jest --maxWorkers=4", - "prepublishOnly": "npm run build" - }, - "dependencies": { - "debug": "^4.0.0", - "tslib": "^2.0.1" - }, - "devDependencies": { - "@types/debug": "^4.1.1", - "@types/jest": "^26.0.10", - "@types/node": "~16.0.0", - "jest": "^26.4.2", - "jest-cli": "^26.0.1", - "lint-staged": "^10.0.2", - "rimraf": "^3.0.0", - "ts-jest": "~26.3.0", - "typescript": "~4.8.0" - } -} diff --git a/packages/@ionic/utils-object/src/__tests__/index.ts b/packages/@ionic/utils-object/src/__tests__/index.ts deleted file mode 100644 index 66a903673c..0000000000 --- a/packages/@ionic/utils-object/src/__tests__/index.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { AliasedMap, createCaseInsensitiveObject } from '../'; - -describe('@ionic/utils-object', () => { - - describe('createCaseInsensitiveObject', () => { - - it('should return undefined for a bad key', () => { - const o = createCaseInsensitiveObject(); - expect(o['bad key']).toBeUndefined(); - }); - - it('should return value for a good key', () => { - const o = createCaseInsensitiveObject(); - o['good key'] = 'val'; - expect(o['good key']).toEqual('val'); - }); - - it('should return value for a good key case insensitive', () => { - const o = createCaseInsensitiveObject(); - o['good key'] = 'val'; - expect(o['GOOD key']).toEqual('val'); - }); - - it('should have correct number of keys', () => { - const o = createCaseInsensitiveObject(); - o['good KEY'] = 'val'; - expect(Object.keys(o).length).toEqual(1); - }); - - it('should delete keys case-insensitively', () => { - const o = createCaseInsensitiveObject(); - o['good KEY'] = 'val'; - expect(o['GOOD key']).toEqual('val'); - expect(delete o['GooD kEy']).toBe(true); - expect(delete o['BaD kEy']).toBe(true); - expect(Object.keys(o).length).toEqual(0); - }); - - it('should check for existence of keys case-insensitively', () => { - const o = createCaseInsensitiveObject(); - o['good KEY'] = 'val'; - expect('GOOD key' in o).toBe(true); - expect('BAD key' in o).toBe(false); - }); - - it('should work for property keys that are symbols', () => { - const o = createCaseInsensitiveObject(); - const s: any = Symbol('key'); - o[s] = 'val'; - expect(o[s]).toEqual('val'); - }); - - it('should work for property keys that are numbers', () => { - const o = createCaseInsensitiveObject(); - o[10] = 'val'; - expect(o[10]).toEqual('val'); - }); - - it('should work with object spread', () => { - const o = createCaseInsensitiveObject(); - o['GOOD key'] = 'val1'; - o['good KEY'] = 'val2'; - expect({ ...o }).toEqual({ 'good key': 'val2' }); - }); - - }); - - describe('AliasedMap', () => { - - class MyAliasedMap extends AliasedMap {} - - describe('getAliases', () => { - - it('should get empty alias map for empty command map', () => { - const m = new MyAliasedMap([]); - const aliasmap = m.getAliases(); - expect(aliasmap.size).toEqual(0); - }); - - it('should get empty alias map for command map with no aliases', () => { - const m = new MyAliasedMap([['foo', { foo: 'bar' }], ['bar', { foo: 'bar' }]]); - const aliasmap = m.getAliases(); - expect(aliasmap.size).toEqual(0); - }); - - it('should get alias map for command map with aliases', () => { - const m = new MyAliasedMap([['foo', { foo: 'bar' }], ['f', 'foo'], ['fo', 'foo']]); - const aliasmap = m.getAliases(); - expect(aliasmap.size).toEqual(1); - expect(aliasmap.get('foo')).toEqual(['f', 'fo']); - }); - - it('should get alias map for command map without resolved command', () => { - const m = new MyAliasedMap([['f', 'foo'], ['fo', 'foo']]); - const aliasmap = m.getAliases(); - expect(aliasmap.size).toEqual(1); - expect(aliasmap.get('foo')).toEqual(['f', 'fo']); - }); - - }); - - describe('resolveAlias', () => { - - it('should return undefined for unknown command', () => { - const m = new MyAliasedMap([]); - expect(m.resolveAlias('bar')).toBeUndefined(); - }); - - it('should return command when immediately found', async () => { - const obj = { foo: 'bar' }; - const m = new MyAliasedMap([['foo', obj]]); - const result = m.resolveAlias('foo'); - expect(result).toBe(obj); - }); - - }); - - describe('keysWithoutAliases', () => { - - it('should return empty array', () => { - const m = new MyAliasedMap([]); - expect(m.keysWithoutAliases()).toEqual([]); - }); - - it('should return non-aliased keys for a plain map', async () => { - const m = new MyAliasedMap([['foo', { foo: 'bar' }], ['bar', { foo: 'bar' }]]); - const result = m.keysWithoutAliases(); - expect(result).toEqual(['foo', 'bar']); - }); - - it('should return non-aliased keys for a map with aliases', async () => { - const m = new MyAliasedMap([['foo', { foo: 'bar' }], ['bar', { foo: 'bar' }], ['f', 'foo'], ['b', 'bar']]); - const result = m.keysWithoutAliases(); - expect(result).toEqual(['foo', 'bar']); - }); - - it('should return non-aliased keys for a map with invalid aliases', async () => { - const m = new MyAliasedMap([['foo', { foo: 'bar' }], ['f', 'foo'], ['g', 'garbage']]); - const result = m.keysWithoutAliases(); - expect(result).toEqual(['foo']); - }); - - }); - - }); - -}); diff --git a/packages/@ionic/utils-object/src/index.ts b/packages/@ionic/utils-object/src/index.ts deleted file mode 100644 index 646613e991..0000000000 --- a/packages/@ionic/utils-object/src/index.ts +++ /dev/null @@ -1,56 +0,0 @@ -export function createCaseInsensitiveObject() { - return new Proxy<{ [key: string]: T; }>({}, CaseInsensitiveProxyHandler); -} - -export const CaseInsensitiveProxyHandler: ProxyHandler = { - has: (obj, prop) => { - return conformPropertyKey(prop) in obj; - }, - get: (obj, prop) => { - return obj[conformPropertyKey(prop)]; - }, - set: (obj, prop, value) => { - obj[conformPropertyKey(prop)] = value; - return true; - }, - deleteProperty: (obj, prop) => { - return delete obj[conformPropertyKey(prop)]; - }, -}; - -const conformPropertyKey = (prop: PropertyKey) => typeof prop === 'string' ? prop.toLowerCase() : prop; - -export type AliasedMapKey = string | symbol; - -export class AliasedMap extends Map { - getAliases(): Map { - const aliasmap = new Map(); - - // TODO: waiting for https://github.com/Microsoft/TypeScript/issues/18562 - const aliases = [...this.entries()].filter(([, v]) => typeof v === 'string' || typeof v === 'symbol') as [AliasedMapKey, AliasedMapKey][]; - - aliases.forEach(([alias, cmd]) => { - const cmdaliases = aliasmap.get(cmd) || []; - cmdaliases.push(alias); - aliasmap.set(cmd, cmdaliases); - }); - - return aliasmap; - } - - resolveAlias(key: AliasedMapKey | K): V | undefined { - const r = this.get(key); - - if (typeof r !== 'string' && typeof r !== 'symbol') { - return r; - } - - return this.resolveAlias(r); - } - - keysWithoutAliases(): K[] { - return [...this.entries()] - .filter((entry): entry is [K, V] => typeof entry[1] !== 'string' && typeof entry[1] !== 'symbol') - .map(([k, v]) => k); - } -} diff --git a/packages/@ionic/utils-object/tsconfig.json b/packages/@ionic/utils-object/tsconfig.json deleted file mode 100644 index 46cccd1031..0000000000 --- a/packages/@ionic/utils-object/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "types": [ - "node" - ] - }, - "include": [ - "src/**/*.ts" - ], - "exclude": [ - "node_modules", - "src/**/__tests__/*.ts" - ] -} diff --git a/packages/@ionic/utils-process/.gitignore b/packages/@ionic/utils-process/.gitignore deleted file mode 100644 index de4d1f007d..0000000000 --- a/packages/@ionic/utils-process/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -dist -node_modules diff --git a/packages/@ionic/utils-process/.npmrc b/packages/@ionic/utils-process/.npmrc deleted file mode 100644 index 43c97e719a..0000000000 --- a/packages/@ionic/utils-process/.npmrc +++ /dev/null @@ -1 +0,0 @@ -package-lock=false diff --git a/packages/@ionic/utils-process/CHANGELOG.md b/packages/@ionic/utils-process/CHANGELOG.md deleted file mode 100644 index ad449ab4a7..0000000000 --- a/packages/@ionic/utils-process/CHANGELOG.md +++ /dev/null @@ -1,264 +0,0 @@ -# Change Log - -All notable changes to this project will be documented in this file. -See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -## [2.1.12](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-process@2.1.11...@ionic/utils-process@2.1.12) (2023-12-19) - - -### Bug Fixes - -* **cli:** resolve vm2 security vulnerability ([#5070](https://github.com/ionic-team/ionic-cli/issues/5070)) ([4050419](https://github.com/ionic-team/ionic-cli/commit/4050419bef70fb92e58b0a83cd4b68b48090e596)) - - - - - -## [2.1.11](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-process@2.1.10...@ionic/utils-process@2.1.11) (2023-03-29) - -**Note:** Version bump only for package @ionic/utils-process - - - - - -## [2.1.10](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-process@2.1.9...@ionic/utils-process@2.1.10) (2022-06-16) - -**Note:** Version bump only for package @ionic/utils-process - - - - - -## [2.1.9](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-process@2.1.8...@ionic/utils-process@2.1.9) (2022-05-09) - -**Note:** Version bump only for package @ionic/utils-process - - - - - -## [2.1.8](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-process@2.1.7...@ionic/utils-process@2.1.8) (2020-09-29) - -**Note:** Version bump only for package @ionic/utils-process - - - - - -## [2.1.7](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-process@2.1.6...@ionic/utils-process@2.1.7) (2020-09-24) - -**Note:** Version bump only for package @ionic/utils-process - - - - - -## [2.1.6](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-process@2.1.5...@ionic/utils-process@2.1.6) (2020-08-28) - -**Note:** Version bump only for package @ionic/utils-process - - - - - -## [2.1.5](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-process@2.1.4...@ionic/utils-process@2.1.5) (2020-08-27) - - -### Bug Fixes - -* **onExit:** switch to signal-exit implementation ([95521e2](https://github.com/ionic-team/ionic-cli/commit/95521e2d4d47795020dc20b53bd4ce99e87e4f73)) - - - - - -## [2.1.4](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-process@2.1.3...@ionic/utils-process@2.1.4) (2020-08-25) - -**Note:** Version bump only for package @ionic/utils-process - - - - - -## [2.1.3](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-process@2.1.2...@ionic/utils-process@2.1.3) (2020-05-12) - - -### Bug Fixes - -* pin tslib to avoid "Cannot set property pathExists" error ([689e1f0](https://github.com/ionic-team/ionic-cli/commit/689e1f038b907356ef855a067a76d4822e7072a8)) - - - - - -## [2.1.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-process@2.1.1...@ionic/utils-process@2.1.2) (2020-05-06) - -**Note:** Version bump only for package @ionic/utils-process - - - - - -## [2.1.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-process@2.1.0...@ionic/utils-process@2.1.1) (2020-03-03) - -**Note:** Version bump only for package @ionic/utils-process - - - - - -# 2.1.0 (2020-02-11) - - -### Features - -* **start:** add new list starter option ([#4315](https://github.com/ionic-team/ionic-cli/issues/4315)) ([1df44c1](https://github.com/ionic-team/ionic-cli/commit/1df44c1591f37b89f2b672857740edd6cb2aea67)) - - - - - -## [2.0.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-process@2.0.1...@ionic/utils-process@2.0.2) (2020-02-10) - -**Note:** Version bump only for package @ionic/utils-process - - - - - -## [2.0.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-process@2.0.0...@ionic/utils-process@2.0.1) (2020-02-03) - -**Note:** Version bump only for package @ionic/utils-process - - - - - -# [2.0.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-process@1.0.9...@ionic/utils-process@2.0.0) (2020-01-25) - - -### chore - -* require Node 10 ([5a47874](https://github.com/ionic-team/ionic-cli/commit/5a478746c074207b6dc96aa8771f04a606deb1ef)) - - -### BREAKING CHANGES - -* A minimum of Node.js 10.3.0 is required. - - - - - -## [1.0.9](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-process@1.0.8...@ionic/utils-process@1.0.9) (2020-01-13) - -**Note:** Version bump only for package @ionic/utils-process - - - - - -## [1.0.8](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-process@1.0.7...@ionic/utils-process@1.0.8) (2019-12-10) - -**Note:** Version bump only for package @ionic/utils-process - - - - - -## [1.0.7](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-process@1.0.6...@ionic/utils-process@1.0.7) (2019-12-05) - -**Note:** Version bump only for package @ionic/utils-process - - - - - -## [1.0.6](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-process@1.0.5...@ionic/utils-process@1.0.6) (2019-11-21) - -**Note:** Version bump only for package @ionic/utils-process - - - - - -## [1.0.5](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-process@1.0.4...@ionic/utils-process@1.0.5) (2019-09-18) - -**Note:** Version bump only for package @ionic/utils-process - - - - - -## [1.0.4](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-process@1.0.3...@ionic/utils-process@1.0.4) (2019-08-23) - -**Note:** Version bump only for package @ionic/utils-process - - - - - -## [1.0.3](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-process@1.0.2...@ionic/utils-process@1.0.3) (2019-08-14) - -**Note:** Version bump only for package @ionic/utils-process - - - - - -## [1.0.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-process@1.0.1...@ionic/utils-process@1.0.2) (2019-08-07) - -**Note:** Version bump only for package @ionic/utils-process - - - - - -## [1.0.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-process@1.0.0...@ionic/utils-process@1.0.1) (2019-06-05) - -**Note:** Version bump only for package @ionic/utils-process - - - - - -# [1.0.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-process@0.1.0...@ionic/utils-process@1.0.0) (2019-05-29) - - -### Bug Fixes - -* **process:** re-raise captured signals ([d82bfde](https://github.com/ionic-team/ionic-cli/commit/d82bfde)) - - -### chore - -* require Node 8 ([5670e68](https://github.com/ionic-team/ionic-cli/commit/5670e68)) - - -### BREAKING CHANGES - -* A minimum of Node.js 8.9.4 is required. - - - - - - -# [0.1.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-process@0.0.1...@ionic/utils-process@0.1.0) (2019-03-06) - - -### Features - -* **process:** add `getPathParts` to split path into parts ([7dfbbc2](https://github.com/ionic-team/ionic-cli/commit/7dfbbc2)) - - - - - -## 0.0.1 (2019-02-27) - - - - -**Note:** Version bump only for package @ionic/utils-process diff --git a/packages/@ionic/utils-process/LICENSE b/packages/@ionic/utils-process/LICENSE deleted file mode 100644 index 7c5808ced6..0000000000 --- a/packages/@ionic/utils-process/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2017 Drifty Co - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/packages/@ionic/utils-process/README.md b/packages/@ionic/utils-process/README.md deleted file mode 100644 index b4f5a77641..0000000000 --- a/packages/@ionic/utils-process/README.md +++ /dev/null @@ -1 +0,0 @@ -# @ionic/utils-process diff --git a/packages/@ionic/utils-process/jest.config.js b/packages/@ionic/utils-process/jest.config.js deleted file mode 100644 index eace8d6d9d..0000000000 --- a/packages/@ionic/utils-process/jest.config.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('../../../jest.config.base'); diff --git a/packages/@ionic/utils-process/lint-staged.config.js b/packages/@ionic/utils-process/lint-staged.config.js deleted file mode 100644 index 5815a72f0d..0000000000 --- a/packages/@ionic/utils-process/lint-staged.config.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('../../../lint-staged.config.base'); diff --git a/packages/@ionic/utils-process/package.json b/packages/@ionic/utils-process/package.json deleted file mode 100644 index 872f9af426..0000000000 --- a/packages/@ionic/utils-process/package.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "name": "@ionic/utils-process", - "version": "2.1.12", - "description": "Process utils for NodeJS", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "homepage": "https://ionicframework.com/", - "author": "Ionic Team (https://ionic.io)", - "license": "MIT", - "engines": { - "node": ">=16.0.0" - }, - "files": [ - "dist/", - "LICENSE", - "README.md" - ], - "repository": { - "type": "git", - "url": "https://github.com/ionic-team/ionic-cli.git" - }, - "bugs": { - "url": "https://github.com/ionic-team/ionic-cli/issues" - }, - "scripts": { - "clean": "rimraf dist", - "lint": "true", - "build": "npm run clean && tsc", - "watch": "tsc -w --preserveWatchOutput", - "test": "jest --maxWorkers=4", - "prepublishOnly": "npm run build" - }, - "dependencies": { - "@ionic/utils-object": "2.1.6", - "@ionic/utils-terminal": "2.3.5", - "debug": "^4.0.0", - "signal-exit": "^3.0.3", - "tree-kill": "^1.2.2", - "tslib": "^2.0.1" - }, - "devDependencies": { - "@types/debug": "^4.1.1", - "@types/jest": "^26.0.10", - "@types/node": "~16.0.0", - "@types/signal-exit": "^3.0.0", - "jest": "^26.4.2", - "jest-cli": "^26.0.1", - "lint-staged": "^10.0.2", - "rimraf": "^3.0.0", - "ts-jest": "~26.3.0", - "typescript": "~4.8.0" - } -} diff --git a/packages/@ionic/utils-process/src/__tests__/index.ts b/packages/@ionic/utils-process/src/__tests__/index.ts deleted file mode 100644 index 30476966c3..0000000000 --- a/packages/@ionic/utils-process/src/__tests__/index.ts +++ /dev/null @@ -1,55 +0,0 @@ -describe('@ionic/utils-process', () => { - - describe('createProcessEnv', () => { - - describe('non-windows', () => { - - const mock_terminal = { TERMINAL_INFO: { windows: false } }; - jest.resetModules(); - jest.mock('@ionic/utils-terminal', () => mock_terminal); - const proc = require('../'); - - it('should be case sensitive', () => { - const o = proc.createProcessEnv(); - o['asdf'] = 'val1'; - o['ASDF'] = 'val2'; - expect(o).toEqual({ 'asdf': 'val1', 'ASDF': 'val2' }); - }); - - it('should use sources and be case sensitive', () => { - const o = proc.createProcessEnv({ 'hello': 'you' }, { 'hello': 'world' }); - o['asdf'] = 'val1'; - o['ASDF'] = 'val2'; - expect(o['hello']).toEqual('world'); - expect(o).toEqual({ 'asdf': 'val1', 'ASDF': 'val2', 'hello': 'world' }); - }); - - }); - - describe('windows', () => { - - const mock_terminal = { TERMINAL_INFO: { windows: true } }; - jest.resetModules(); - jest.mock('@ionic/utils-terminal', () => mock_terminal); - const proc = require('../'); - - it('should be case insensitive', () => { - const o = proc.createProcessEnv(); - o['asdf'] = 'val1'; - o['ASDF'] = 'val2'; - expect(o).toEqual({ 'asdf': 'val2' }); - }); - - it('should use sources and be case insensitive', () => { - const o = proc.createProcessEnv({ 'HELLO': 'YOU' }, { 'hello': 'world' }); - o['asdf'] = 'val1'; - o['ASDF'] = 'val2'; - expect(o['hello']).toEqual('world'); - expect(o).toEqual({ 'asdf': 'val2', 'hello': 'world' }); - }); - - }); - - }); - -}); diff --git a/packages/@ionic/utils-process/src/index.ts b/packages/@ionic/utils-process/src/index.ts deleted file mode 100644 index 51ae15768d..0000000000 --- a/packages/@ionic/utils-process/src/index.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { createCaseInsensitiveObject } from '@ionic/utils-object'; -import { TERMINAL_INFO } from '@ionic/utils-terminal'; -import { debug as Debug } from 'debug'; -import * as pathlib from 'path'; -import onSignalExit from 'signal-exit'; -import treeKill from 'tree-kill'; - -const debug = Debug('ionic:utils-process'); - -export const ERROR_TIMEOUT_REACHED = new Error('TIMEOUT_REACHED'); - -export function killProcessTree(pid: number, signal: string | number = 'SIGTERM'): Promise { - return new Promise((resolve, reject) => { - treeKill(pid, signal, err => { - if (err) { - debug('error while killing process tree for %d: %O', pid, err); - return reject(err); - } - - resolve(); - }); - }); -} - -/** - * Creates an alternative implementation of `process.env` object. - * - * On a Windows shell, `process.env` is a magic object that offers - * case-insensitive environment variable access. On other platforms, case - * sensitivity matters. This method creates an empty "`process.env`" object - * type that works for all platforms. - */ -export function createProcessEnv(...sources: { [key: string]: string | undefined; }[]): NodeJS.ProcessEnv { - return Object.assign(TERMINAL_INFO.windows ? createCaseInsensitiveObject() : {}, ...sources); -} - -/** - * Split a PATH string into path parts. - */ -export function getPathParts(envpath = process.env.PATH || ''): string[] { - return envpath.split(pathlib.delimiter); -} - -/** - * Resolves when the given amount of milliseconds has passed. - */ -export async function sleep(ms: number): Promise { - return new Promise(resolve => { - setTimeout(resolve, ms); - }); -} - -/** - * Resolves when a given predicate is true or a timeout is reached. - * - * Configure `interval` to set how often the `predicate` is called. - * - * By default, `timeout` is Infinity. If given a value (in ms), and that - * timeout value is reached, this function will reject with - * the `ERROR_TIMEOUT_REACHED` error. - */ -export async function sleepUntil(predicate: () => boolean, { interval = 30, timeout = Infinity }: { interval?: number; timeout?: number; }): Promise { - let ms = 0; - - while (!predicate()) { - await sleep(interval); - ms += interval; - - if (ms > timeout) { - throw ERROR_TIMEOUT_REACHED; - } - } -} - -/** - * Never resolves and keeps Node running. - */ -export async function sleepForever(): Promise { - return new Promise(() => { - setInterval(() => { /* do nothing */ }, 1000); - }); -} - -/** - * Register a synchronous function to be called once the process exits. - */ -export function onExit(fn: () => void) { - onSignalExit(() => { - debug('onExit: process.exit/normal shutdown'); - fn(); - }); -} - -export type ExitFn = () => Promise; - -const exitFns = new Set(); - -/** - * Register an asynchronous function to be called when the process wants to - * exit. - * - * A handler will be registered for the 'SIGINT', 'SIGTERM', 'SIGHUP', - * 'SIGBREAK' signals. If any of the signal events is emitted, `fn` will be - * called exactly once, awaited upon, and then the process will exit once all - * registered functions are resolved. - */ -export function onBeforeExit(fn: ExitFn): void { - exitFns.add(fn); -} - -/** - * Remove a function that was registered with `onBeforeExit`. - */ -export function offBeforeExit(fn: ExitFn): void { - exitFns.delete(fn); -} - -type BeforeExitSignal = 'SIGINT' | 'SIGTERM' | 'SIGHUP' | 'SIGBREAK'; - -const once = (fn: () => Promise) => { - let called = false; - - return async (): Promise => { - if (!called) { - await fn(); - called = true; - } - }; -}; - -const beforeExitHandlerWrapper = (signal: 'process.exit' | BeforeExitSignal) => once(async () => { - debug('onBeforeExit handler: %O received', signal); - debug('onBeforeExit handler: running %O functions', exitFns.size); - - await Promise.all([...exitFns.values()].map(async fn => { - try { - await fn(); - } catch (e: any) { - debug('onBeforeExit handler: error from function: %O', e); - } - })); - - if (signal !== 'process.exit') { - debug('onBeforeExit handler: killing self (exit code %O, signal %O)', process.exitCode ? process.exitCode : 0, signal); - process.removeListener(signal, BEFORE_EXIT_SIGNAL_LISTENERS[signal]); - process.kill(process.pid, signal); - } -}); - -type BeforeExitSignalListener = () => Promise; -type BeforeExitSignalListeners = { [key in BeforeExitSignal]: BeforeExitSignalListener; }; - -const BEFORE_EXIT_SIGNAL_LISTENERS: BeforeExitSignalListeners = { - SIGINT: beforeExitHandlerWrapper('SIGINT'), - SIGTERM: beforeExitHandlerWrapper('SIGTERM'), - SIGHUP: beforeExitHandlerWrapper('SIGHUP'), - SIGBREAK: beforeExitHandlerWrapper('SIGBREAK'), -}; - -for (const [signal, fn] of Object.entries(BEFORE_EXIT_SIGNAL_LISTENERS)) { - process.on(signal as BeforeExitSignal, fn); -} - -const processExitHandler = beforeExitHandlerWrapper('process.exit'); - -/** - * Asynchronous `process.exit()`, for running functions registered with - * `onBeforeExit`. - */ -export async function processExit(exitCode = 0) { - process.exitCode = exitCode; - await processExitHandler(); - debug('processExit: exiting (exit code: %O)', process.exitCode); - process.exit(); -} diff --git a/packages/@ionic/utils-process/tsconfig.json b/packages/@ionic/utils-process/tsconfig.json deleted file mode 100644 index 46cccd1031..0000000000 --- a/packages/@ionic/utils-process/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "types": [ - "node" - ] - }, - "include": [ - "src/**/*.ts" - ], - "exclude": [ - "node_modules", - "src/**/__tests__/*.ts" - ] -} diff --git a/packages/@ionic/utils-stream/.gitignore b/packages/@ionic/utils-stream/.gitignore deleted file mode 100644 index de4d1f007d..0000000000 --- a/packages/@ionic/utils-stream/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -dist -node_modules diff --git a/packages/@ionic/utils-stream/.npmrc b/packages/@ionic/utils-stream/.npmrc deleted file mode 100644 index 43c97e719a..0000000000 --- a/packages/@ionic/utils-stream/.npmrc +++ /dev/null @@ -1 +0,0 @@ -package-lock=false diff --git a/packages/@ionic/utils-stream/CHANGELOG.md b/packages/@ionic/utils-stream/CHANGELOG.md deleted file mode 100644 index 61e814434b..0000000000 --- a/packages/@ionic/utils-stream/CHANGELOG.md +++ /dev/null @@ -1,198 +0,0 @@ -# Change Log - -All notable changes to this project will be documented in this file. -See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -## [3.1.7](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-stream@3.1.6...@ionic/utils-stream@3.1.7) (2023-12-19) - - -### Bug Fixes - -* **cli:** resolve vm2 security vulnerability ([#5070](https://github.com/ionic-team/ionic-cli/issues/5070)) ([4050419](https://github.com/ionic-team/ionic-cli/commit/4050419bef70fb92e58b0a83cd4b68b48090e596)) - - - - - -## [3.1.6](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-stream@3.1.5...@ionic/utils-stream@3.1.6) (2023-03-29) - -**Note:** Version bump only for package @ionic/utils-stream - - - - - -## [3.1.5](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-stream@3.1.4...@ionic/utils-stream@3.1.5) (2020-08-28) - -**Note:** Version bump only for package @ionic/utils-stream - - - - - -## [3.1.4](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-stream@3.1.3...@ionic/utils-stream@3.1.4) (2020-08-25) - -**Note:** Version bump only for package @ionic/utils-stream - - - - - -## [3.1.3](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-stream@3.1.2...@ionic/utils-stream@3.1.3) (2020-05-12) - - -### Bug Fixes - -* pin tslib to avoid "Cannot set property pathExists" error ([689e1f0](https://github.com/ionic-team/ionic-cli/commit/689e1f038b907356ef855a067a76d4822e7072a8)) - - - - - -## [3.1.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-stream@3.1.1...@ionic/utils-stream@3.1.2) (2020-05-06) - -**Note:** Version bump only for package @ionic/utils-stream - - - - - -## [3.1.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-stream@3.1.0...@ionic/utils-stream@3.1.1) (2020-03-03) - -**Note:** Version bump only for package @ionic/utils-stream - - - - - -# 3.1.0 (2020-02-11) - - -### Features - -* **start:** add new list starter option ([#4315](https://github.com/ionic-team/ionic-cli/issues/4315)) ([1df44c1](https://github.com/ionic-team/ionic-cli/commit/1df44c1591f37b89f2b672857740edd6cb2aea67)) - - - - - -## [3.0.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-stream@3.0.1...@ionic/utils-stream@3.0.2) (2020-02-10) - -**Note:** Version bump only for package @ionic/utils-stream - - - - - -## [3.0.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-stream@3.0.0...@ionic/utils-stream@3.0.1) (2020-02-03) - -**Note:** Version bump only for package @ionic/utils-stream - - - - - -# [3.0.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-stream@2.0.5...@ionic/utils-stream@3.0.0) (2020-01-25) - - -### chore - -* require Node 10 ([5a47874](https://github.com/ionic-team/ionic-cli/commit/5a478746c074207b6dc96aa8771f04a606deb1ef)) - - -### BREAKING CHANGES - -* A minimum of Node.js 10.3.0 is required. - - - - - -## [2.0.5](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-stream@2.0.4...@ionic/utils-stream@2.0.5) (2019-12-05) - -**Note:** Version bump only for package @ionic/utils-stream - - - - - -## [2.0.4](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-stream@2.0.3...@ionic/utils-stream@2.0.4) (2019-09-18) - -**Note:** Version bump only for package @ionic/utils-stream - - - - - -## [2.0.3](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-stream@2.0.2...@ionic/utils-stream@2.0.3) (2019-08-23) - -**Note:** Version bump only for package @ionic/utils-stream - - - - - -## [2.0.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-stream@2.0.1...@ionic/utils-stream@2.0.2) (2019-08-14) - -**Note:** Version bump only for package @ionic/utils-stream - - - - - -## [2.0.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-stream@2.0.0...@ionic/utils-stream@2.0.1) (2019-08-07) - -**Note:** Version bump only for package @ionic/utils-stream - - - - - -# [2.0.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-stream@1.0.1...@ionic/utils-stream@2.0.0) (2019-06-10) - - -### Code Refactoring - -* remove combineStreams ([93f4ff2](https://github.com/ionic-team/ionic-cli/commit/93f4ff2)) - - -### BREAKING CHANGES - -* `combineStreams` has been removed. Use -`stream-combiner2` directly. - - - - - -## [1.0.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-stream@1.0.0...@ionic/utils-stream@1.0.1) (2019-06-05) - -**Note:** Version bump only for package @ionic/utils-stream - - - - - -# [1.0.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-stream@0.0.1...@ionic/utils-stream@1.0.0) (2019-05-29) - - -### chore - -* require Node 8 ([5670e68](https://github.com/ionic-team/ionic-cli/commit/5670e68)) - - -### BREAKING CHANGES - -* A minimum of Node.js 8.9.4 is required. - - - - - - -## 0.0.1 (2019-02-27) - - - - -**Note:** Version bump only for package @ionic/utils-stream diff --git a/packages/@ionic/utils-stream/LICENSE b/packages/@ionic/utils-stream/LICENSE deleted file mode 100644 index 7c5808ced6..0000000000 --- a/packages/@ionic/utils-stream/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2017 Drifty Co - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/packages/@ionic/utils-stream/README.md b/packages/@ionic/utils-stream/README.md deleted file mode 100644 index fce2c8f9a3..0000000000 --- a/packages/@ionic/utils-stream/README.md +++ /dev/null @@ -1 +0,0 @@ -# @ionic/utils-stream diff --git a/packages/@ionic/utils-stream/jest.config.js b/packages/@ionic/utils-stream/jest.config.js deleted file mode 100644 index eace8d6d9d..0000000000 --- a/packages/@ionic/utils-stream/jest.config.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('../../../jest.config.base'); diff --git a/packages/@ionic/utils-stream/lint-staged.config.js b/packages/@ionic/utils-stream/lint-staged.config.js deleted file mode 100644 index 5815a72f0d..0000000000 --- a/packages/@ionic/utils-stream/lint-staged.config.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('../../../lint-staged.config.base'); diff --git a/packages/@ionic/utils-stream/package.json b/packages/@ionic/utils-stream/package.json deleted file mode 100644 index 14b1fa2f46..0000000000 --- a/packages/@ionic/utils-stream/package.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "name": "@ionic/utils-stream", - "version": "3.1.7", - "description": "Stream utils for NodeJS", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "homepage": "https://ionicframework.com/", - "author": "Ionic Team (https://ionic.io)", - "license": "MIT", - "engines": { - "node": ">=16.0.0" - }, - "files": [ - "dist/", - "LICENSE", - "README.md" - ], - "repository": { - "type": "git", - "url": "https://github.com/ionic-team/ionic-cli.git" - }, - "bugs": { - "url": "https://github.com/ionic-team/ionic-cli/issues" - }, - "scripts": { - "clean": "rimraf dist", - "lint": "true", - "build": "npm run clean && tsc", - "watch": "tsc -w --preserveWatchOutput", - "test": "jest --maxWorkers=4", - "prepublishOnly": "npm run build" - }, - "dependencies": { - "debug": "^4.0.0", - "tslib": "^2.0.1" - }, - "devDependencies": { - "@types/debug": "^4.1.1", - "@types/jest": "^26.0.10", - "@types/node": "~16.0.0", - "jest": "^26.4.2", - "jest-cli": "^26.0.1", - "lint-staged": "^10.0.2", - "rimraf": "^3.0.0", - "stream-combiner2": "^1.1.1", - "ts-jest": "~26.3.0", - "typescript": "~4.8.0" - } -} diff --git a/packages/@ionic/utils-stream/src/__tests__/index.ts b/packages/@ionic/utils-stream/src/__tests__/index.ts deleted file mode 100644 index b0ca3ce832..0000000000 --- a/packages/@ionic/utils-stream/src/__tests__/index.ts +++ /dev/null @@ -1,246 +0,0 @@ -import { PassThrough, Writable } from 'stream'; -import combineStreams from 'stream-combiner2'; -import { NullStream, ReadableStreamBuffer, WritableStreamBuffer, growBufferForAppendedData } from '../'; - -describe('@ionic/utils-stream', () => { - - beforeEach(() => { - jest.useFakeTimers(); - }); - - afterEach(() => { - jest.clearAllTimers(); - }); - - describe('NullStream', () => { - - it('should allow writes', () => { - const ns = new NullStream(); - ns.write('hello world!'); - }); - - it('should write nothing', done => { - const rsb = new ReadableStreamBuffer(); - const ns = new NullStream(); - const wsb = new WritableStreamBuffer(); - const ws = combineStreams(ns, wsb); - const spy = jest.spyOn(wsb, 'write'); - rsb.pipe(ws); - rsb.feed('hello world!'); - rsb.stop(); - ws.on('finish', () => { - expect(spy).not.toHaveBeenCalled(); - done(); - }); - }); - - }); - - describe('ReadableStreamBuffer', () => { - - it('should be fed a string', () => { - const msg = 'hello world!'; - const rsb = new ReadableStreamBuffer(); - const spy = jest.spyOn((rsb as any).buffer, 'write'); - expect(rsb.size).toEqual(0); - rsb.feed(msg); - rsb.stop(); - expect(rsb.size).toEqual(Buffer.byteLength(msg)); - expect(spy).toHaveBeenCalledTimes(1); - expect(spy).toHaveBeenCalledWith(msg, 0, Buffer.byteLength(msg), 'utf8'); - }); - - it('should be readable via data event', done => { - let result = ''; - const rsb = new ReadableStreamBuffer(); - rsb.on('data', chunk => { result += chunk.toString(); }); - rsb.on('end', () => { - expect(result).toEqual('hello world!'); - done(); - }); - rsb.feed('hello world!'); - rsb.stop(); - }); - - it('should be readable via readable event', done => { - let result = ''; - const rsb = new ReadableStreamBuffer(); - rsb.on('readable', () => { - let chunk; - while (null !== (chunk = rsb.read())) { - result += chunk.toString(); - } - }); - rsb.on('end', () => { - expect(result).toEqual('hello world!'); - done(); - }); - rsb.feed('hello world!'); - rsb.stop(); - }); - - it('should be pipeable', done => { - let result = ''; - const ws = new class extends Writable { _write(chunk: any, enc: string, cb: () => void) { result += chunk.toString(); cb(); } }; - const rsb = new ReadableStreamBuffer(); - ws.on('pipe', src => { - expect(src).toBe(rsb); - }); - rsb.pipe(ws); - ws.on('finish', () => { - expect(result).toEqual('hello world!'); - done(); - }); - rsb.feed('hello world!'); - rsb.stop(); - }); - - }); - - describe('WritableStreamBuffer', () => { - - it('should return empty string for empty buffer', () => { - const wsb = new WritableStreamBuffer(); - const result = wsb.consume(); - expect(result.toString()).toEqual(''); - }); - - it('should write', () => { - const wsb = new WritableStreamBuffer(); - const msg = 'hello world!'; - wsb.write(msg); - const result = wsb.consume(); - expect(result.toString()).toEqual(msg); - }); - - it('should write and clear buffer', () => { - const wsb = new WritableStreamBuffer(); - const msg = 'hello world!'; - wsb.write(msg); - wsb.consume(); - expect(wsb.size).toEqual(0); - const result = wsb.consume(); - expect(result.length).toEqual(0); - }); - - it('should be able to consume some bytes', () => { - const wsb = new WritableStreamBuffer(); - const msg = 'hello world!'; - wsb.write(msg); - const result = wsb.consume(5); - expect(wsb.size).toEqual(7); - expect(result.toString()).toEqual('hello'); - }); - - it('should be writable after consume', () => { - const wsb = new WritableStreamBuffer(); - const msg1 = 'hello world!'; - const msg2 = `it's a great day.`; - wsb.write(msg1); - const result1 = wsb.consume(); - expect(wsb.size).toEqual(0); - wsb.write(msg2); - const result2 = wsb.consume(); - expect(wsb.size).toEqual(0); - expect(result1.toString()).toEqual(msg1); - expect(result2.toString()).toEqual(msg2); - }); - - it('should be pipeable', () => { - const msg = 'hello world!'; - const wsb = new WritableStreamBuffer(); - const rs = new PassThrough(); - rs.pipe(wsb); - rs.write(msg); - rs.end(); - const result = wsb.consume(); - expect(result.toString()).toEqual(msg); - }); - - it('should be doubly pipeable', done => { - const wsb = new WritableStreamBuffer(); - const rs1 = new PassThrough(); - const rs2 = new PassThrough(); - rs1.pipe(wsb); - rs2.pipe(wsb); - - const i1 = setInterval(() => { rs1.write('a') }, 100); - const i2 = setInterval(() => { rs2.write('b') }, 150); - - setTimeout(() => { - clearInterval(i1); - rs1.end(); - }, 2000); - - setTimeout(() => { - clearInterval(i2); - rs2.end(); - }, 3000); - - jest.advanceTimersByTime(3000); - - wsb.on('finish', () => { - const result = wsb.consume(); - expect(result.toString()).toEqual(expect.stringMatching(/[ab]{40}/)); - done(); - }); - }); - - }); - - describe('ReadableStreamBuffer and WritableStreamBuffer', () => { - - it('should be pipeable together', done => { - const rsb = new ReadableStreamBuffer(); - const wsb = new WritableStreamBuffer(); - rsb.pipe(wsb); - rsb.feed('hello world!'); - rsb.stop(); - - wsb.on('finish', () => { - expect(wsb.consume().toString()).toEqual('hello world!'); - done(); - }); - }); - - }); - - describe('growBufferForAppendedData', () => { - - it('should grow the buffer', () => { - const msg = 'hello world!'; - const buf1 = Buffer.from(msg); - const buf2 = growBufferForAppendedData(buf1, Buffer.byteLength(msg), 4); - expect(buf2.length).toEqual(16); - expect(buf2.toString().replace(/\0{4}$/g, '')).toEqual(msg); - }); - - it('should not create new buffer if not needed', () => { - const buf1 = Buffer.alloc(16); - const msg = 'hello world!'; - const msglength = Buffer.byteLength(msg); - const appendsize = 4; - buf1.write(msg); - const buf2 = growBufferForAppendedData(buf1, msglength, appendsize); - buf2.fill('-', msglength); - expect(buf2.toString()).toEqual(msg + '-'.repeat(appendsize)); - expect(buf2.length).toEqual(16); - expect(buf2).toBe(buf1); - }); - - it('should grow the buffer by factor', () => { - const buf1 = Buffer.alloc(16); - const msg1 = 'hello world!'; - const msg1length = Buffer.byteLength(msg1); - const msg2 = `it's a great day.`; - buf1.write(msg1); - const buf2 = growBufferForAppendedData(buf1, msg1length, Math.ceil(Buffer.byteLength(msg2) / 16) * 16); - expect(buf2.length).toEqual(48); - buf2.write(' ', msg1length); - buf2.write(msg2, msg1length + 1); - expect(buf2.toString().replace(/\0+$/g, '')).toEqual(`${msg1} ${msg2}`); - }); - - }); - -}); diff --git a/packages/@ionic/utils-stream/src/index.ts b/packages/@ionic/utils-stream/src/index.ts deleted file mode 100644 index 035eeb6146..0000000000 --- a/packages/@ionic/utils-stream/src/index.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { Readable, ReadableOptions, Writable, WritableOptions } from 'stream'; - -const DEFAULT_CHUNK_SIZE = 4; -const DEFAULT_ALLOC_SIZE = 32; -const DEFAULT_GROW_SIZE = 16; - -export class NullStream extends Writable { - _write(chunk: any, encoding: string, callback: () => void): void { - callback(); - } -} - -export interface ReadableStreamBufferOptions extends ReadableOptions { - chunkSize?: number; - allocSize?: number; - growSize?: number; -} - -export class ReadableStreamBuffer extends Readable { - protected buffer: Buffer; - protected _size = 0; - protected _stopped = false; - protected chunkSize: number; - protected growSize: number; - - constructor(opts?: ReadableStreamBufferOptions) { - super(opts); - this.buffer = Buffer.alloc(opts && opts.allocSize ? opts.allocSize : DEFAULT_ALLOC_SIZE); - this.chunkSize = opts && opts.chunkSize ? opts.chunkSize : DEFAULT_CHUNK_SIZE; - this.growSize = opts && opts.growSize ? opts.growSize : DEFAULT_GROW_SIZE; - } - - get size(): number { - return this._size; - } - - get stopped(): boolean { - return this._stopped; - } - - _read(): void { - this._send(); - } - - feed(data: Buffer | string, encoding: BufferEncoding = 'utf8'): void { - if (this._stopped) { - throw new Error('ReadableStreamBuffer is stopped. Can no longer feed.'); - } - - const datasize = typeof data === 'string' ? Buffer.byteLength(data) : data.length; - this.buffer = growBufferForAppendedData(this.buffer, this._size, Math.ceil(datasize / this.growSize) * this.growSize); - - if (typeof data === 'string') { - this.buffer.write(data, this._size, datasize, encoding); - } else { - this.buffer.copy(data, this._size, 0); - } - - this._size += datasize; - } - - stop(): void { - if (this._stopped) { - return; - } - - this._stopped = true; - - if (this._size === 0) { - this.push(null); - } - } - - protected _send() { - const chunkSize = Math.min(this.chunkSize, this._size); - let done = false; - - if (chunkSize > 0) { - const chunk = Buffer.alloc(chunkSize); - this.buffer.copy(chunk, 0, 0, chunkSize); - done = !this.push(chunk); - this.buffer.copy(this.buffer, 0, chunkSize, this._size); - this._size -= chunkSize; - } - - if (this._size === 0 && this._stopped) { - this.push(null); - } - - if (!done) { - setTimeout(() => this._send(), 1); - } - } -} - -export interface WritableStreamBufferOptions extends WritableOptions { - allocSize?: number; - growSize?: number; -} - -export class WritableStreamBuffer extends Writable { - protected buffer: Buffer; - protected _size = 0; - protected growSize: number; - - constructor(opts?: WritableStreamBufferOptions) { - super(opts); - this.buffer = Buffer.alloc(opts && opts.allocSize ? opts.allocSize : DEFAULT_ALLOC_SIZE); - this.growSize = opts && opts.growSize ? opts.growSize : DEFAULT_GROW_SIZE; - } - - get size(): number { - return this._size; - } - - _write(chunk: any, encoding: string, callback: () => void): void { - this.buffer = growBufferForAppendedData(this.buffer, this._size, Math.ceil(chunk.length / this.growSize) * this.growSize); - chunk.copy(this.buffer, this._size, 0); - this._size += chunk.length; - callback(); - } - - consume(bytes?: number): Buffer { - bytes = typeof bytes === 'number' ? bytes : this._size; - const data = Buffer.alloc(bytes); - this.buffer.copy(data, 0, 0, data.length); - this.buffer.copy(this.buffer, 0, data.length); - this._size -= data.length; - - return data; - } -} - -export function growBufferForAppendedData(buf: Buffer, actualsize: number, appendsize: number): Buffer { - if ((buf.length - actualsize) >= appendsize) { - return buf; - } - - const newbuffer = Buffer.alloc(buf.length + appendsize); - buf.copy(newbuffer, 0, 0, actualsize); - return newbuffer; -} diff --git a/packages/@ionic/utils-stream/tsconfig.json b/packages/@ionic/utils-stream/tsconfig.json deleted file mode 100644 index db3257a64f..0000000000 --- a/packages/@ionic/utils-stream/tsconfig.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "types": [ - "node" - ] - }, - "include": [ - "../../../types/stream-combiner2.d.ts", - "src/**/*.ts" - ], - "exclude": [ - "node_modules", - "src/**/__tests__/*.ts" - ] -} diff --git a/packages/@ionic/utils-subprocess/.gitignore b/packages/@ionic/utils-subprocess/.gitignore deleted file mode 100644 index de4d1f007d..0000000000 --- a/packages/@ionic/utils-subprocess/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -dist -node_modules diff --git a/packages/@ionic/utils-subprocess/.npmrc b/packages/@ionic/utils-subprocess/.npmrc deleted file mode 100644 index 43c97e719a..0000000000 --- a/packages/@ionic/utils-subprocess/.npmrc +++ /dev/null @@ -1 +0,0 @@ -package-lock=false diff --git a/packages/@ionic/utils-subprocess/CHANGELOG.md b/packages/@ionic/utils-subprocess/CHANGELOG.md deleted file mode 100644 index a52c3e556b..0000000000 --- a/packages/@ionic/utils-subprocess/CHANGELOG.md +++ /dev/null @@ -1,350 +0,0 @@ -# Change Log - -All notable changes to this project will be documented in this file. -See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -## [3.0.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-subprocess@3.0.0...@ionic/utils-subprocess@3.0.1) (2023-12-19) - - -### Bug Fixes - -* **cli:** resolve vm2 security vulnerability ([#5070](https://github.com/ionic-team/ionic-cli/issues/5070)) ([4050419](https://github.com/ionic-team/ionic-cli/commit/4050419bef70fb92e58b0a83cd4b68b48090e596)) - - - - - -# [3.0.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-subprocess@2.1.14...@ionic/utils-subprocess@3.0.0) (2023-11-08) - - -### Bug Fixes - -* use native ES2022 error cause ([#5010](https://github.com/ionic-team/ionic-cli/issues/5010)) ([a97ba2b](https://github.com/ionic-team/ionic-cli/commit/a97ba2bcac4556017ba010692f71fed2bef3f77b)) - - -### BREAKING CHANGES - -* `message`, `stack`, and `error` properties removed from `BaseError` and `SubprocessError` - - - - - -## [2.1.14](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-subprocess@2.1.13...@ionic/utils-subprocess@2.1.14) (2023-11-08) - - -### Reverts - -* use native ES2022 error cause ([#5060](https://github.com/ionic-team/ionic-cli/issues/5060)) ([1e64a1a](https://github.com/ionic-team/ionic-cli/commit/1e64a1ada60545adf8e7c99fbd1f8766cf2416f9)) - - - - - -## [2.1.13](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-subprocess@2.1.12...@ionic/utils-subprocess@2.1.13) (2023-11-07) - - -### Bug Fixes - -* use native ES2022 error cause ([#5010](https://github.com/ionic-team/ionic-cli/issues/5010)) ([0c4cd0f](https://github.com/ionic-team/ionic-cli/commit/0c4cd0f47e00b43e8c0ce4eef072351a846b566c)) - - - - - -## [2.1.12](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-subprocess@2.1.11...@ionic/utils-subprocess@2.1.12) (2023-03-29) - -**Note:** Version bump only for package @ionic/utils-subprocess - - - - - -## [2.1.11](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-subprocess@2.1.10...@ionic/utils-subprocess@2.1.11) (2022-06-16) - -**Note:** Version bump only for package @ionic/utils-subprocess - - - - - -## [2.1.10](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-subprocess@2.1.9...@ionic/utils-subprocess@2.1.10) (2022-05-09) - -**Note:** Version bump only for package @ionic/utils-subprocess - - - - - -## [2.1.9](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-subprocess@2.1.8...@ionic/utils-subprocess@2.1.9) (2022-03-04) - - -### Bug Fixes - -* bump cross-spawn version to avoid windows problem ([#4784](https://github.com/ionic-team/ionic-cli/issues/4784)) ([2c88a71](https://github.com/ionic-team/ionic-cli/commit/2c88a7180e87497a27a97ca979c612d6663cbb2a)) - - - - - -## [2.1.8](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-subprocess@2.1.7...@ionic/utils-subprocess@2.1.8) (2020-09-29) - -**Note:** Version bump only for package @ionic/utils-subprocess - - - - - -## [2.1.7](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-subprocess@2.1.6...@ionic/utils-subprocess@2.1.7) (2020-09-24) - -**Note:** Version bump only for package @ionic/utils-subprocess - - - - - -## [2.1.6](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-subprocess@2.1.5...@ionic/utils-subprocess@2.1.6) (2020-08-28) - -**Note:** Version bump only for package @ionic/utils-subprocess - - - - - -## [2.1.5](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-subprocess@2.1.4...@ionic/utils-subprocess@2.1.5) (2020-08-27) - -**Note:** Version bump only for package @ionic/utils-subprocess - - - - - -## [2.1.4](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-subprocess@2.1.3...@ionic/utils-subprocess@2.1.4) (2020-08-25) - -**Note:** Version bump only for package @ionic/utils-subprocess - - - - - -## [2.1.3](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-subprocess@2.1.2...@ionic/utils-subprocess@2.1.3) (2020-05-12) - - -### Bug Fixes - -* pin tslib to avoid "Cannot set property pathExists" error ([689e1f0](https://github.com/ionic-team/ionic-cli/commit/689e1f038b907356ef855a067a76d4822e7072a8)) - - - - - -## [2.1.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-subprocess@2.1.1...@ionic/utils-subprocess@2.1.2) (2020-05-06) - -**Note:** Version bump only for package @ionic/utils-subprocess - - - - - -## [2.1.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-subprocess@2.1.0...@ionic/utils-subprocess@2.1.1) (2020-03-03) - -**Note:** Version bump only for package @ionic/utils-subprocess - - - - - -# 2.1.0 (2020-02-11) - - -### Features - -* **start:** add new list starter option ([#4315](https://github.com/ionic-team/ionic-cli/issues/4315)) ([1df44c1](https://github.com/ionic-team/ionic-cli/commit/1df44c1591f37b89f2b672857740edd6cb2aea67)) - - - - - -## [2.0.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-subprocess@2.0.1...@ionic/utils-subprocess@2.0.2) (2020-02-10) - -**Note:** Version bump only for package @ionic/utils-subprocess - - - - - -## [2.0.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-subprocess@2.0.0...@ionic/utils-subprocess@2.0.1) (2020-02-03) - -**Note:** Version bump only for package @ionic/utils-subprocess - - - - - -# [2.0.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-subprocess@1.0.13...@ionic/utils-subprocess@2.0.0) (2020-01-25) - - -### chore - -* require Node 10 ([5a47874](https://github.com/ionic-team/ionic-cli/commit/5a478746c074207b6dc96aa8771f04a606deb1ef)) - - -### BREAKING CHANGES - -* A minimum of Node.js 10.3.0 is required. - - - - - -## [1.0.13](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-subprocess@1.0.12...@ionic/utils-subprocess@1.0.13) (2020-01-13) - -**Note:** Version bump only for package @ionic/utils-subprocess - - - - - -## [1.0.12](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-subprocess@1.0.11...@ionic/utils-subprocess@1.0.12) (2019-12-10) - -**Note:** Version bump only for package @ionic/utils-subprocess - - - - - -## [1.0.11](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-subprocess@1.0.10...@ionic/utils-subprocess@1.0.11) (2019-12-05) - -**Note:** Version bump only for package @ionic/utils-subprocess - - - - - -## [1.0.10](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-subprocess@1.0.9...@ionic/utils-subprocess@1.0.10) (2019-11-21) - -**Note:** Version bump only for package @ionic/utils-subprocess - - - - - -## [1.0.9](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-subprocess@1.0.8...@ionic/utils-subprocess@1.0.9) (2019-10-14) - -**Note:** Version bump only for package @ionic/utils-subprocess - - - - - -## [1.0.8](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-subprocess@1.0.7...@ionic/utils-subprocess@1.0.8) (2019-09-18) - -**Note:** Version bump only for package @ionic/utils-subprocess - - - - - -## [1.0.7](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-subprocess@1.0.6...@ionic/utils-subprocess@1.0.7) (2019-08-28) - -**Note:** Version bump only for package @ionic/utils-subprocess - - - - - -## [1.0.6](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-subprocess@1.0.5...@ionic/utils-subprocess@1.0.6) (2019-08-23) - -**Note:** Version bump only for package @ionic/utils-subprocess - - - - - -## [1.0.5](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-subprocess@1.0.4...@ionic/utils-subprocess@1.0.5) (2019-08-14) - -**Note:** Version bump only for package @ionic/utils-subprocess - - - - - -## [1.0.4](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-subprocess@1.0.3...@ionic/utils-subprocess@1.0.4) (2019-08-07) - -**Note:** Version bump only for package @ionic/utils-subprocess - - - - - -## [1.0.3](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-subprocess@1.0.2...@ionic/utils-subprocess@1.0.3) (2019-06-28) - -**Note:** Version bump only for package @ionic/utils-subprocess - - - - - -## [1.0.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-subprocess@1.0.1...@ionic/utils-subprocess@1.0.2) (2019-06-10) - -**Note:** Version bump only for package @ionic/utils-subprocess - - - - - -## [1.0.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-subprocess@1.0.0...@ionic/utils-subprocess@1.0.1) (2019-06-05) - -**Note:** Version bump only for package @ionic/utils-subprocess - - - - - -# [1.0.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-subprocess@0.1.0...@ionic/utils-subprocess@1.0.0) (2019-05-29) - - -### Bug Fixes - -* **subprocess:** check all executable extensions on windows ([#3949](https://github.com/ionic-team/ionic-cli/issues/3949)) ([e1cf74e](https://github.com/ionic-team/ionic-cli/commit/e1cf74e)) - - -### chore - -* require Node 8 ([5670e68](https://github.com/ionic-team/ionic-cli/commit/5670e68)) - - -### BREAKING CHANGES - -* A minimum of Node.js 8.9.4 is required. - - - - - - -# [0.1.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-subprocess@0.0.1...@ionic/utils-subprocess@0.1.0) (2019-03-06) - - -### Bug Fixes - -* **subprocess:** allow symlinks for commands found by `which` ([ce5bf80](https://github.com/ionic-team/ionic-cli/commit/ce5bf80)) -* **subprocess:** check if command found by `which` is executable ([638cf61](https://github.com/ionic-team/ionic-cli/commit/638cf61)) - - -### Features - -* **subprocess:** add `convertPATH` utility ([eb14e8d](https://github.com/ionic-team/ionic-cli/commit/eb14e8d)) -* **subprocess:** add `findExecutables` (like `which`, but for all) ([95acb53](https://github.com/ionic-team/ionic-cli/commit/95acb53)) -* **subprocess:** options for `bashify()` ([465fafc](https://github.com/ionic-team/ionic-cli/commit/465fafc)) - - - - - -## 0.0.1 (2019-02-27) - - - - -**Note:** Version bump only for package @ionic/utils-subprocess diff --git a/packages/@ionic/utils-subprocess/LICENSE b/packages/@ionic/utils-subprocess/LICENSE deleted file mode 100644 index 7c5808ced6..0000000000 --- a/packages/@ionic/utils-subprocess/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2017 Drifty Co - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/packages/@ionic/utils-subprocess/README.md b/packages/@ionic/utils-subprocess/README.md deleted file mode 100644 index b4f5a77641..0000000000 --- a/packages/@ionic/utils-subprocess/README.md +++ /dev/null @@ -1 +0,0 @@ -# @ionic/utils-process diff --git a/packages/@ionic/utils-subprocess/jest.config.js b/packages/@ionic/utils-subprocess/jest.config.js deleted file mode 100644 index eace8d6d9d..0000000000 --- a/packages/@ionic/utils-subprocess/jest.config.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('../../../jest.config.base'); diff --git a/packages/@ionic/utils-subprocess/lint-staged.config.js b/packages/@ionic/utils-subprocess/lint-staged.config.js deleted file mode 100644 index 5815a72f0d..0000000000 --- a/packages/@ionic/utils-subprocess/lint-staged.config.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('../../../lint-staged.config.base'); diff --git a/packages/@ionic/utils-subprocess/package.json b/packages/@ionic/utils-subprocess/package.json deleted file mode 100644 index 90d51bf519..0000000000 --- a/packages/@ionic/utils-subprocess/package.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "name": "@ionic/utils-subprocess", - "version": "3.0.1", - "description": "Subprocess utils for NodeJS", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "homepage": "https://ionicframework.com/", - "author": "Ionic Team (https://ionic.io)", - "license": "MIT", - "engines": { - "node": ">=16.0.0" - }, - "files": [ - "dist/", - "LICENSE", - "README.md" - ], - "repository": { - "type": "git", - "url": "https://github.com/ionic-team/ionic-cli.git" - }, - "bugs": { - "url": "https://github.com/ionic-team/ionic-cli/issues" - }, - "scripts": { - "clean": "rimraf dist", - "lint": "true", - "build": "npm run clean && tsc", - "watch": "tsc -w --preserveWatchOutput", - "test": "jest --maxWorkers=4", - "prepublishOnly": "npm run build" - }, - "dependencies": { - "@ionic/utils-array": "2.1.6", - "@ionic/utils-fs": "3.1.7", - "@ionic/utils-process": "2.1.12", - "@ionic/utils-stream": "3.1.7", - "@ionic/utils-terminal": "2.3.5", - "cross-spawn": "^7.0.3", - "debug": "^4.0.0", - "tslib": "^2.0.1" - }, - "devDependencies": { - "@types/cross-spawn": "^6.0.0", - "@types/debug": "^4.1.1", - "@types/jest": "^26.0.10", - "@types/node": "~16.0.0", - "jest": "^26.4.2", - "jest-cli": "^26.0.1", - "lint-staged": "^10.0.2", - "rimraf": "^3.0.0", - "ts-jest": "~26.3.0", - "typescript": "~4.8.0" - } -} diff --git a/packages/@ionic/utils-subprocess/src/__tests__/index.ts b/packages/@ionic/utils-subprocess/src/__tests__/index.ts deleted file mode 100644 index 411d301573..0000000000 --- a/packages/@ionic/utils-subprocess/src/__tests__/index.ts +++ /dev/null @@ -1,447 +0,0 @@ -import { createProcessEnv } from '@ionic/utils-process'; -import { ReadableStreamBuffer, WritableStreamBuffer } from '@ionic/utils-stream'; -import { EventEmitter } from 'events'; -import * as os from 'os'; -import * as path from 'path'; - -const promisifyEvent = (emitter: EventEmitter, event: string | symbol): Promise => { - return new Promise((resolve, reject) => { - emitter.once(event, (value: any) => { - resolve(value); - }); - - emitter.once('error', (err: Error) => { - reject(err); - }); - }); -}; - -describe('@ionic/utils-subprocess', () => { - - const mock_os = os; - - describe('expandTildePath', () => { - - jest.resetModules(); - jest.mock('os', () => ({ ...mock_os, homedir: () => '/home/me' })); - const { expandTildePath } = require('../'); - - it('should not change empty string', () => { - const result = expandTildePath(''); - expect(result).toEqual(''); - }); - - it('should not change absolute path', () => { - const p = '/home/me/path/to/something'; - const result = expandTildePath(p); - expect(result).toEqual(p); - }); - - it('should not change relative path', () => { - const p = '../path/to/something'; - const result = expandTildePath(p); - expect(result).toEqual(p); - }); - - it('should not path with tilde in it', () => { - const p = '/path/to/~/something'; - const result = expandTildePath(p); - expect(result).toEqual(p); - }); - - it('should expand tilde path', () => { - const p = '~/path/to/something'; - const result = expandTildePath(p); - expect(result).toEqual('/home/me/path/to/something'); - }); - - }); - - describe('Subprocess', () => { - - jest.setTimeout(300); - - beforeEach(() => { - jest.useFakeTimers(); - }); - - afterEach(() => { - jest.clearAllTimers(); - }); - - jest.resetModules(); - const mockCrossSpawn = jest.fn(); - jest.mock('cross-spawn', () => mockCrossSpawn); - jest.mock('os', () => ({ ...mock_os, homedir: () => '/home/me' })); - const { Subprocess, SubprocessError, convertPATH } = require('../'); - - beforeEach(() => { - jest.resetAllMocks(); - }); - - it('should set attributes', async () => { - const name = 'cmd'; - const args = ['foo', 'bar', 'baz']; - const cmd = new Subprocess(name, args); - expect(cmd.name).toEqual(name); - expect((cmd as any).path).not.toBeDefined(); - expect(cmd.args).toEqual(args); - }); - - it('should set attributes for pathed name', async () => { - const name = path.resolve('/path', 'to', 'cmd'); - const args = ['foo', 'bar', 'baz']; - const cmd = new Subprocess(name, args); - expect(cmd.name).toEqual('cmd'); - expect((cmd as any).path).toEqual(name); - expect(cmd.args).toEqual(args); - }); - - it('should provide default env option', async () => { - const cmd = new Subprocess('cmd', []); - expect(cmd.options).toEqual({ env: createProcessEnv({ ...process.env, PATH: convertPATH(process.env.PATH) }) }); - }); - - it('should provide only PATH with empty env', async () => { - const PATH = convertPATH(process.env.PATH); - const cmd = new Subprocess('cmd', [], { env: {} }); - expect(cmd.options).toEqual({ env: createProcessEnv({ PATH }) }); - }); - - it('should not alter PATH if provided', async () => { - const PATH = '/path/to/bin'; - const cmd = new Subprocess('cmd', [], { env: { PATH } }); - expect(cmd.options.env.PATH).toEqual(PATH); - }); - - it('should alter PATH with tildes if provided', async () => { - const PATH = ['/path/to/bin', '~/bin'].join(path.delimiter); - const cmd = new Subprocess('cmd', [], { env: { PATH } }); - expect(cmd.options.env.PATH).toEqual(['/path/to/bin', '/home/me/bin'].join(path.delimiter)); - }); - - it('should bashify command and args', async () => { - const name = 'cmd'; - const args = ['foo', 'bar', 'baz']; - const cmd = new Subprocess(name, args); - const result = cmd.bashify(); - expect(result).toEqual('cmd foo bar baz'); - }); - - it('should bashify command and args with pathed name', async () => { - const name = path.resolve('/path', 'to', 'cmd'); - const args = ['foo', 'bar', 'baz']; - const cmd = new Subprocess(name, args); - const result = cmd.bashify(); - expect(result).toEqual('cmd foo bar baz'); - }); - - it('should bashify command and args with pathed name and keep path with maskArgv0 = false', async () => { - const name = path.resolve('/path', 'to', 'cmd'); - const args = ['foo', 'bar', 'baz']; - const cmd = new Subprocess(name, args); - const result = cmd.bashify({ maskArgv0: false }); - expect(result).toEqual(`${name} foo bar baz`); - }); - - it('should bashify command and args with pathed argv1', async () => { - const name = path.resolve('/path', 'to', 'cmd'); - const argv1 = path.resolve('/path/to/foo'); - const args = [argv1, 'bar', 'baz']; - const cmd = new Subprocess(name, args); - const result = cmd.bashify(); - expect(result).toEqual(`cmd ${argv1} bar baz`); - }); - - it('should bashify command and args with pathed name and argv1 and mask both', async () => { - const name = path.resolve('/path', 'to', 'cmd'); - const argv1 = path.resolve('/path/to/foo'); - const args = [argv1, 'bar', 'baz']; - const cmd = new Subprocess(name, args); - const result = cmd.bashify({ maskArgv0: true, maskArgv1: true }); - expect(result).toEqual(`cmd foo bar baz`); - }); - - it('should bashify shifted command and args', async () => { - const name = path.resolve('/path', 'to', 'cmd'); - const argv1 = path.resolve('/path/to/foo'); - const args = [argv1, 'bar', 'baz']; - const cmd = new Subprocess(name, args); - const result = cmd.bashify({ shiftArgv0: true }); - expect(result).toEqual(`foo bar baz`); - }); - - it('should bashify shifted command and args but not mask', async () => { - const name = path.resolve('/path', 'to', 'cmd'); - const argv1 = path.resolve('/path/to/foo'); - const args = [argv1, 'bar', 'baz']; - const cmd = new Subprocess(name, args); - const result = cmd.bashify({ maskArgv0: false, shiftArgv0: true }); - expect(result).toEqual(`${argv1} bar baz`); - }); - - it('should bashify command and args with spaces', async () => { - const name = 'cmd'; - const args = ['foo bar baz']; - const cmd = new Subprocess(name, args); - const result = cmd.bashify(); - expect(result).toEqual('cmd "foo bar baz"'); - }); - - it('should bashify command and args with spaces with double quotes inside', async () => { - const name = 'cmd'; - const args = ['foo "bar" baz']; - const cmd = new Subprocess(name, args); - const result = cmd.bashify(); - expect(result).toEqual('cmd "foo \\"bar\\" baz"'); - }); - - it('should call spawn with correct program/args', async () => { - const result = {}; - mockCrossSpawn.mockImplementation(() => result); - const name = 'cmd'; - const args = ['foo', 'bar', 'baz']; - const options = { env: { PATH: '' } }; - const cmd = new Subprocess(name, args, options); - const expectedOptions = { env: createProcessEnv(options.env) }; - expect(cmd.spawn()).toBe(result); - expect(mockCrossSpawn).toHaveBeenCalledTimes(1); - expect(mockCrossSpawn).toHaveBeenCalledWith(name, args, expectedOptions); - }); - - it('should call spawn with correct program/args with pathed name', async () => { - const result = {}; - mockCrossSpawn.mockImplementation(() => result); - const name = path.resolve('/path', 'to', 'cmd'); - const args = ['foo', 'bar', 'baz']; - const options = { env: { PATH: '' } }; - const cmd = new Subprocess(name, args, options); - const expectedOptions = { env: createProcessEnv(options.env) }; - expect(cmd.spawn()).toBe(result); - expect(mockCrossSpawn).toHaveBeenCalledTimes(1); - expect(mockCrossSpawn).toHaveBeenCalledWith(name, args, expectedOptions); - }); - - it('should pipe stdout and stderr in run()', async () => { - const cmd = new Subprocess('cmd', []); - const mockSpawnStdout = new ReadableStreamBuffer(); - const mockSpawnStderr = new ReadableStreamBuffer(); - const cp = new class extends EventEmitter { stdout = mockSpawnStdout; stderr = mockSpawnStderr; }; - mockCrossSpawn.mockImplementation(() => cp); - const stdoutMock = new WritableStreamBuffer(); - const stderrMock = new WritableStreamBuffer(); - const promise = cmd.run(); - promise.p.stdout?.pipe(stdoutMock); - promise.p.stderr?.pipe(stderrMock); - mockSpawnStdout.feed('hello world!'); - mockSpawnStdout.stop(); - mockSpawnStderr.feed('oh no!'); - mockSpawnStderr.stop(); - await Promise.all([promisifyEvent(stdoutMock, 'finish'), promisifyEvent(stderrMock, 'finish')]); - cp.emit('close', 0); - await promise; - expect(mockCrossSpawn).toHaveBeenCalledTimes(1); - expect(stdoutMock.consume().toString()).toEqual('hello world!'); - expect(stderrMock.consume().toString()).toEqual('oh no!'); - }); - - it('should error for non-zero exit', async () => { - const cmd = new Subprocess('cmd', []); - const mockSpawnStdout = new ReadableStreamBuffer(); - const mockSpawnStderr = new ReadableStreamBuffer(); - const cp = new class extends EventEmitter { stdout = mockSpawnStdout; stderr = mockSpawnStderr; }; - mockCrossSpawn.mockImplementation(() => cp); - const buf = new WritableStreamBuffer(); - const promise = cmd.run(); - promise.p.stdout?.pipe(buf); - promise.p.stderr?.pipe(buf); - mockSpawnStdout.stop(); - mockSpawnStderr.stop(); - await promisifyEvent(buf, 'finish'); - cp.emit('close', 1); - await expect(promise).rejects.toThrow('Non-zero exit from subprocess.'); - }); - - it('should error for signal exit', async () => { - const cmd = new Subprocess('cmd', []); - const mockSpawnStdout = new ReadableStreamBuffer(); - const mockSpawnStderr = new ReadableStreamBuffer(); - const cp = new class extends EventEmitter { stdout = mockSpawnStdout; stderr = mockSpawnStderr; }; - mockCrossSpawn.mockImplementation(() => cp); - const buf = new WritableStreamBuffer(); - const promise = cmd.run(); - promise.p.stdout?.pipe(buf); - promise.p.stderr?.pipe(buf); - mockSpawnStdout.stop(); - mockSpawnStderr.stop(); - await promisifyEvent(buf, 'finish'); - cp.emit('close', null, 'SIGINT'); - await expect(promise).rejects.toThrow('Signal exit from subprocess.'); - }); - - it('should have child process in run() return value', async () => { - const cmd = new Subprocess('cmd', []); - const mockSpawnStdout = new ReadableStreamBuffer(); - const mockSpawnStderr = new ReadableStreamBuffer(); - const cp = new class extends EventEmitter { stdout = mockSpawnStdout; stderr = mockSpawnStderr; }; - mockCrossSpawn.mockImplementation(() => cp); - const promise = cmd.run(); - expect(promise.p).toBe(cp); - }); - - it('should resolve stdout and stderr in combinedOutput()', async () => { - const cmd = new Subprocess('cmd', []); - const mockSpawnStdout = new ReadableStreamBuffer(); - const mockSpawnStderr = new ReadableStreamBuffer(); - const cp = new class extends EventEmitter { stdout = mockSpawnStdout; stderr = mockSpawnStderr; }; - mockCrossSpawn.mockImplementation(() => cp); - const p = cmd.combinedOutput(); - const outletter = '1'; - const errletter = '2'; - const outinput = outletter.repeat(26); - const errinput = errletter.repeat(26); - mockSpawnStdout.feed(outinput); - mockSpawnStdout.stop(); - mockSpawnStderr.feed(errinput); - mockSpawnStderr.stop(); - await Promise.all([promisifyEvent(mockSpawnStdout, 'end'), promisifyEvent(mockSpawnStderr, 'end')]); - cp.emit('close', 0); - const result = await p; - expect(result.length).toEqual(outinput.length + errinput.length); - expect(result.split('').filter((l: string) => l !== outletter).join('')).toEqual(errinput); - expect(result.split('').filter((l: string) => l !== errletter).join('')).toEqual(outinput); - }); - - it('should error with combined output for output()', async done => { - const cmd = new Subprocess('cmd', []); - const mockSpawnStdout = new ReadableStreamBuffer(); - const mockSpawnStderr = new ReadableStreamBuffer(); - const cp = new class extends EventEmitter { stdout = mockSpawnStdout; stderr = mockSpawnStderr; }; - mockCrossSpawn.mockImplementation(() => cp); - const p = cmd.output(); - const outletter = '1'; - const errletter = '2'; - const outinput = outletter.repeat(26); - const errinput = errletter.repeat(26); - mockSpawnStdout.feed(outinput); - mockSpawnStdout.stop(); - mockSpawnStderr.feed(errinput); - mockSpawnStderr.stop(); - await Promise.all([promisifyEvent(mockSpawnStdout, 'end'), promisifyEvent(mockSpawnStderr, 'end')]); - cp.emit('close', 1); - try { - await p; - } catch (e: any) { - if (!(e instanceof SubprocessError)) { - throw new Error('not SubprocessError'); - } - - if (!e.output) { - throw new Error('no output'); - } - - expect(e.output.length).toEqual(outinput.length + errinput.length); - expect(e.output.split('').filter((l: string) => l !== outletter).join('')).toEqual(errinput); - expect(e.output.split('').filter((l: string) => l !== errletter).join('')).toEqual(outinput); - done(); - } - }); - - it('should error with combined output for combinedOutput()', async done => { - const cmd = new Subprocess('cmd', []); - const mockSpawnStdout = new ReadableStreamBuffer(); - const mockSpawnStderr = new ReadableStreamBuffer(); - const cp = new class extends EventEmitter { stdout = mockSpawnStdout; stderr = mockSpawnStderr; }; - mockCrossSpawn.mockImplementation(() => cp); - const p = cmd.combinedOutput(); - const outletter = '1'; - const errletter = '2'; - const outinput = outletter.repeat(26); - const errinput = errletter.repeat(26); - mockSpawnStdout.feed(outinput); - mockSpawnStdout.stop(); - mockSpawnStderr.feed(errinput); - mockSpawnStderr.stop(); - await Promise.all([promisifyEvent(mockSpawnStdout, 'end'), promisifyEvent(mockSpawnStderr, 'end')]); - cp.emit('close', 1); - try { - await p; - } catch (e: any) { - if (!(e instanceof SubprocessError)) { - throw new Error('not SubprocessError'); - } - - if (!e.output) { - throw new Error('no output'); - } - - expect(e.output.length).toEqual(outinput.length + errinput.length); - expect(e.output.split('').filter((l: string) => l !== outletter).join('')).toEqual(errinput); - expect(e.output.split('').filter((l: string) => l !== errletter).join('')).toEqual(outinput); - done(); - } - }); - - it('should resolve stdout in output()', async () => { - const cmd = new Subprocess('cmd', []); - const mockSpawnStdout = new ReadableStreamBuffer(); - const mockSpawnStderr = new ReadableStreamBuffer(); - const cp = new class extends EventEmitter { stdout = mockSpawnStdout; stderr = mockSpawnStderr; }; - mockCrossSpawn.mockImplementation(() => cp); - const p = cmd.output(); - mockSpawnStdout.feed('hello world!'); - mockSpawnStdout.stop(); - mockSpawnStderr.feed('oh no!'); - mockSpawnStderr.stop(); - await Promise.all([promisifyEvent(mockSpawnStdout, 'end'), promisifyEvent(mockSpawnStderr, 'end')]); - cp.emit('close', 0); - const result = await p; - expect(result).toEqual('hello world!'); - }); - - }); - - describe('which/findExecutables', () => { - describe('windows', () => { - const originalPATHEXT = process.env.PATHEXT; - const mockBinDir1 = 'C:\\path\\to\\nodejs'; - const mockBinDir2 = 'C:\\other\\path\\to\\nodejs'; - const mockBinPath1 = path.win32.join(mockBinDir1, 'node.exe'); - const mockBinPath2 = path.win32.join(mockBinDir2, 'node.cmd'); - const mockPATH = `C:\\my\\home\\dir;C:\\some\\other\\dir;${mockBinDir1};${mockBinDir2}`; - - process.env.PATHEXT = '.COM;.EXE;.BAT;.CMD'; - - afterAll(() => { - process.env.PATHEXT = originalPATHEXT; - }); - - jest.resetModules(); - jest.mock('path', () => path.win32); - jest.mock('@ionic/utils-terminal', () => ({ TERMINAL_INFO: { windows: true } })); - jest.doMock('@ionic/utils-fs', () => ({ - isExecutableFile: async (filePath: string) => filePath === mockBinPath1 || filePath === mockBinPath2 - })); - - const { which, findExecutables } = require('../'); - - it('should find the first executable in PATH', async () => { - const result = await which('node', { PATH: mockPATH }); - expect(result).toEqual(mockBinPath1); - }); - - it('should not append an extension if already provided', async () => { - const result = await which('node.cmd', { PATH: mockPATH }); - expect(result).toEqual(mockBinPath2); - }); - - it('should find all executables in PATH', async () => { - const result = await findExecutables('node', { PATH: mockPATH }); - expect(result.length).toEqual(2); - expect(result[0]).toEqual(mockBinPath1); - expect(result[1]).toEqual(mockBinPath2); - }); - }); - }); - -}); diff --git a/packages/@ionic/utils-subprocess/src/index.ts b/packages/@ionic/utils-subprocess/src/index.ts deleted file mode 100644 index 4488ab57fd..0000000000 --- a/packages/@ionic/utils-subprocess/src/index.ts +++ /dev/null @@ -1,308 +0,0 @@ -import { concurrentFilter, map } from '@ionic/utils-array'; -import { isExecutableFile } from '@ionic/utils-fs'; -import { createProcessEnv, getPathParts } from '@ionic/utils-process'; -import { WritableStreamBuffer } from '@ionic/utils-stream'; -import { TERMINAL_INFO } from '@ionic/utils-terminal'; -import { ChildProcess, ForkOptions, SpawnOptions, fork as _fork } from 'child_process'; -import crossSpawn from 'cross-spawn'; -import * as os from 'os'; -import * as pathlib from 'path'; - -export const ERROR_COMMAND_NOT_FOUND = 'ERR_SUBPROCESS_COMMAND_NOT_FOUND'; -export const ERROR_NON_ZERO_EXIT = 'ERR_SUBPROCESS_NON_ZERO_EXIT'; -export const ERROR_SIGNAL_EXIT = 'ERR_SUBPROCESS_SIGNAL_EXIT'; - -export const TILDE_PATH_REGEX = /^~($|\/|\\)/; - -export function expandTildePath(p: string): string { - const h = os.homedir(); - return p.replace(TILDE_PATH_REGEX, `${h}$1`); -} - -/** - * Prepare the PATH environment variable for use with subprocesses. - * - * If a raw tilde is found in PATH, e.g. `~/.bin`, it is expanded. The raw - * tilde works in Bash, but not in Node's `child_process` outside of a shell. - * - * This is a utility method. You do not need to use it with `Subprocess`. - * - * @param path Defaults to `process.env.PATH` - */ -export function convertPATH(path = process.env.PATH || ''): string { - return path.split(pathlib.delimiter).map(expandTildePath).join(pathlib.delimiter); -} - -export class SubprocessError extends Error { - readonly name = 'SubprocessError'; - - code?: typeof ERROR_COMMAND_NOT_FOUND | typeof ERROR_NON_ZERO_EXIT | typeof ERROR_SIGNAL_EXIT; - output?: string; - signal?: string; - exitCode?: number; -} - -export interface SubprocessOptions extends SpawnOptions { } - -export interface SubprocessBashifyOptions { - - /** - * Mask file path to first argument. - * - * The first argument to subprocesses is the program name or path, e.g. - * `/path/to/bin/my-program`. If `true`, `bashify()` will return the program - * name without a file path, e.g. `my-program`. - * - * The default is `true`. - */ - maskArgv0?: boolean; - - /** - * Mask file path to second argument. - * - * In some subprocesses, the second argument is a script file to run, e.g. - * `node ./scripts/post-install`. If `true`, `bashify()` will return the - * script name without a file path, e.g. `node post-install`. - * - * The default is `false`. - */ - maskArgv1?: boolean; - - /** - * Remove the first argument from output. - * - * Useful to make a command such as `node ./scripts/post-install` appear as - * simply `post-install`. - * - * The default is `false`. - */ - shiftArgv0?: boolean; -} - -export class Subprocess { - protected readonly path?: string; - protected _options: SpawnOptions; - - constructor(public name: string, public args: readonly string[], options: SubprocessOptions = {}) { - const masked = this.maskArg(name); - - if (masked !== name) { - this.name = masked; - this.path = name; - } - - this._options = options; - } - - get options(): Readonly { - const opts = this._options; - - if (!opts.env) { - opts.env = process.env; - } - - const env = createProcessEnv(opts.env || {}, { - PATH: convertPATH(typeof opts.env.PATH === 'string' ? opts.env.PATH : process.env.PATH), - }); - - return { ...opts, env }; - } - - async output(): Promise { - this._options.stdio = 'pipe'; - - const promise = this.run(); - const stdoutBuf = new WritableStreamBuffer(); - const stderrBuf = new WritableStreamBuffer(); - const combinedBuf = new WritableStreamBuffer(); - - promise.p.stdout?.pipe(stdoutBuf); - promise.p.stdout?.pipe(combinedBuf); - promise.p.stderr?.pipe(stderrBuf); - promise.p.stderr?.pipe(combinedBuf); - - try { - await promise; - } catch (e: any) { - stdoutBuf.end(); - stderrBuf.end(); - e.output = combinedBuf.consume().toString(); - throw e; - } - - stderrBuf.end(); - combinedBuf.end(); - - return stdoutBuf.consume().toString(); - } - - async combinedOutput(): Promise { - this._options.stdio = 'pipe'; - - const promise = this.run(); - const buf = new WritableStreamBuffer(); - - promise.p.stdout?.pipe(buf); - promise.p.stderr?.pipe(buf); - - try { - await promise; - } catch (e: any) { - e.output = buf.consume().toString(); - throw e; - } - - return buf.consume().toString(); - } - - run(): Promise & { p: ChildProcess; } { - const p = this.spawn(); - - const promise = new Promise((resolve, reject) => { - p.on('error', (error: NodeJS.ErrnoException) => { - let err: SubprocessError; - - if (error.code === 'ENOENT') { - err = new SubprocessError('Command not found.', { cause: error }); - err.code = ERROR_COMMAND_NOT_FOUND; - } else { - err = new SubprocessError('Command error.', { cause: error }); - } - - reject(err); - }); - - p.on('close', (code, signal) => { - let err: SubprocessError; - - if (code === 0) { - return resolve(); - } else if (signal) { - err = new SubprocessError('Signal exit from subprocess.'); - err.code = ERROR_SIGNAL_EXIT; - err.signal = signal; - } else if (code) { - err = new SubprocessError('Non-zero exit from subprocess.'); - err.code = ERROR_NON_ZERO_EXIT; - err.exitCode = code; - } else { - return resolve(); - } - - reject(err); - }); - }); - - Object.defineProperties(promise, { - p: { value: p }, - }); - - return promise as any; - } - - spawn(): ChildProcess { - return spawn(this.path ? this.path : this.name, this.args, this.options); - } - - bashify({ maskArgv0 = true, maskArgv1 = false, shiftArgv0 = false }: SubprocessBashifyOptions = {}): string { - const args = [this.path ? this.path : this.name, ...this.args]; - - if (shiftArgv0) { - args.shift(); - } - - if (args[0] && maskArgv0) { - args[0] = this.maskArg(args[0]); - } - - if (args[1] && maskArgv1) { - args[1] = this.maskArg(args[1]); - } - - return args.length > 0 - ? args.map(arg => this.bashifyArg(arg)).join(' ') - : ''; - } - - bashifyArg(arg: string): string { - return arg.includes(' ') ? `"${arg.replace(/\"/g, '\\"')}"` : arg; - } - - maskArg(arg: string): string { - const i = arg.lastIndexOf(pathlib.sep); - - return i >= 0 ? arg.substring(i + 1) : arg; - } -} - -export function spawn(command: string, args: readonly string[] = [], options?: SpawnOptions): ChildProcess { - return crossSpawn(command, [...args], options); -} - -export function fork(modulePath: string, args: readonly string[] = [], options: ForkOptions & Pick = {}): ChildProcess { - return _fork(modulePath, [...args], options); -} - -export interface WhichOptions { - PATH?: string; - PATHEXT?: string; -} - -const DEFAULT_PATHEXT = TERMINAL_INFO.windows ? '.COM;.EXE;.BAT;.CMD' : undefined; - -/** - * Find the first instance of a program in PATH. - * - * If `program` contains a path separator, this function will merely return it. - * - * @param program A command name, such as `ionic` - */ -export async function which(program: string, { PATH = process.env.PATH, PATHEXT = process.env.PATHEXT || DEFAULT_PATHEXT }: WhichOptions = {}): Promise { - if (program.includes(pathlib.sep)) { - return program; - } - - const results = await _findExecutables(program, { PATH }); - - if (!results.length) { - const err: NodeJS.ErrnoException = new Error(`${program} cannot be found within PATH`); - err.code = 'ENOENT'; - throw err; - } - - return results[0]; -} - -/** - * Find all instances of a program in PATH. - * - * If `program` contains a path separator, this function will merely return it - * inside an array. - * - * @param program A command name, such as `ionic` - */ -export async function findExecutables(program: string, { PATH = process.env.PATH, PATHEXT = process.env.PATHEXT || DEFAULT_PATHEXT }: WhichOptions = {}): Promise { - if (program.includes(pathlib.sep)) { - return [program]; - } - - return _findExecutables(program, { PATH }); -} - -async function _findExecutables(program: string, { PATH = process.env.PATH, PATHEXT = process.env.PATHEXT || DEFAULT_PATHEXT }: WhichOptions = {}): Promise { - const pathParts = getPathParts(PATH); - let programNames: string[]; - - // if windows, cycle through all possible executable extensions - // ex: node.exe, npm.cmd, etc. - if (TERMINAL_INFO.windows) { - const exts = getPathParts(PATHEXT).map(ext => ext.toLowerCase()); - // don't append extensions if one has already been provided - programNames = exts.includes(pathlib.extname(program).toLowerCase()) ? [program] : exts.map(ext => program + ext); - } else { - programNames = [program]; - } - - return ([] as string[]).concat(...await map(programNames, async (programName): Promise => - concurrentFilter(pathParts.map(p => pathlib.join(p, programName)), async p => isExecutableFile(p)))); -} diff --git a/packages/@ionic/utils-subprocess/tsconfig.json b/packages/@ionic/utils-subprocess/tsconfig.json deleted file mode 100644 index db3257a64f..0000000000 --- a/packages/@ionic/utils-subprocess/tsconfig.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "types": [ - "node" - ] - }, - "include": [ - "../../../types/stream-combiner2.d.ts", - "src/**/*.ts" - ], - "exclude": [ - "node_modules", - "src/**/__tests__/*.ts" - ] -} diff --git a/packages/@ionic/utils-terminal/.gitignore b/packages/@ionic/utils-terminal/.gitignore deleted file mode 100644 index de4d1f007d..0000000000 --- a/packages/@ionic/utils-terminal/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -dist -node_modules diff --git a/packages/@ionic/utils-terminal/.npmrc b/packages/@ionic/utils-terminal/.npmrc deleted file mode 100644 index 43c97e719a..0000000000 --- a/packages/@ionic/utils-terminal/.npmrc +++ /dev/null @@ -1 +0,0 @@ -package-lock=false diff --git a/packages/@ionic/utils-terminal/CHANGELOG.md b/packages/@ionic/utils-terminal/CHANGELOG.md deleted file mode 100644 index b1c0953b73..0000000000 --- a/packages/@ionic/utils-terminal/CHANGELOG.md +++ /dev/null @@ -1,263 +0,0 @@ -# Change Log - -All notable changes to this project will be documented in this file. -See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -## [2.3.5](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-terminal@2.3.4...@ionic/utils-terminal@2.3.5) (2023-12-19) - - -### Bug Fixes - -* **cli:** resolve vm2 security vulnerability ([#5070](https://github.com/ionic-team/ionic-cli/issues/5070)) ([4050419](https://github.com/ionic-team/ionic-cli/commit/4050419bef70fb92e58b0a83cd4b68b48090e596)) - - - - - -## [2.3.4](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-terminal@2.3.3...@ionic/utils-terminal@2.3.4) (2023-03-29) - -**Note:** Version bump only for package @ionic/utils-terminal - - - - - -## [2.3.3](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-terminal@2.3.2...@ionic/utils-terminal@2.3.3) (2022-06-16) - -**Note:** Version bump only for package @ionic/utils-terminal - - - - - -## [2.3.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-terminal@2.3.1...@ionic/utils-terminal@2.3.2) (2022-05-09) - -**Note:** Version bump only for package @ionic/utils-terminal - - - - - -## [2.3.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-terminal@2.3.0...@ionic/utils-terminal@2.3.1) (2020-09-29) - - -### Bug Fixes - -* add signal-exit dependency in correct place ([178e5e5](https://github.com/ionic-team/ionic-cli/commit/178e5e51cdc3593e3d096a5197e1dc0e17292bbd)) - - - - - -# [2.3.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-terminal@2.2.1...@ionic/utils-terminal@2.3.0) (2020-09-24) - - -### Features - -* **format:** add more formatting utilities ([d30f099](https://github.com/ionic-team/ionic-cli/commit/d30f099f50df18816fb1d3064c434f1b318518a2)) -* **format:** terminal prose formatting utils ([8912348](https://github.com/ionic-team/ionic-cli/commit/8912348ca348ae6192ddfff1af88f9c9443d205d)) - - - - - -## [2.2.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-terminal@2.2.0...@ionic/utils-terminal@2.2.1) (2020-08-28) - -**Note:** Version bump only for package @ionic/utils-terminal - - - - - -# [2.2.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-terminal@2.1.4...@ionic/utils-terminal@2.2.0) (2020-08-27) - - -### Features - -* add some ansi escape codes + cursor wrapper ([edbdb57](https://github.com/ionic-team/ionic-cli/commit/edbdb572bfe2fb5710eff7e49a483c86601ba425)) - - - - - -## [2.1.4](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-terminal@2.1.3...@ionic/utils-terminal@2.1.4) (2020-08-25) - -**Note:** Version bump only for package @ionic/utils-terminal - - - - - -## [2.1.3](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-terminal@2.1.2...@ionic/utils-terminal@2.1.3) (2020-05-12) - - -### Bug Fixes - -* pin tslib to avoid "Cannot set property pathExists" error ([689e1f0](https://github.com/ionic-team/ionic-cli/commit/689e1f038b907356ef855a067a76d4822e7072a8)) - - - - - -## [2.1.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-terminal@2.1.1...@ionic/utils-terminal@2.1.2) (2020-05-06) - -**Note:** Version bump only for package @ionic/utils-terminal - - - - - -## [2.1.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-terminal@2.1.0...@ionic/utils-terminal@2.1.1) (2020-03-03) - -**Note:** Version bump only for package @ionic/utils-terminal - - - - - -# 2.1.0 (2020-02-11) - - -### Features - -* **start:** add new list starter option ([#4315](https://github.com/ionic-team/ionic-cli/issues/4315)) ([1df44c1](https://github.com/ionic-team/ionic-cli/commit/1df44c1591f37b89f2b672857740edd6cb2aea67)) - - - - - -## [2.0.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-terminal@2.0.1...@ionic/utils-terminal@2.0.2) (2020-02-10) - -**Note:** Version bump only for package @ionic/utils-terminal - - - - - -## [2.0.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-terminal@2.0.0...@ionic/utils-terminal@2.0.1) (2020-02-03) - -**Note:** Version bump only for package @ionic/utils-terminal - - - - - -# [2.0.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-terminal@1.1.2...@ionic/utils-terminal@2.0.0) (2020-01-25) - - -### chore - -* require Node 10 ([5a47874](https://github.com/ionic-team/ionic-cli/commit/5a478746c074207b6dc96aa8771f04a606deb1ef)) - - -### BREAKING CHANGES - -* A minimum of Node.js 10.3.0 is required. - - - - - -## [1.1.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-terminal@1.1.1...@ionic/utils-terminal@1.1.2) (2019-12-10) - - -### Bug Fixes - -* **windows:** do not check TERM for windows detection ([ac4b417](https://github.com/ionic-team/ionic-cli/commit/ac4b417385c0c7859674e2ba59e495e9abc5bce4)) - - - - - -## [1.1.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-terminal@1.1.0...@ionic/utils-terminal@1.1.1) (2019-12-05) - -**Note:** Version bump only for package @ionic/utils-terminal - - - - - -# [1.1.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-terminal@1.0.5...@ionic/utils-terminal@1.1.0) (2019-11-21) - - -### Bug Fixes - -* **shell:** use shell from NodeJS if available ([53d0afa](https://github.com/ionic-team/ionic-cli/commit/53d0afaea8966f7742220896a98da570c706fb63)) - - -### Features - -* **ci:** support GitHub Actions detection ([5646c8e](https://github.com/ionic-team/ionic-cli/commit/5646c8e083862dbf976cd6cdecabe209c0ad8cfd)) - - - - - -## [1.0.5](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-terminal@1.0.4...@ionic/utils-terminal@1.0.5) (2019-09-18) - -**Note:** Version bump only for package @ionic/utils-terminal - - - - - -## [1.0.4](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-terminal@1.0.3...@ionic/utils-terminal@1.0.4) (2019-08-23) - -**Note:** Version bump only for package @ionic/utils-terminal - - - - - -## [1.0.3](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-terminal@1.0.2...@ionic/utils-terminal@1.0.3) (2019-08-14) - -**Note:** Version bump only for package @ionic/utils-terminal - - - - - -## [1.0.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-terminal@1.0.1...@ionic/utils-terminal@1.0.2) (2019-08-07) - -**Note:** Version bump only for package @ionic/utils-terminal - - - - - -## [1.0.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-terminal@1.0.0...@ionic/utils-terminal@1.0.1) (2019-06-05) - -**Note:** Version bump only for package @ionic/utils-terminal - - - - - -# [1.0.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-terminal@0.0.1...@ionic/utils-terminal@1.0.0) (2019-05-29) - - -### chore - -* require Node 8 ([5670e68](https://github.com/ionic-team/ionic-cli/commit/5670e68)) - - -### Features - -* include path to shell in terminal info ([79480b9](https://github.com/ionic-team/ionic-cli/commit/79480b9)) - - -### BREAKING CHANGES - -* A minimum of Node.js 8.9.4 is required. - - - - - - -## 0.0.1 (2019-02-27) - - - - -**Note:** Version bump only for package @ionic/utils-terminal diff --git a/packages/@ionic/utils-terminal/LICENSE b/packages/@ionic/utils-terminal/LICENSE deleted file mode 100644 index 7c5808ced6..0000000000 --- a/packages/@ionic/utils-terminal/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2017 Drifty Co - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/packages/@ionic/utils-terminal/README.md b/packages/@ionic/utils-terminal/README.md deleted file mode 100644 index 735a564e2d..0000000000 --- a/packages/@ionic/utils-terminal/README.md +++ /dev/null @@ -1 +0,0 @@ -# @ionic/utils-terminal diff --git a/packages/@ionic/utils-terminal/jest.config.js b/packages/@ionic/utils-terminal/jest.config.js deleted file mode 100644 index eace8d6d9d..0000000000 --- a/packages/@ionic/utils-terminal/jest.config.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('../../../jest.config.base'); diff --git a/packages/@ionic/utils-terminal/lint-staged.config.js b/packages/@ionic/utils-terminal/lint-staged.config.js deleted file mode 100644 index 5815a72f0d..0000000000 --- a/packages/@ionic/utils-terminal/lint-staged.config.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('../../../lint-staged.config.base'); diff --git a/packages/@ionic/utils-terminal/package.json b/packages/@ionic/utils-terminal/package.json deleted file mode 100644 index d4e3e53fff..0000000000 --- a/packages/@ionic/utils-terminal/package.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "name": "@ionic/utils-terminal", - "version": "2.3.5", - "description": "Terminal utils for NodeJS", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "homepage": "https://ionicframework.com/", - "author": "Ionic Team (https://ionic.io)", - "license": "MIT", - "engines": { - "node": ">=16.0.0" - }, - "files": [ - "dist/", - "LICENSE", - "README.md" - ], - "repository": { - "type": "git", - "url": "https://github.com/ionic-team/ionic-cli.git" - }, - "bugs": { - "url": "https://github.com/ionic-team/ionic-cli/issues" - }, - "scripts": { - "clean": "rimraf dist", - "lint": "true", - "build": "npm run clean && tsc", - "watch": "tsc -w --preserveWatchOutput", - "test": "jest --maxWorkers=4", - "prepublishOnly": "npm run build" - }, - "dependencies": { - "@types/slice-ansi": "^4.0.0", - "debug": "^4.0.0", - "signal-exit": "^3.0.3", - "slice-ansi": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", - "tslib": "^2.0.1", - "untildify": "^4.0.0", - "wrap-ansi": "^7.0.0" - }, - "devDependencies": { - "@types/debug": "^4.1.1", - "@types/jest": "^26.0.10", - "@types/node": "~16.0.0", - "@types/signal-exit": "^3.0.0", - "@types/wrap-ansi": "^3.0.0", - "jest": "^26.4.2", - "jest-cli": "^26.0.1", - "lint-staged": "^10.0.2", - "rimraf": "^3.0.0", - "ts-jest": "~26.3.0", - "typescript": "~4.8.0" - } -} diff --git a/packages/@ionic/utils-terminal/src/__tests__/format.ts b/packages/@ionic/utils-terminal/src/__tests__/format.ts deleted file mode 100644 index 3c51c0e6b4..0000000000 --- a/packages/@ionic/utils-terminal/src/__tests__/format.ts +++ /dev/null @@ -1,340 +0,0 @@ -import * as os from 'os'; -import * as path from 'path'; - -describe('@ionic/utils-terminal', () => { - - describe('format', () => { - - describe('wordWrap', () => { - - const { wordWrap } = require('../format'); - - it('should do nothing to empty string', () => { - const result = wordWrap('', {}); - expect(result).toEqual(''); - }); - - it('should do nothing to newline', () => { - const result = wordWrap('\n', {}); - expect(result).toEqual('\n'); - }); - - it('should wrap words', () => { - const result = wordWrap('hello world', { width: 5 }); - expect(result).toEqual('hello\nworld'); - }); - - it('should wrap words with proper indentation', () => { - const result = wordWrap('hello world', { width: 5, indentation: 5 }); - expect(result).toEqual('hello\n world'); - }); - - it('should wrap many words', () => { - const result = wordWrap(`I'm sorry Dave, I'm afraid I can't do that.`, { width: 10 }); - expect(result).toEqual(`I'm sorry\nDave, I'm\nafraid I\ncan't do\nthat.`); - }); - - it('should wrap a command and append a character', () => { - const result = wordWrap(`git commit -m 'Initial commit' --no-gpg-sign`, { width: 40, indentation: 4, append: ' \\' }); - expect(result).toEqual(`git commit -m 'Initial commit' \\\n --no-gpg-sign`); - }); - - }); - - describe('prettyPath', () => { - - let originalCwd: () => string; - const mock_os = os; - - jest.resetModules(); - - beforeEach(() => { - originalCwd = process.cwd; - process.cwd = () => '/home/user/dir1/dir2/dir3'; - }); - - afterEach(() => { - process.cwd = originalCwd; - }); - - describe('posix', () => { - - jest.resetModules(); - const mock_path_posix = path.posix; - const mock_homedir = () => '/home/user'; - jest.mock('os', () => ({ ...mock_os, homedir: mock_homedir })); - jest.mock('path', () => mock_path_posix); - const { prettyPath } = require('../format'); - - it('should pretty print file in cwd', () => { - const result = prettyPath('/home/user/dir1/dir2/dir3/file.txt'); - expect(result).toEqual('./file.txt'); - }); - - it('should pretty print file in a subdirectory', () => { - const result = prettyPath('/home/user/dir1/dir2/dir3/dir4/file.txt'); - expect(result).toEqual('./dir4/file.txt'); - }); - - it('should pretty print file in a sub subdirectory', () => { - const result = prettyPath('/home/user/dir1/dir2/dir3/dir4/dir5/file.txt'); - expect(result).toEqual('./dir4/dir5/file.txt'); - }); - - it('should pretty print file in one directory back', () => { - const result = prettyPath('/home/user/dir1/dir2/file.txt'); - expect(result).toEqual('../file.txt'); - }); - - it('should pretty print file in two directories back', () => { - const result = prettyPath('/home/user/dir1/file.txt'); - expect(result).toEqual('../../file.txt'); - }); - - it('should pretty print file in two directories back into subdirectory', () => { - const result = prettyPath('/home/user/dir1/weirddir/file.txt'); - expect(result).toEqual('../../weirddir/file.txt'); - }); - - it('should pretty print from home path three directories back', () => { - const result = prettyPath('/home/user/file.txt'); - expect(result).toEqual('~/file.txt'); - }); - - it('should pretty print cwd', () => { - const result = prettyPath('/home/user/dir1/dir2/dir3'); - expect(result).toEqual('.'); - }); - - it('should pretty print a subdirectory', () => { - const result = prettyPath('/home/user/dir1/dir2/dir3/dir4'); - expect(result).toEqual('./dir4'); - }); - - it('should pretty print a sub subdirectory', () => { - const result = prettyPath('/home/user/dir1/dir2/dir3/dir4/dir5'); - expect(result).toEqual('./dir4/dir5'); - }); - - it('should pretty print one directory back', () => { - const result = prettyPath('/home/user/dir1/dir2'); - expect(result).toEqual('..'); - }); - - it('should pretty print two directories back into subdirectory', () => { - const result = prettyPath('/home/user/dir1/weirddir'); - expect(result).toEqual('../../weirddir'); - }); - - it('should pretty print two directories back', () => { - const result = prettyPath('/home/user/dir1'); - expect(result).toEqual('~/dir1'); - }); - - it('should pretty print home dir', () => { - const result = prettyPath('/home/user'); - expect(result).toEqual('~'); - }); - - }); - - describe('windows', () => { - - let originalCwd: () => string; - jest.resetModules(); - const mock_path_win32 = path.win32; - const mock_homedir = () => 'C:\\home\\user'; - jest.mock('os', () => ({ ...mock_os, homedir: mock_homedir })); - jest.mock('path', () => mock_path_win32); - const { prettyPath } = require('../format'); - - beforeEach(() => { - originalCwd = process.cwd; - process.cwd = () => 'C:\\home\\user\\dir1\\dir2\\dir3'; - }); - - afterEach(() => { - process.cwd = originalCwd; - }); - - it('should pretty print file in cwd', () => { - const result = prettyPath('C:\\home\\user\\dir1\\dir2\\dir3\\file.txt'); - expect(result).toEqual('.\\file.txt'); - }); - - it('should pretty print file in a subdirectory', () => { - const result = prettyPath('C:\\home\\user\\dir1\\dir2\\dir3\\dir4\\file.txt'); - expect(result).toEqual('.\\dir4\\file.txt'); - }); - - it('should pretty print file in a sub subdirectory', () => { - const result = prettyPath('C:\\home\\user\\dir1\\dir2\\dir3\\dir4\\dir5\\file.txt'); - expect(result).toEqual('.\\dir4\\dir5\\file.txt'); - }); - - it('should pretty print file in one directory back', () => { - const result = prettyPath('C:\\home\\user\\dir1\\dir2\\file.txt'); - expect(result).toEqual('..\\file.txt'); - }); - - it('should pretty print file in two directories back', () => { - const result = prettyPath('C:\\home\\user\\dir1\\file.txt'); - expect(result).toEqual('..\\..\\file.txt'); - }); - - it('should pretty print file in two directories back into subdirectory', () => { - const result = prettyPath('C:\\home\\user\\dir1\\weirddir\\file.txt'); - expect(result).toEqual('..\\..\\weirddir\\file.txt'); - }); - - it('should pretty print from home path three directories back', () => { - const result = prettyPath('C:\\home\\user\\file.txt'); - expect(result).toEqual('~\\file.txt'); - }); - - it('should pretty print cwd', () => { - const result = prettyPath('C:\\home\\user\\dir1\\dir2\\dir3'); - expect(result).toEqual('.'); - }); - - it('should pretty print a subdirectory', () => { - const result = prettyPath('C:\\home\\user\\dir1\\dir2\\dir3\\dir4'); - expect(result).toEqual('.\\dir4'); - }); - - it('should pretty print a sub subdirectory', () => { - const result = prettyPath('C:\\home\\user\\dir1\\dir2\\dir3\\dir4\\dir5'); - expect(result).toEqual('.\\dir4\\dir5'); - }); - - it('should pretty print one directory back', () => { - const result = prettyPath('C:\\home\\user\\dir1\\dir2'); - expect(result).toEqual('..'); - }); - - it('should pretty print two directories back into subdirectory', () => { - const result = prettyPath('C:\\home\\user\\dir1\\weirddir'); - expect(result).toEqual('..\\..\\weirddir'); - }); - - it('should pretty print two directories back', () => { - const result = prettyPath('C:\\home\\user\\dir1'); - expect(result).toEqual('~\\dir1'); - }); - - it('should pretty print home dir', () => { - const result = prettyPath('C:\\home\\user'); - expect(result).toEqual('~'); - }); - - }); - - }); - - describe('generateFillSpaceStringList', () => { - - const { generateFillSpaceStringList } = require('../format'); - - it('should return empty array for no list', () => { - const result = generateFillSpaceStringList([]); - expect(result).toEqual([]); - }); - - it('should return spaces array for single character strings', () => { - const result = generateFillSpaceStringList(['a', 'b']); - expect(result).toEqual([' ', ' ']); - }); - - it('should return spaces array for equal length strings', () => { - const result = generateFillSpaceStringList(['foo', 'bar']); - expect(result).toEqual([' ', ' ']); - }); - - it('should return spaces array for varying string lengths', () => { - const result = generateFillSpaceStringList(['foo', 'bar', 'foobar', 'foobarbaz']); - expect(result).toEqual([' ', ' ', ' ', ' ']); - }); - - it('should return ', () => { - const result = generateFillSpaceStringList(['foo', 'bar'], 5); - expect(result).toEqual([' ', ' ']); - }); - - }); - - describe('columnar', () => { - - const { columnar } = require('../format'); - - it('should generate empty string from empty matrix', async () => { - const result = columnar([], {}); - expect(result).toEqual(''); - }); - - it('should work without column headers', async () => { - const result = columnar([['a', 'b', 'c']], {}); - expect(result).toEqual('a | b | c'); - }); - - it('should work for multiple rows', async () => { - const result = columnar([['cat', 'dog'], ['spongebob', 'squarepants'], ['hey', 'arnold']], { headers: ['col1', 'col2'] }); - expect(result).toEqual(` -col1 | col2 ------------------------ -cat | dog -spongebob | squarepants -hey | arnold - `.trim()); - }); - - it('should size rows correctly when cells are longer', async () => { - const result = columnar([['foo', 'bar', 'baz']], { headers: ['a', 'b', 'c'] }); - expect(result).toEqual(` -a | b | c ---------------- -foo | bar | baz - `.trim()); - }); - - it('should size rows correctly when headers are longer', async () => { - const result = columnar([['a', 'b', 'c']], { headers: ['foo', 'bar', 'baz'] }); - expect(result).toEqual(` -foo | bar | baz ---------------- -a | b | c - `.trim()); - }); - - it('should work with custom separators', async () => { - const result = columnar([['foo', 'bar', 'baz']], { hsep: '=', vsep: '/\\', headers: ['a', 'b', 'c'] }); - expect(result).toEqual(` -a /\\ b /\\ c -================= -foo /\\ bar /\\ baz - `.trim()); - }); - - it('should have appropriate spacing for cells with newlines', async () => { - const result = columnar([['mr\n& mr', 'cat', 'dog,\nthe third'], ['mr', 'spongebob', 'squarepants'], ['', 'the', 'brain']], {}); - expect(result).toEqual(` -mr | cat | dog, -& mr | | the third -mr | spongebob | squarepants - | the | brain - `.trim()); - }); - - it('should separate columns and rows with only one space with empty string separators', async () => { - const result = columnar([['a', 'b', 'c'], ['1', '2', '3']], { vsep: '', hsep: '', headers: ['x', 'x', 'x'] }); - expect(result).toEqual(` -x x x -a b c -1 2 3 - `.trim()); - }); - - }); - - }); - -}); diff --git a/packages/@ionic/utils-terminal/src/ansi.ts b/packages/@ionic/utils-terminal/src/ansi.ts deleted file mode 100644 index 0b5ff4f2bb..0000000000 --- a/packages/@ionic/utils-terminal/src/ansi.ts +++ /dev/null @@ -1,34 +0,0 @@ -const ESC = '\u001B['; - -/** - * ANSI escape codes (WIP) - * - * @see https://en.wikipedia.org/wiki/ANSI_escape_code - */ -export class EscapeCode { - static readonly cursorLeft = (): string => `${ESC}G`; - static readonly cursorUp = (count = 1): string => `${ESC}${count}A`; - static readonly cursorDown = (count = 1): string => `${ESC}${count}B`; - static readonly cursorForward = (count = 1): string => `${ESC}${count}C`; - static readonly cursorBackward = (count = 1): string => `${ESC}${count}D`; - static readonly cursorHide = (): string => `${ESC}?25l`; - static readonly cursorShow = (): string => `${ESC}?25h`; - - static readonly eraseLine = (): string => `${ESC}2K`; - static readonly eraseLines = (count: number): string => { - let seq = ''; - - for (let i = 0; i < count; i++) { - seq += EscapeCode.eraseLine(); - - if (i < count - 1) { - seq += EscapeCode.cursorUp(); - } - } - - return `${seq}${EscapeCode.cursorLeft()}`; - } - static readonly eraseUp = (): string => `${ESC}1J`; - static readonly eraseDown = (): string => `${ESC}J`; - static readonly eraseScreen = (): string => `${ESC}2J`; -} diff --git a/packages/@ionic/utils-terminal/src/cursor.ts b/packages/@ionic/utils-terminal/src/cursor.ts deleted file mode 100644 index def0352baa..0000000000 --- a/packages/@ionic/utils-terminal/src/cursor.ts +++ /dev/null @@ -1,39 +0,0 @@ -import onExit from 'signal-exit'; - -import { EscapeCode } from './ansi'; - -export class Cursor { - static stream: NodeJS.WriteStream = process.stderr; - private static _isVisible = true; - private static _listenerAttached = false; - - static show() { - if (Cursor.stream.isTTY) { - Cursor._isVisible = true; - Cursor.stream.write(EscapeCode.cursorShow()); - } - } - - static hide() { - if (Cursor.stream.isTTY) { - if (!Cursor._listenerAttached) { - onExit(() => { - Cursor.show(); - }); - - Cursor._listenerAttached = true; - } - - Cursor._isVisible = false; - Cursor.stream.write(EscapeCode.cursorHide()); - } - } - - static toggle() { - if (Cursor._isVisible) { - Cursor.hide(); - } else { - Cursor.show(); - } - } -} diff --git a/packages/@ionic/utils-terminal/src/format.ts b/packages/@ionic/utils-terminal/src/format.ts deleted file mode 100644 index 0782c596d3..0000000000 --- a/packages/@ionic/utils-terminal/src/format.ts +++ /dev/null @@ -1,139 +0,0 @@ -import * as os from 'os'; -import * as path from 'path'; - -import sliceAnsi = require('slice-ansi'); -import stringWidth = require('string-width'); -import stripAnsi = require('strip-ansi'); -import wrapAnsi = require('wrap-ansi'); -import untildify = require('untildify'); - -export { sliceAnsi, stringWidth, stripAnsi }; - -const MIN_TTY_WIDTH = 80; -const MAX_TTY_WIDTH = 120; -export const TTY_WIDTH = process.stdout.columns ? Math.max(MIN_TTY_WIDTH, Math.min(process.stdout.columns, MAX_TTY_WIDTH)) : Infinity; - -export function indent(n = 4): string { - return ' '.repeat(n); -} - -export interface WordWrapOptions { - width?: number; - indentation?: number; - append?: string; -} - -export function wordWrap(msg: string, { width = TTY_WIDTH, indentation = 0, append = '' }: WordWrapOptions) { - return wrapAnsi(msg, width - indentation - append.length, { trim: true }).split('\n').join(`${append}\n${indent(indentation)}`); -} - -export function prettyPath(p: string): string { - p = expandPath(p); - const cwd = process.cwd(); - const d = path.dirname(p); - const h = os.homedir(); - const distanceFromCwd = Math.abs(d.split(path.sep).length - cwd.split(path.sep).length); - - if (cwd === d) { - return '.' + path.sep + path.basename(p); - } else if (d.startsWith(cwd)) { - return '.' + path.sep + p.substring(cwd.length + 1); - } else if (distanceFromCwd <= 2) { - const rel = path.relative(cwd, p); - return rel ? rel : '.'; - } else if (p === h) { - return '~'; - } else if (p.indexOf(h) === 0) { - return '~' + path.sep + p.substring(h.length + 1); - } - - return p; -} - -export function expandPath(p: string): string { - return path.resolve(untildify(p)); -} - -export function generateFillSpaceStringList(list: string[], optimalLength = 1, fillCharacter = ' '): string[] { - if (optimalLength < 2) { - optimalLength = 2; - } - - const longestItem = Math.max(...list.map(item => stringWidth(item))); - const fullLength = longestItem > optimalLength ? longestItem + 1 : optimalLength; - const fullLengthString = fillCharacter.repeat(fullLength); - - return list.map(item => sliceAnsi(fullLengthString, 0, fullLength - stringWidth(item))); -} - -export interface ColumnarOptions { - hsep?: string; - vsep?: string; - headers?: string[]; -} - -/** - * Basic CLI table generator with support for ANSI colors. - * - * @param rows 2-dimensional matrix containing cells. An array of columns, - * which are arrays of cells. - * @param options.vsep The vertical separator character, default is - * `chalk.dim('|')`. Supply an empty string to hide - * the separator altogether. - * @param options.hsep The horizontal separator character, default is - * `chalk.dim('-')`. This is used under the headers, - * if supplied. Supply an empty string to hide the - * separator altogether. - * @param options.headers An array of header cells. - */ -export function columnar(rows: string[][], { hsep = '-', vsep = '|', headers }: ColumnarOptions): string { - const includeHeaders = headers ? true : false; - - if (!rows[0]) { - return ''; - } - - const columnCount = headers ? headers.length : rows[0].length; - const columns = headers ? - headers.map(header => [header]) : - rows[0].map(() => []); - - for (const row of rows) { - let highestLineCount = 0; - const splitRows = row.map(cell => { - const lines = cell.split('\n'); - highestLineCount = Math.max(highestLineCount, lines.length); - return lines; - }); - - for (const rowIndex in row) { - if (columns[rowIndex]) { - columns[rowIndex].push(...splitRows[rowIndex], ...Array(highestLineCount - splitRows[rowIndex].length).fill('')); - } - } - } - - const paddedColumns = columns.map((col, columnIndex) => { - if (columnIndex < columnCount - 1) { - const spaceCol = generateFillSpaceStringList(col); - return col.map((cell, cellIndex) => `${cell}${spaceCol[cellIndex]}${vsep === '' ? '' : `${vsep} `}`); - } else { - return col; - } - }); - - let longestRowLength = 0; - const singleColumn = paddedColumns.reduce((a, b) => { - return a.map((_, i) => { - const r = a[i] + b[i]; - longestRowLength = Math.max(longestRowLength, stringWidth(r)); - return r; - }); - }); - - if (includeHeaders && hsep !== '') { - singleColumn.splice(1, 0, hsep.repeat(longestRowLength)); - } - - return singleColumn.join('\n'); -} diff --git a/packages/@ionic/utils-terminal/src/index.ts b/packages/@ionic/utils-terminal/src/index.ts deleted file mode 100644 index f3632e2e6c..0000000000 --- a/packages/@ionic/utils-terminal/src/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './ansi'; -export * from './cursor'; -export * from './format'; -export * from './info'; diff --git a/packages/@ionic/utils-terminal/src/info.ts b/packages/@ionic/utils-terminal/src/info.ts deleted file mode 100644 index 7230db5bdf..0000000000 --- a/packages/@ionic/utils-terminal/src/info.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { debug as Debug } from 'debug'; -import * as os from 'os'; - -const debug = Debug('ionic:utils-terminal:info'); - -/** - * These environment variables work for: GitHub Actions, Travis CI, CircleCI, - * Gitlab CI, AppVeyor, CodeShip, Jenkins, TeamCity, Bitbucket Pipelines, AWS - * CodeBuild - */ -export const CI_ENVIRONMENT_VARIABLES: readonly string[] = ['CI', 'BUILD_ID', 'BUILD_NUMBER', 'BITBUCKET_COMMIT', 'CODEBUILD_BUILD_ARN', 'GITHUB_ACTIONS']; -export const CI_ENVIRONMENT_VARIABLES_DETECTED = CI_ENVIRONMENT_VARIABLES.filter(v => !!process.env[v]); - -function getShell(): string { - const { shell } = os.userInfo(); - - if (shell) { - return shell; - } - - if (process.env.SHELL) { - return process.env.SHELL; - } - - if (process.platform === 'darwin') { - return '/bin/bash'; - } - - if (process.platform === 'win32') { - return process.env.COMSPEC ? process.env.COMSPEC : 'cmd.exe'; - } - - return '/bin/sh'; -} - -if (CI_ENVIRONMENT_VARIABLES_DETECTED.length > 0) { - debug(`Environment variables for CI detected: ${CI_ENVIRONMENT_VARIABLES_DETECTED.join(', ')}`); -} - -export interface TerminalInfo { - /** - * Whether this is in CI or not. - */ - readonly ci: boolean; - - /** - * Path to the user's shell program. - */ - readonly shell: string; - - /** - * Whether the terminal is an interactive TTY or not. - */ - readonly tty: boolean; - - /** - * Whether this is a Windows shell or not. - */ - readonly windows: boolean; -} - -export const TERMINAL_INFO: TerminalInfo = Object.freeze({ - ci: CI_ENVIRONMENT_VARIABLES_DETECTED.length > 0, - shell: getShell(), - tty: Boolean(process.stdin.isTTY && process.stdout.isTTY && process.stderr.isTTY), - windows: process.platform === 'win32' || !!( - process.env.OSTYPE && /^(msys|cygwin)$/.test(process.env.OSTYPE) || - process.env.MSYSTEM && /^MINGW(32|64)$/.test(process.env.MSYSTEM) - ), -}); diff --git a/packages/@ionic/utils-terminal/tsconfig.json b/packages/@ionic/utils-terminal/tsconfig.json deleted file mode 100644 index 46cccd1031..0000000000 --- a/packages/@ionic/utils-terminal/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "types": [ - "node" - ] - }, - "include": [ - "src/**/*.ts" - ], - "exclude": [ - "node_modules", - "src/**/__tests__/*.ts" - ] -} diff --git a/packages/cli-scripts/.npmrc b/packages/cli-scripts/.npmrc deleted file mode 100644 index 43c97e719a..0000000000 --- a/packages/cli-scripts/.npmrc +++ /dev/null @@ -1 +0,0 @@ -package-lock=false diff --git a/packages/cli-scripts/CHANGELOG.md b/packages/cli-scripts/CHANGELOG.md deleted file mode 100644 index f6611e9e29..0000000000 --- a/packages/cli-scripts/CHANGELOG.md +++ /dev/null @@ -1,1149 +0,0 @@ -# Change Log - -All notable changes to this project will be documented in this file. -See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -## [2.1.75](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.74...cli-scripts@2.1.75) (2025-03-18) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.74](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.73...cli-scripts@2.1.74) (2024-01-02) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.73](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.72...cli-scripts@2.1.73) (2023-12-19) - - -### Bug Fixes - -* **cli:** resolve vm2 security vulnerability ([#5070](https://github.com/ionic-team/ionic-cli/issues/5070)) ([4050419](https://github.com/ionic-team/ionic-cli/commit/4050419bef70fb92e58b0a83cd4b68b48090e596)) - - - - - -## [2.1.72](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.71...cli-scripts@2.1.72) (2023-11-08) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.71](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.70...cli-scripts@2.1.71) (2023-11-08) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.70](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.69...cli-scripts@2.1.70) (2023-11-07) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.69](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.68...cli-scripts@2.1.69) (2023-11-07) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.68](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.67...cli-scripts@2.1.68) (2023-05-02) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.67](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.66...cli-scripts@2.1.67) (2023-05-01) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.66](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.65...cli-scripts@2.1.66) (2023-04-05) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.65](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.64...cli-scripts@2.1.65) (2023-03-29) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.64](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.63...cli-scripts@2.1.64) (2023-03-17) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.63](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.62...cli-scripts@2.1.63) (2023-03-17) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.62](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.61...cli-scripts@2.1.62) (2023-01-13) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.61](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.60...cli-scripts@2.1.61) (2023-01-13) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.60](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.59...cli-scripts@2.1.60) (2022-12-20) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.59](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.58...cli-scripts@2.1.59) (2022-12-13) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.58](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.57...cli-scripts@2.1.58) (2022-11-15) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.57](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.56...cli-scripts@2.1.57) (2022-10-06) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.56](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.55...cli-scripts@2.1.56) (2022-09-29) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.55](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.54...cli-scripts@2.1.55) (2022-06-21) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.54](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.53...cli-scripts@2.1.54) (2022-06-16) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.53](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.52...cli-scripts@2.1.53) (2022-05-09) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.52](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.51...cli-scripts@2.1.52) (2022-03-15) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.51](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.50...cli-scripts@2.1.51) (2022-03-04) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.50](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.49...cli-scripts@2.1.50) (2021-11-10) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.49](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.48...cli-scripts@2.1.49) (2021-10-25) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.48](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.47...cli-scripts@2.1.48) (2021-09-03) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.47](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.46...cli-scripts@2.1.47) (2021-08-11) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.46](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.45...cli-scripts@2.1.46) (2021-06-08) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.45](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.44...cli-scripts@2.1.45) (2021-06-03) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.44](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.43...cli-scripts@2.1.44) (2021-05-19) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.43](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.42...cli-scripts@2.1.43) (2021-05-19) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.42](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.41...cli-scripts@2.1.42) (2021-05-11) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.41](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.40...cli-scripts@2.1.41) (2021-04-30) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.40](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.39...cli-scripts@2.1.40) (2021-04-28) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.39](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.38...cli-scripts@2.1.39) (2021-02-22) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.38](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.37...cli-scripts@2.1.38) (2021-02-18) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.37](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.36...cli-scripts@2.1.37) (2021-01-27) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.36](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.35...cli-scripts@2.1.36) (2020-12-10) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.35](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.34...cli-scripts@2.1.35) (2020-11-17) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.34](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.33...cli-scripts@2.1.34) (2020-10-29) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.33](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.32...cli-scripts@2.1.33) (2020-10-15) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.32](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.31...cli-scripts@2.1.32) (2020-10-12) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.31](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.30...cli-scripts@2.1.31) (2020-10-05) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.30](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.29...cli-scripts@2.1.30) (2020-09-29) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.29](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.28...cli-scripts@2.1.29) (2020-09-24) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.28](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.27...cli-scripts@2.1.28) (2020-09-02) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.27](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.26...cli-scripts@2.1.27) (2020-08-29) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.26](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.25...cli-scripts@2.1.26) (2020-08-28) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.25](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.24...cli-scripts@2.1.25) (2020-08-27) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.24](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.23...cli-scripts@2.1.24) (2020-08-27) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.23](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.22...cli-scripts@2.1.23) (2020-08-26) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.22](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.21...cli-scripts@2.1.22) (2020-08-25) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.21](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.20...cli-scripts@2.1.21) (2020-08-20) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.20](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.19...cli-scripts@2.1.20) (2020-07-29) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.19](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.18...cli-scripts@2.1.19) (2020-07-27) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.18](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.17...cli-scripts@2.1.18) (2020-06-15) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.17](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.16...cli-scripts@2.1.17) (2020-06-02) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.16](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.15...cli-scripts@2.1.16) (2020-05-27) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.15](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.14...cli-scripts@2.1.15) (2020-05-17) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.14](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.13...cli-scripts@2.1.14) (2020-05-12) - - -### Bug Fixes - -* pin tslib to avoid "Cannot set property pathExists" error ([689e1f0](https://github.com/ionic-team/ionic-cli/commit/689e1f038b907356ef855a067a76d4822e7072a8)) - - - - - -## [2.1.13](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.12...cli-scripts@2.1.13) (2020-05-11) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.12](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.11...cli-scripts@2.1.12) (2020-05-06) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.11](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.10...cli-scripts@2.1.11) (2020-04-29) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.10](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.9...cli-scripts@2.1.10) (2020-04-14) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.9](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.8...cli-scripts@2.1.9) (2020-04-07) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.8](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.7...cli-scripts@2.1.8) (2020-04-05) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.7](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.6...cli-scripts@2.1.7) (2020-04-05) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.6](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.5...cli-scripts@2.1.6) (2020-04-02) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.5](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.4...cli-scripts@2.1.5) (2020-03-30) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.4](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.3...cli-scripts@2.1.4) (2020-03-18) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.3](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.2...cli-scripts@2.1.3) (2020-03-16) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.2](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.1...cli-scripts@2.1.2) (2020-03-09) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.1.1](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.0...cli-scripts@2.1.1) (2020-03-03) - -**Note:** Version bump only for package cli-scripts - - - - - -# 2.1.0 (2020-02-11) - - -### Features - -* **start:** add new list starter option ([#4315](https://github.com/ionic-team/ionic-cli/issues/4315)) ([1df44c1](https://github.com/ionic-team/ionic-cli/commit/1df44c1591f37b89f2b672857740edd6cb2aea67)) - - - - - -## [2.0.2](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.0.1...cli-scripts@2.0.2) (2020-02-10) - -**Note:** Version bump only for package cli-scripts - - - - - -## [2.0.1](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.0.0...cli-scripts@2.0.1) (2020-02-03) - -**Note:** Version bump only for package cli-scripts - - - - - -# [2.0.0](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@1.0.31...cli-scripts@2.0.0) (2020-01-25) - - -### chore - -* rename Ionic CLI package to `@ionic/cli` ([1f86b34](https://github.com/ionic-team/ionic-cli/commit/1f86b34dc4ebfd565c240a6b4a44f3815a18b769)) -* require Node 10 ([5a47874](https://github.com/ionic-team/ionic-cli/commit/5a478746c074207b6dc96aa8771f04a606deb1ef)) - - -### BREAKING CHANGES - -* The npm package for the Ionic CLI is now `@ionic/cli`. -* A minimum of Node.js 10.3.0 is required. - - - - - -## [1.0.31](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@1.0.30...cli-scripts@1.0.31) (2020-01-15) - -**Note:** Version bump only for package cli-scripts - - - - - -## [1.0.30](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@1.0.29...cli-scripts@1.0.30) (2020-01-13) - -**Note:** Version bump only for package cli-scripts - - - - - -## [1.0.29](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@1.0.28...cli-scripts@1.0.29) (2019-12-13) - -**Note:** Version bump only for package cli-scripts - - - - - -## [1.0.28](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@1.0.27...cli-scripts@1.0.28) (2019-12-10) - -**Note:** Version bump only for package cli-scripts - - - - - -## [1.0.27](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@1.0.26...cli-scripts@1.0.27) (2019-12-09) - -**Note:** Version bump only for package cli-scripts - - - - - -## [1.0.26](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@1.0.25...cli-scripts@1.0.26) (2019-12-05) - -**Note:** Version bump only for package cli-scripts - - - - - -## [1.0.25](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@1.0.24...cli-scripts@1.0.25) (2019-11-25) - -**Note:** Version bump only for package cli-scripts - - - - - -## [1.0.24](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@1.0.23...cli-scripts@1.0.24) (2019-11-24) - -**Note:** Version bump only for package cli-scripts - - - - - -## [1.0.23](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@1.0.22...cli-scripts@1.0.23) (2019-11-21) - -**Note:** Version bump only for package cli-scripts - - - - - -## [1.0.22](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@1.0.21...cli-scripts@1.0.22) (2019-11-12) - -**Note:** Version bump only for package cli-scripts - - - - - -## [1.0.21](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@1.0.20...cli-scripts@1.0.21) (2019-10-30) - -**Note:** Version bump only for package cli-scripts - - - - - -## [1.0.20](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@1.0.19...cli-scripts@1.0.20) (2019-10-14) - -**Note:** Version bump only for package cli-scripts - - - - - -## [1.0.19](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@1.0.18...cli-scripts@1.0.19) (2019-10-14) - -**Note:** Version bump only for package cli-scripts - - - - - -## [1.0.18](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@1.0.17...cli-scripts@1.0.18) (2019-10-01) - -**Note:** Version bump only for package cli-scripts - - - - - -## [1.0.17](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@1.0.16...cli-scripts@1.0.17) (2019-09-20) - -**Note:** Version bump only for package cli-scripts - - - - - -## [1.0.16](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@1.0.15...cli-scripts@1.0.16) (2019-09-20) - -**Note:** Version bump only for package cli-scripts - - - - - -## [1.0.15](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@1.0.14...cli-scripts@1.0.15) (2019-09-18) - -**Note:** Version bump only for package cli-scripts - - - - - -## [1.0.14](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@1.0.13...cli-scripts@1.0.14) (2019-09-10) - -**Note:** Version bump only for package cli-scripts - - - - - -## [1.0.13](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@1.0.12...cli-scripts@1.0.13) (2019-08-28) - -**Note:** Version bump only for package cli-scripts - - - - - -## [1.0.12](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@1.0.11...cli-scripts@1.0.12) (2019-08-23) - -**Note:** Version bump only for package cli-scripts - - - - - -## [1.0.11](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@1.0.10...cli-scripts@1.0.11) (2019-08-14) - -**Note:** Version bump only for package cli-scripts - - - - - -## [1.0.10](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@1.0.9...cli-scripts@1.0.10) (2019-08-07) - -**Note:** Version bump only for package cli-scripts - - - - - -## [1.0.9](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@1.0.8...cli-scripts@1.0.9) (2019-07-15) - -**Note:** Version bump only for package cli-scripts - - - - - -## [1.0.8](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@1.0.7...cli-scripts@1.0.8) (2019-07-09) - -**Note:** Version bump only for package cli-scripts - - - - - -## [1.0.7](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@1.0.6...cli-scripts@1.0.7) (2019-06-28) - -**Note:** Version bump only for package cli-scripts - - - - - -## [1.0.6](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@1.0.5...cli-scripts@1.0.6) (2019-06-26) - -**Note:** Version bump only for package cli-scripts - - - - - -## [1.0.5](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@1.0.4...cli-scripts@1.0.5) (2019-06-26) - -**Note:** Version bump only for package cli-scripts - - - - - -## [1.0.4](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@1.0.3...cli-scripts@1.0.4) (2019-06-21) - -**Note:** Version bump only for package cli-scripts - - - - - -## [1.0.3](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@1.0.2...cli-scripts@1.0.3) (2019-06-18) - -**Note:** Version bump only for package cli-scripts - - - - - -## [1.0.2](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@1.0.1...cli-scripts@1.0.2) (2019-06-10) - -**Note:** Version bump only for package cli-scripts - - - - - -## [1.0.1](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@1.0.0...cli-scripts@1.0.1) (2019-06-05) - -**Note:** Version bump only for package cli-scripts - - - - - -# [1.0.0](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@0.0.36...cli-scripts@1.0.0) (2019-05-29) - - -### chore - -* require Node 8 ([5670e68](https://github.com/ionic-team/ionic-cli/commit/5670e68)) - - -### BREAKING CHANGES - -* A minimum of Node.js 8.9.4 is required. - - - - - - -## [0.0.36](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@0.0.35...cli-scripts@0.0.36) (2019-03-12) - - - - -**Note:** Version bump only for package cli-scripts - - -## [0.0.35](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@0.0.34...cli-scripts@0.0.35) (2019-03-06) - - - - -**Note:** Version bump only for package cli-scripts - - -## [0.0.34](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@0.0.33...cli-scripts@0.0.34) (2019-02-27) - - - - -**Note:** Version bump only for package cli-scripts - - -## [0.0.33](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@0.0.32...cli-scripts@0.0.33) (2019-02-15) - - - - -**Note:** Version bump only for package cli-scripts - - -## [0.0.32](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@0.0.31...cli-scripts@0.0.32) (2019-02-04) - - - - -**Note:** Version bump only for package cli-scripts - - -## [0.0.31](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@0.0.30...cli-scripts@0.0.31) (2019-01-30) - - - - -**Note:** Version bump only for package cli-scripts - - -## [0.0.30](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@0.0.29...cli-scripts@0.0.30) (2019-01-29) - - - - -**Note:** Version bump only for package cli-scripts - - -## [0.0.29](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@0.0.28...cli-scripts@0.0.29) (2019-01-23) - - - - -**Note:** Version bump only for package cli-scripts - - -## [0.0.28](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@0.0.27...cli-scripts@0.0.28) (2019-01-14) - - - - -**Note:** Version bump only for package cli-scripts - - -## [0.0.27](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@0.0.26...cli-scripts@0.0.27) (2019-01-08) - - - - -**Note:** Version bump only for package cli-scripts - - -## [0.0.26](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@0.0.25...cli-scripts@0.0.26) (2019-01-07) - - - - -**Note:** Version bump only for package cli-scripts - - -## [0.0.25](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@0.0.24...cli-scripts@0.0.25) (2018-12-19) - - - - -**Note:** Version bump only for package cli-scripts - - -## [0.0.24](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@0.0.23...cli-scripts@0.0.24) (2018-11-27) - - - - -**Note:** Version bump only for package cli-scripts - - -## [0.0.23](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@0.0.22...cli-scripts@0.0.23) (2018-11-20) - - -### Bug Fixes - -* **docs:** add name to dummy config files ([7976f29](https://github.com/ionic-team/ionic-cli/commit/7976f29)) - - - - - -## [0.0.22](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@0.0.21...cli-scripts@0.0.22) (2018-11-04) - - - - -**Note:** Version bump only for package cli-scripts - - -## [0.0.21](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@0.0.20...cli-scripts@0.0.21) (2018-10-31) - - - - -**Note:** Version bump only for package cli-scripts - - -## [0.0.20](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@0.0.19...cli-scripts@0.0.20) (2018-10-05) - - - - -**Note:** Version bump only for package cli-scripts - - -## [0.0.19](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@0.0.18...cli-scripts@0.0.19) (2018-10-03) - - - - -**Note:** Version bump only for package cli-scripts - - -## [0.0.18](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@0.0.17...cli-scripts@0.0.18) (2018-09-05) - - - - -**Note:** Version bump only for package cli-scripts - - -## [0.0.17](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@0.0.16...cli-scripts@0.0.17) (2018-08-20) - - - - -**Note:** Version bump only for package cli-scripts - - -## [0.0.16](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@0.0.15...cli-scripts@0.0.16) (2018-08-15) - - - - -**Note:** Version bump only for package cli-scripts - - -## [0.0.15](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@0.0.14...cli-scripts@0.0.15) (2018-08-09) - - -### Bug Fixes - -* **serve:** fix unclosed connection issue again ([#3500](https://github.com/ionic-team/ionic-cli/issues/3500)) ([1f0ef3b](https://github.com/ionic-team/ionic-cli/commit/1f0ef3b)) - - - - - -## [0.0.14](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@0.0.13...cli-scripts@0.0.14) (2018-08-07) - - - - -**Note:** Version bump only for package cli-scripts - - -## [0.0.13](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@0.0.12...cli-scripts@0.0.13) (2018-08-06) - - - - -**Note:** Version bump only for package cli-scripts - - -## [0.0.12](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@0.0.11...cli-scripts@0.0.12) (2018-08-02) - - - - -**Note:** Version bump only for package cli-scripts - - -## [0.0.11](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@0.0.10...cli-scripts@0.0.11) (2018-07-30) - - - - -**Note:** Version bump only for package cli-scripts - - -## [0.0.10](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@0.0.9...cli-scripts@0.0.10) (2018-07-26) - - - - -**Note:** Version bump only for package cli-scripts - - -## [0.0.9](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@0.0.8...cli-scripts@0.0.9) (2018-07-25) - - - - -**Note:** Version bump only for package cli-scripts diff --git a/packages/cli-scripts/LICENSE b/packages/cli-scripts/LICENSE deleted file mode 100644 index 7c5808ced6..0000000000 --- a/packages/cli-scripts/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2017 Drifty Co - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/packages/cli-scripts/README.md b/packages/cli-scripts/README.md deleted file mode 100644 index ad6b0f901c..0000000000 --- a/packages/cli-scripts/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# Ionic CLI Scripts - -These are mostly internal scripts to manage CLI development. - -**Note**: This package only works as a package within the full lerna-powered -[CLI monorepo](https://github.com/ionic-team/ionic-cli). diff --git a/packages/cli-scripts/bin/ionic-cli-scripts b/packages/cli-scripts/bin/ionic-cli-scripts deleted file mode 100755 index 6dfa4ca3f2..0000000000 --- a/packages/cli-scripts/bin/ionic-cli-scripts +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env node - -'use strict'; - -process.title = 'ionic-cli-scripts'; -process.on('unhandledRejection', function(r) { console.error(r); }); - -var cli = require('../'); -cli.run(process.argv.slice(2), process.env); diff --git a/packages/cli-scripts/jest.config.js b/packages/cli-scripts/jest.config.js deleted file mode 100644 index f53d37480f..0000000000 --- a/packages/cli-scripts/jest.config.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('../../jest.config.base'); diff --git a/packages/cli-scripts/lint-staged.config.js b/packages/cli-scripts/lint-staged.config.js deleted file mode 100644 index a7e43fd870..0000000000 --- a/packages/cli-scripts/lint-staged.config.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('../../lint-staged.config.base'); diff --git a/packages/cli-scripts/package.json b/packages/cli-scripts/package.json deleted file mode 100644 index c472ef2ba0..0000000000 --- a/packages/cli-scripts/package.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "private": true, - "name": "cli-scripts", - "version": "2.1.75", - "bin": { - "ionic-cli-scripts": "./bin/ionic-cli-scripts" - }, - "scripts": { - "clean": "rimraf dist", - "lint": "true", - "build": "npm run clean && tsc", - "watch": "tsc -w --preserveWatchOutput", - "test": "jest --maxWorkers=4", - "prepublishOnly": "npm run build" - }, - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "dependencies": { - "@ionic/cli": "7.2.1", - "@ionic/cli-framework": "6.0.1", - "@ionic/utils-fs": "3.1.7", - "ansi-styles": "^4.0.0", - "chalk": "^4.0.0", - "escape-string-regexp": "^4.0.0", - "strip-ansi": "^6.0.0", - "tslib": "^2.0.1" - }, - "devDependencies": { - "@types/ansi-styles": "^3.2.0", - "@types/jest": "^26.0.10", - "@types/node": "~16.0.0", - "jest": "^26.4.2", - "jest-cli": "^26.0.1", - "lint-staged": "^10.0.2", - "rimraf": "^3.0.0", - "ts-jest": "~26.3.0", - "typescript": "~4.8.0" - } -} diff --git a/packages/cli-scripts/projects/angular/.gitignore b/packages/cli-scripts/projects/angular/.gitignore deleted file mode 100644 index 3c3629e647..0000000000 --- a/packages/cli-scripts/projects/angular/.gitignore +++ /dev/null @@ -1 +0,0 @@ -node_modules diff --git a/packages/cli-scripts/projects/angular/ionic.config.json b/packages/cli-scripts/projects/angular/ionic.config.json deleted file mode 100644 index 657d8a2270..0000000000 --- a/packages/cli-scripts/projects/angular/ionic.config.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "docs", - "type": "angular" -} diff --git a/packages/cli-scripts/src/docs/__tests__/utils.ts b/packages/cli-scripts/src/docs/__tests__/utils.ts deleted file mode 100644 index 3f79c1e963..0000000000 --- a/packages/cli-scripts/src/docs/__tests__/utils.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { ansi2md, convertHTMLEntities, links2md } from '../utils'; - -describe('cli-scripts', () => { - - describe('docs/utils', () => { - - describe('ansi2md', () => { - - it('should not affect regular text', () => { - const result = ansi2md('hello world'); - expect(result).toEqual('hello world'); - }); - - it('should strip yellow', () => { - const result = ansi2md('hello \u001b[33mworld\u001b[39m'); - expect(result).toEqual('hello world'); - }); - - it('should mark cyan as md code', () => { - const result = ansi2md('hello \u001b[36mworld\u001b[39m'); - expect(result).toEqual('hello `world`'); - }); - - it('should mark bold as md bold', () => { - const result = ansi2md('hello \u001b[1mworld\u001b[22m'); - expect(result).toEqual('hello **world**'); - }); - - }); - - describe('links2md', () => { - - it('should work with empty string', () => { - const result = links2md(''); - expect(result).toEqual(''); - }); - - it('should work with multiple lines', () => { - const result = links2md('hello\nworld!\n'); - expect(result).toEqual('hello\nworld!\n'); - }); - - it('should replace a link with md version', () => { - const website = 'https://ionicframework.com'; - const result = links2md(website); - expect(result).toEqual(`[${website}](${website})`); - }); - - it('should replace links with md', () => { - const website = 'https://ionicframework.com'; - const result = links2md(`visit my cool website: **${website}**\nthank you`); - expect(result).toEqual(`visit my cool website: **[${website}](${website})**\nthank you`); - }); - - it('should escape footnote numbers', () => { - const website = 'https://ionicframework.com'; - const result = links2md(`See the docs[1]\n\n[1]: **${website}**`); - expect(result).toEqual(`See the docs\\[1\\]\n\n\\[1\\]: **[${website}](${website})**`); - }); - - }); - - describe('convertHTMLEntities', () => { - - it('should not affect regular text', () => { - const result = convertHTMLEntities('hello world'); - expect(result).toEqual('hello world'); - }); - - it('should not affect symbols that are not tags', () => { - const result = convertHTMLEntities('I <3 TypeScript'); - expect(result).toEqual('I <3 TypeScript'); - }); - - it('should convert tag-like symbols', () => { - const result = convertHTMLEntities('in the resources/ folder'); - expect(result).toEqual('in the resources/<platform> folder'); - }); - - it('should respect tag-like symbols in code ticks', () => { - const result = convertHTMLEntities('in the `resources/` folder'); - expect(result).toEqual('in the `resources/` folder'); - }); - - }); - - }); - -}); diff --git a/packages/cli-scripts/src/docs/index.ts b/packages/cli-scripts/src/docs/index.ts deleted file mode 100644 index dbcf12c72e..0000000000 --- a/packages/cli-scripts/src/docs/index.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { ProjectType, generateContext, loadExecutor } from '@ionic/cli'; -import { Command, CommandHelpSchemaFootnote, CommandHelpSchemaInput, CommandHelpSchemaOption, CommandLineInputs, CommandLineOptions, CommandMetadata } from '@ionic/cli-framework'; -import { strcmp } from '@ionic/cli-framework/utils/string'; -import { CommandHelpSchema, NamespaceSchemaHelpFormatter } from '@ionic/cli/lib/help'; -import { mkdirp, remove, writeFile } from '@ionic/utils-fs'; -import chalk from 'chalk'; -import * as lodash from 'lodash'; -import * as path from 'path'; -import stripAnsi = require('strip-ansi'); - -import { ansi2md, convertHTMLEntities, links2md } from './utils'; - -const PROJECTS_DIRECTORY = path.resolve(__dirname, '..', '..', 'projects'); -const STAGING_DIRECTORY = path.resolve(__dirname, '..', '..', '..', '..', 'docs'); - -export class DocsCommand extends Command { - async getMetadata(): Promise { - return { - name: 'docs', - summary: '', - }; - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions) { - await remove(STAGING_DIRECTORY); - await mkdirp(STAGING_DIRECTORY); - - const projectTypes: ProjectType[] = ['angular']; - const baseCtx = await generateContext(); - - for (const projectType of projectTypes) { - // TODO: possible to do this without a physical directory? - const ctx = { ...baseCtx, execPath: path.resolve(PROJECTS_DIRECTORY, projectType) }; - const executor = await loadExecutor(ctx, []); - - const location = await executor.namespace.locate([]); - const formatter = new NamespaceSchemaHelpFormatter({ location, namespace: executor.namespace }); - const formatted = await formatter.serialize(); - const projectJson = { type: projectType, ...formatted }; - - // TODO: `serialize()` from base formatter isn't typed properly - projectJson.commands = await Promise.all(projectJson.commands.map(async cmd => this.extractCommand(cmd as CommandHelpSchema))); - projectJson.commands.sort((a, b) => strcmp(a.name, b.name)); - - await writeFile(path.resolve(STAGING_DIRECTORY, `${projectType}.json`), JSON.stringify(projectJson, undefined, 2) + '\n', { encoding: 'utf8' }); - } - - process.stdout.write(`${chalk.green('Done.')}\n`); - } - - private async extractCommand(command: CommandHelpSchema): Promise { - const processText = lodash.flow([ansi2md, stripAnsi, links2md, convertHTMLEntities, text => text.trim()]); - - return { - ...command, - summary: processText(command.summary), - description: await this.formatFootnotes(processText(command.description), command.footnotes), - footnotes: command.footnotes.filter(footnote => footnote.type !== 'link'), // we format link footnotes in `formatFootnotes()` - inputs: await Promise.all(command.inputs.map(input => this.extractInput(input))), - options: await Promise.all(command.options.map(opt => this.extractOption(opt))), - }; - } - - private async formatFootnotes(description: string, footnotes: readonly CommandHelpSchemaFootnote[]): Promise { - return description.replace(/(\S+)\[\^([A-z0-9-]+)\]/g, (match, p1, p2) => { - const m = Number.parseInt(p2, 10); - const id = !Number.isNaN(m) ? m : p2; - const foundFootnote = footnotes.find(footnote => footnote.id === id); - - if (!foundFootnote) { - throw new Error('Bad footnote.'); - } - - return foundFootnote.type === 'link' ? `[${p1}](${foundFootnote.url})` : match; // TODO: handle text footnotes - }); - } - - private async extractInput(input: CommandHelpSchemaInput): Promise { - return { - ...input, - summary: stripAnsi(links2md(ansi2md(input.summary))).trim(), - }; - } - - private async extractOption(option: CommandHelpSchemaOption): Promise { - return { - ...option, - summary: stripAnsi(links2md(ansi2md(option.summary))).trim(), - }; - } -} diff --git a/packages/cli-scripts/src/docs/utils.ts b/packages/cli-scripts/src/docs/utils.ts deleted file mode 100644 index 6f4247338d..0000000000 --- a/packages/cli-scripts/src/docs/utils.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Colors } from '@ionic/cli-framework'; -import { COLORS } from '@ionic/cli/lib/color'; -import chalk from 'chalk'; -import escapeStringRegexp from 'escape-string-regexp'; - -interface CodePair { - open: string; - close: string; -} - -interface Color { - _styler: CodePair; -} - -type ColorRegistry = { [K in keyof Colors]: Color }; - -export function links2md(str: string): string { - str = str.replace(/((http|https):\/\/(\w+:{0,1}\w*@)?([^\s\*\)`]+)(\/|\/([\w#!:.?+=&%@!\-\/]))?)/g, '[$1]($1)'); - str = str.replace(/\[(\d+)\]/g, '\\\[$1\\\]'); - return str; -} - -export function ansi2md(str: string): string { - const yellow = chalk.yellow as any as Color; - const { input, strong } = COLORS as any as ColorRegistry; - str = convertAnsiToMd(str, [input._styler], { open: '`', close: '`' }); - str = convertAnsiToMd(str, [yellow._styler], { open: '', close: '' }); - str = convertAnsiToMd(str, [strong._styler], { open: '**', close: '**' }); - return str; -} - -export function convertHTMLEntities(str: string): string { - return str.replace(/(?<=^(?:[^\`]|\`[^\`]*\`)*)\<(\S+)\>/g, '<$1>'); -} - -function convertAnsiToMd(str: string, styles: readonly CodePair[], md: CodePair): string { - const start = styles.map(style => style.open).join(''); - const end = [...styles].reverse().map(style => style.close).join(''); - const re = new RegExp(escapeStringRegexp(start) + '(.*?)' + escapeStringRegexp(end), 'g'); - - return str.replace(re, md.open + '$1' + md.close); -} diff --git a/packages/cli-scripts/src/index.ts b/packages/cli-scripts/src/index.ts deleted file mode 100644 index ec95c8e705..0000000000 --- a/packages/cli-scripts/src/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { CommandMap, Namespace, execute } from '@ionic/cli-framework'; - -import { DocsCommand } from './docs'; - -class CLIScriptsNamespace extends Namespace { - async getMetadata() { - return { - name: 'ionic-cli-scripts', - summary: '', - }; - } - - async getCommands(): Promise { - return new CommandMap([ - ['docs', async () => new DocsCommand(this)], - ]); - } -} - -const namespace = new CLIScriptsNamespace(); - -export async function run(argv: string[], env: NodeJS.ProcessEnv) { - await execute({ namespace, argv, env }); -} diff --git a/packages/cli-scripts/tsconfig.json b/packages/cli-scripts/tsconfig.json deleted file mode 100644 index f407c7c64b..0000000000 --- a/packages/cli-scripts/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist" - }, - "include": [ - "src/**/*.ts" - ], - "exclude": [ - "node_modules", - "src/**/__tests__/*.ts" - ] -} diff --git a/spec/cli.spec.js b/spec/cli.spec.js new file mode 100644 index 0000000000..07b240f7cb --- /dev/null +++ b/spec/cli.spec.js @@ -0,0 +1,342 @@ +var IonicAppLib = require('ionic-app-lib'), + Ionitron = require('../lib/ionic/ionitron'), + IonicCli = require('../lib/cli'), + Q = require('q'), + Task = require('../lib/ionic/task').Task, + Info = IonicAppLib.info, + Utils = IonicAppLib.utils, + rewire = require('rewire'); + +describe('Cli', function() { + + beforeEach(function() { + spyOn(IonicCli, 'processExit'); + spyOn(IonicCli, 'printAvailableTasks'); + spyOn(IonicCli, 'doRuntimeCheck'); + spyOn(IonicAppLib.events, 'on'); + spyOn(process, 'on'); + spyOn(Info, 'checkRuntime'); + + spyOn(Utils, 'fail').andCallFake(function(err){ + console.log(err); + console.log(err.stack); + throw err; + }); + }); + + it('should have cli defined', function() { + expect(IonicCli).toBeDefined(); + }); + + it('should have cli tasks defined', function() { + expect(IonicCli.Tasks).toBeDefined(); + }); + + describe('#run', function() { + + beforeEach(function() { + var fakeTask = function() {}; + fakeTask.prototype = new Task(); + fakeTask.prototype.run = function() {}; + + spyOn(IonicCli, 'lookupTask').andReturn(fakeTask); + }); + + describe('#Cli methods', function() { + it('should run checkLatestVersion on run', function() { + var deferred = Q.defer(); + deferred.resolve(); + spyOn(IonicCli, 'checkLatestVersion').andReturn(deferred); + IonicCli.run(['node', 'bin/ionic', 'run', 'ios']); + expect(IonicCli.checkLatestVersion).toHaveBeenCalled(); + }); + + it('should run info doRuntimeCheck on run', function() { + spyOn(IonicCli, 'printHelpLines'); + IonicCli.run(['node', 'bin/ionic', '--h']); + expect(IonicCli.doRuntimeCheck).toHaveBeenCalled(); + }); + + it('should run ionitron when argument is passed', function() { + spyOn(Ionitron, 'print'); + IonicCli.run(['node', 'bin/ionic', '--ionitron']); + expect(Ionitron.print).toHaveBeenCalled(); + }); + + it('should change log level to debug when verbose arg is passed', function() { + spyOn(IonicCli, 'tryBuildingTask').andReturn(false); + expect(IonicAppLib.logging.logger.level).toBe('info'); + IonicCli.run(['node', 'bin/ionic', '--verbose']); + expect(IonicAppLib.logging.logger.level).toBe('debug'); + }); + + it('should get version when version flag passed', function() { + spyOn(IonicCli, 'version'); + IonicCli.run(['node', 'bin/ionic', '--version']); + expect(IonicCli.version).toHaveBeenCalled(); + }); + + it('should call help when help argument passed', function() { + spyOn(IonicCli, 'printHelpLines'); + IonicCli.run(['node', 'bin/ionic', '--help']); + expect(IonicCli.printHelpLines).toHaveBeenCalled(); + }); + + it('should call help when help shorthand argument passed', function() { + spyOn(IonicCli, 'printHelpLines'); + IonicCli.run(['node', 'bin/ionic', '--h']); + expect(IonicCli.printHelpLines).toHaveBeenCalled(); + }); + + it('should print available tasks if no valid command is passed', function() { + spyOn(IonicCli, 'tryBuildingTask').andReturn(false); + IonicCli.run(['node', 'bin/ionic']); + expect(IonicCli.printAvailableTasks).toHaveBeenCalled(); + }); + + it('should get the correct task by name', function() { + var task = IonicCli.getTaskWithName('start'); + expect(task).toBeDefined(); + expect(task.name).toBe('start'); + expect(task.args).toBeDefined(); + }); + + it('should call attachErrorHandling', function() { + spyOn(IonicCli, 'attachErrorHandling'); + IonicCli.run(['node', 'bin/ionic']); + expect(IonicCli.attachErrorHandling).toHaveBeenCalled(); + }); + + it('should set up console logging helpers', function() { + spyOn(IonicCli, 'setUpConsoleLoggingHelpers'); + IonicCli.run(['node', 'bin/ionic']); + expect(IonicCli.setUpConsoleLoggingHelpers).toHaveBeenCalled(); + }); + + it('should get boolean options from start task', function() { + var tasks = require('../lib/tasks/cliTasks'); + var task = IonicCli.getTaskWithName('start'); + var booleanOptions = IonicCli.getBooleanOptionsForTask(task); + //We expect 6 total = 3 options, each with short hand notation. + expect(booleanOptions.length).toBe(6); + }); + }); + }); + + describe('#commands options', function() { + var fakeTask; + + beforeEach(function() { + fakeTask = function() {}; + fakeTask.prototype = new Task(); + fakeTask.prototype.run = function() {}; + + spyOn(IonicCli, 'lookupTask').andReturn(fakeTask); + spyOn(fakeTask.prototype, 'run').andCallThrough(); + + }); + + it('should parse start options correctly', function() { + var processArgs = [ 'node', '/usr/local/bin/ionic', 'start', 's1', '-w', '--appname', 'asdf']; + + IonicCli.run(processArgs); + + expect(fakeTask.prototype.run).toHaveBeenCalled(); + var taskArgs = fakeTask.prototype.run.mostRecentCall.args; + + var taskArgv = taskArgs[1]; + expect(taskArgv._.length).toBe(2); + expect(taskArgv._[0]).toBe('start'); + expect(taskArgv._[1]).toBe('s1'); + expect(taskArgv.appname).toBe('asdf'); + expect(taskArgv.s).toBe(false); + expect(taskArgv.sass).toBe(false); + expect(taskArgv.l).toBe(false); + expect(taskArgv.list).toBe(false); + expect(taskArgv['no-cordova']).toBe(false); + expect(taskArgv.w).toBe(true); + expect(taskArgv.id).toBeUndefined(); + expect(taskArgv.i).toBeUndefined(); + }); + + it('should parse serve options correctly', function() { + var processArgs = [ 'node', '/usr/local/bin/ionic', 'serve', '--nogulp', '--all', '--browser', 'firefox']; + + IonicCli.run(processArgs); + + expect(fakeTask.prototype.run).toHaveBeenCalled(); + var taskArgs = fakeTask.prototype.run.mostRecentCall.args; + + var taskArgv = taskArgs[1]; + // console.log('taskArgv', taskArgv); + //should only have serve in the command args + expect(taskArgv._.length).toBe(1); + expect(taskArgv.browser).toBe('firefox'); + expect(taskArgv.nogulp).toBe(true); + expect(taskArgv.all).toBe(true); + expect(taskArgv.lab).toBe(false); + expect(taskArgv.nobrowser).toBe(false); + + }); + + + it('should parse upload options correctly', function() { + var note = 'A note for notes'; + var processArgs = [ 'node', '/usr/local/bin/ionic', 'upload', '--email', 'user@ionic.io', '--password', 'pass', '--note', note]; + + IonicCli.run(processArgs); + + expect(fakeTask.prototype.run).toHaveBeenCalled(); + var taskArgs = fakeTask.prototype.run.mostRecentCall.args; + + var taskArgv = taskArgs[1]; + + //should only have serve in the command args + expect(taskArgv._.length).toBe(1); + expect(taskArgv.note).toBe(note); + expect(taskArgv.email).toBe('user@ionic.io'); + expect(taskArgv.password).toBe('pass'); + + }); + + it('should parse login options correctly', function() { + var processArgs = [ 'node', '/usr/local/bin/ionic', 'login', '--email', 'user@ionic.io', '--password', 'pass']; + + IonicCli.run(processArgs); + + expect(fakeTask.prototype.run).toHaveBeenCalled(); + var taskArgs = fakeTask.prototype.run.mostRecentCall.args; + + var taskArgv = taskArgs[1]; + + //should only have serve in the command args + expect(taskArgv._.length).toBe(1); + expect(taskArgv.email).toBe('user@ionic.io'); + expect(taskArgv.password).toBe('pass'); + }); + + it('should parse run options correctly', function() { + var processArgs = [ 'node', '/usr/local/bin/ionic', 'run', 'ios', '--livereload', '--port', '5000', '-r', '35730', '--consolelogs', '--serverlogs', '--device']; + + IonicCli.run(processArgs); + + expect(fakeTask.prototype.run).toHaveBeenCalled(); + var taskArgs = fakeTask.prototype.run.mostRecentCall.args; + + var taskArgv = taskArgs[1]; + + //should only have serve in the command args + expect(taskArgv._.length).toBe(2); + expect(taskArgv.r).toBe(35730); + expect(taskArgv.port).toBe(5000); + expect(taskArgv.consolelogs).toBe(true); + expect(taskArgv.serverlogs).toBe(true); + expect(taskArgv.livereload).toBe(true); + expect(taskArgv.device).toBe(true); + + }); + + it('should parse emulate options correctly', function() { + var processArgs = [ 'node', '/usr/local/bin/ionic', 'emulate', 'android', '--livereload', '--address', 'localhost', '--port', '5000', '-r', '35730', '--consolelogs', '--serverlogs']; + + IonicCli.run(processArgs); + + expect(fakeTask.prototype.run).toHaveBeenCalled(); + var taskArgs = fakeTask.prototype.run.mostRecentCall.args; + + var taskArgv = taskArgs[1]; + + //should only have serve in the command args + expect(taskArgv._.length).toBe(2); + expect(taskArgv._[1]).toBe('android'); + expect(taskArgv.r).toBe(35730); + expect(taskArgv.address).toBe('localhost'); + expect(taskArgv.port).toBe(5000); + expect(taskArgv.consolelogs).toBe(true); + expect(taskArgv.serverlogs).toBe(true); + expect(taskArgv.livereload).toBe(true); + }); + + it('should parse state options correctly', function() { + var processArgs = [ 'node', '/usr/local/bin/ionic', 'state', 'save', '--plugins']; + + IonicCli.run(processArgs); + + expect(fakeTask.prototype.run).toHaveBeenCalled(); + var taskArgs = fakeTask.prototype.run.mostRecentCall.args; + + var taskArgv = taskArgs[1]; + + //should only have serve in the command args + expect(taskArgv._.length).toBe(2); + expect(taskArgv._[1]).toBe('save'); + expect(taskArgv.plugins).toBe(true); + expect(taskArgv.platforms).toBe(false); + }); + + it('should parse plugin options correctly', function() { + var processArgs = [ 'node', '/usr/local/bin/ionic', 'plugin', 'add', 'org.apache.cordova.splashscreen', '--nosave', '--searchpath', '../']; + + IonicCli.run(processArgs); + + expect(fakeTask.prototype.run).toHaveBeenCalled(); + var taskArgs = fakeTask.prototype.run.mostRecentCall.args; + + var taskArgv = taskArgs[1]; + + //should only have serve in the command args + expect(taskArgv._.length).toBe(3); + expect(taskArgv._[0]).toBe('plugin'); + expect(taskArgv._[1]).toBe('add'); + expect(taskArgv._[2]).toBe('org.apache.cordova.splashscreen'); + expect(taskArgv.nosave).toBe(true); + expect(taskArgv.searchpath).toBe('../'); + }); + + it('should parse build options correctly', function() { + var processArgs = [ 'node', '/usr/local/bin/ionic', 'build', 'ios', '--nohooks']; + + IonicCli.run(processArgs); + + expect(fakeTask.prototype.run).toHaveBeenCalled(); + var taskArgs = fakeTask.prototype.run.mostRecentCall.args; + + var taskArgv = taskArgs[1]; + + //should only have serve in the command args + expect(taskArgv._.length).toBe(2); + expect(taskArgv._[0]).toBe('build'); + expect(taskArgv._[1]).toBe('ios'); + expect(taskArgv.nohooks).toBe(true); + }); + + describe('version checking for checkRuntime', function() { + var IonicCli; + beforeEach(function() { + IonicCli = rewire('../lib/cli'); + }); + + it('should do runtime check when version is not checked', function() { + var IonicConfigSpy = createSpyObj('IonicConfig', ['get', 'set', 'save']); + IonicConfigSpy.get.andReturn('1.6.4'); + IonicCli.__set__('IonicConfig', IonicConfigSpy); + IonicCli.doRuntimeCheck('1.6.4'); + expect(Info.checkRuntime).not.toHaveBeenCalled(); + expect(IonicConfigSpy.set).not.toHaveBeenCalled(); + expect(IonicConfigSpy.save).not.toHaveBeenCalled(); + }); + + it('should do runtime check when version is not checked', function() { + var IonicConfigSpy = createSpyObj('IonicConfig', ['get', 'set', 'save']); + IonicConfigSpy.get.andReturn('1.6.4'); + IonicCli.__set__('IonicConfig', IonicConfigSpy); + IonicCli.doRuntimeCheck('1.6.5'); + expect(Info.checkRuntime).toHaveBeenCalled(); + expect(IonicConfigSpy.get).toHaveBeenCalled(); + expect(IonicConfigSpy.set).toHaveBeenCalledWith('lastVersionChecked', '1.6.5'); + expect(IonicConfigSpy.save).toHaveBeenCalled(); + }); + }) + + }); +}); diff --git a/tsconfig.base.json b/tsconfig.base.json deleted file mode 100644 index d13a4a9107..0000000000 --- a/tsconfig.base.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "compilerOptions": { - "allowUnreachableCode": false, - "declaration": true, - "importHelpers": true, - "module": "commonjs", - "moduleResolution": "node", - "noFallthroughCasesInSwitch": true, - "esModuleInterop": true, - "noUnusedLocals": true, - "pretty": true, - "strict": true, - "target": "ES2021", - "types": [], - "lib": ["ES2021", "ES2022.Error"] - } -} diff --git a/types/leek.d.ts b/types/leek.d.ts deleted file mode 100644 index e128d5dbe9..0000000000 --- a/types/leek.d.ts +++ /dev/null @@ -1,43 +0,0 @@ -declare module 'leek' { - namespace Leek { - interface LeekOptions { - name: string; - trackingCode: string; - globalName: string; - version: string; - adapterUrls?: string[]; - adapterServers?: string[]; - silent?: boolean; - } - interface TrackOptions { - name: string; - message: string; - } - interface TrackErrorOptions { - description: string; - isFatal: boolean; - } - interface TrackTimingOptions { - category: string; - variable: string; - value: string; - label: string; - } - interface TrackEventOptions { - name: string; - category: string; - label: string; - value: string; - } - } - class Leek { - constructor(options: Leek.LeekOptions); - setName(value: string): void; - track(meta: Leek.TrackOptions): Promise; - trackError(meta: Leek.TrackErrorOptions): Promise; - trackTiming(meta: Leek.TrackTimingOptions): Promise - trackEvent(meta: Leek.TrackEventOptions): Promise; - } - - export = Leek; -} diff --git a/types/ssh-config.d.ts b/types/ssh-config.d.ts deleted file mode 100644 index 7074213683..0000000000 --- a/types/ssh-config.d.ts +++ /dev/null @@ -1,45 +0,0 @@ -declare module "ssh-config" { - namespace SSHConfig { - var DIRECTIVE: number; // 1 - var COMMENT: number; // 2 - - interface SSHConfigFindOptions { - Host?: string; - Match?: string; - } - - interface ConfigComment { - type: typeof COMMENT; - content: string; - before: string; - after: string; - } - - interface ConfigDirective { - type: typeof DIRECTIVE; - before: string; - after: string; - param: string; - value: string; - separator: string; - } - - interface ConfigHostDirective extends ConfigDirective { - config: SSHConfig; - } - - type ConfigMatchDirective = ConfigHostDirective; - - type Config = ConfigComment | ConfigDirective | ConfigHostDirective | ConfigMatchDirective; - - interface SSHConfig extends Array { - find(arg: any): any; // https://github.com/dotnil/ssh-config/blob/40b55c0e31790dd78a0d4364b0e8a0a358293385/index.js#L61 - } - - // function find(config: SSHConfig, options?: SSHConfigFindOptions): ; - function parse(str: string): SSHConfig; - function stringify(config: SSHConfig): string; - } - - export = SSHConfig; -} diff --git a/types/stream-combiner2.d.ts b/types/stream-combiner2.d.ts deleted file mode 100644 index f5aa5d9762..0000000000 --- a/types/stream-combiner2.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -declare module 'stream-combiner2' { - function combine(...writables: NodeJS.WritableStream[]): NodeJS.WritableStream; - - namespace combine {} - - export = combine; -} diff --git a/types/tiny-lr.d.ts b/types/tiny-lr.d.ts deleted file mode 100644 index b1333b9a80..0000000000 --- a/types/tiny-lr.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -declare module "tiny-lr" { - import * as http from 'http'; - - namespace TinyLR {} - - interface TinyLRServer { - listen(port: number, host: string, cb?: () => {}): void; - close(): void; - changed(p: any): void; - } - - function TinyLR(): TinyLRServer; - - export = TinyLR; -}