From 1b916ac89941a4ba0450c6a0660e901a0f5f7d6c Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 30 Dec 2022 16:44:14 +0530 Subject: [PATCH 001/112] refactor: rewrite from scratch --- .github/COMMIT_CONVENTION.md | 70 - .github/CONTRIBUTING.md | 46 - .github/ISSUE_TEMPLATE/bug_report.md | 29 - .github/ISSUE_TEMPLATE/feature_request.md | 28 - .github/PULL_REQUEST_TEMPLATE.md | 28 - .github/labels.json | 170 ++ .github/workflows/test.yml | 36 +- .gitignore | 1 + .husky/commit-msg | 7 +- CHANGELOG.md | 256 --- bin/{japaTypes.ts => japa_types.ts} | 0 bin/test.ts | 5 +- example/index.ts | 117 -- examples/main.ts | 125 ++ examples/parent.ts | 5 + index.ts | 17 - package.json | 135 +- src/BaseCommand/index.ts | 323 ---- src/Contracts/index.ts | 445 ----- src/Decorators/args.ts | 47 +- src/Decorators/flags.ts | 77 +- src/Exceptions/index.ts | 154 -- src/Generator/File.ts | 147 -- src/Generator/StringTransformer.ts | 124 -- src/Generator/index.ts | 65 - src/HelpCommand/index.ts | 23 - src/Hooks/index.ts | 53 - src/Kernel/index.ts | 755 -------- src/Manifest/Generator.ts | 117 -- src/Manifest/Loader.ts | 149 -- src/Parser/index.ts | 342 ---- src/commands/base.ts | 509 +++++ src/commands/help.ts | 173 ++ src/commands/list.ts | 163 ++ src/errors.ts | 109 ++ src/formatters/argument.ts | 73 + src/formatters/command.ts | 111 ++ src/formatters/flag.ts | 133 ++ src/formatters/info.ts | 67 + src/formatters/list.ts | 64 + src/helpers.ts | 47 + src/kernel.ts | 752 ++++++++ src/loaders/list.ts | 38 + src/parser.ts | 141 ++ src/types.ts | 375 ++++ src/utils/handleError.ts | 24 - src/utils/help.ts | 337 ---- src/utils/listDirectoryFiles.ts | 52 - src/utils/sortAndGroupCommands.ts | 76 - src/utils/template.ts | 59 - src/utils/validateCommand.ts | 77 - src/yars_config.ts | 30 + test-helpers/index.ts | 30 - test/fixtures/template1.mustache | 1 - test/fixtures/template1.txt | 1 - test/generator-file.spec.ts | 265 --- test/generator.spec.ts | 61 - test/kernel-flow.spec.ts | 1347 ------------- test/kernel.spec.ts | 2135 --------------------- test/list-directory-files.spec.ts | 87 - test/manifest-generator.spec.ts | 299 --- test/manifest-loader.spec.ts | 507 ----- test/parser.spec.ts | 346 ---- test/sort-commands.spec.ts | 82 - test/template.spec.ts | 91 - tests/base_command/define_args.spec.ts | 161 ++ tests/base_command/define_flags.spec.ts | 213 ++ tests/base_command/exec.spec.ts | 320 +++ tests/base_command/main.spec.ts | 150 ++ tests/base_command/serialize.spec.ts | 138 ++ tests/base_command/validate.spec.ts | 458 +++++ tests/commands/list.spec.ts | 216 +++ tests/decorators/args.spec.ts | 71 + tests/decorators/flags.spec.ts | 42 + tests/formatters/arg.spec.ts | 90 + tests/formatters/command.spec.ts | 237 +++ tests/formatters/flag.spec.ts | 186 ++ tests/formatters/list.spec.ts | 119 ++ tests/helpers.spec.ts | 45 + tests/kernel/boot.spec.ts | 106 + tests/kernel/default_command.spec.ts | 42 + tests/kernel/exec.spec.ts | 278 +++ tests/kernel/find.spec.ts | 180 ++ tests/kernel/flag_listeners.spec.ts | 152 ++ tests/kernel/gloal_flags.spec.ts | 48 + tests/kernel/handle.spec.ts | 258 +++ tests/kernel/loaders.spec.ts | 84 + tests/kernel/main.spec.ts | 305 +++ tests/kernel/terminate.spec.ts | 86 + tests/parser.spec.ts | 448 +++++ tsconfig.json | 35 +- 91 files changed, 7677 insertions(+), 9349 deletions(-) delete mode 100644 .github/COMMIT_CONVENTION.md delete mode 100644 .github/CONTRIBUTING.md delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.md delete mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/labels.json delete mode 100644 CHANGELOG.md rename bin/{japaTypes.ts => japa_types.ts} (100%) delete mode 100644 example/index.ts create mode 100644 examples/main.ts create mode 100644 examples/parent.ts delete mode 100644 index.ts delete mode 100644 src/BaseCommand/index.ts delete mode 100644 src/Contracts/index.ts delete mode 100644 src/Exceptions/index.ts delete mode 100644 src/Generator/File.ts delete mode 100644 src/Generator/StringTransformer.ts delete mode 100644 src/Generator/index.ts delete mode 100644 src/HelpCommand/index.ts delete mode 100644 src/Hooks/index.ts delete mode 100644 src/Kernel/index.ts delete mode 100644 src/Manifest/Generator.ts delete mode 100644 src/Manifest/Loader.ts delete mode 100644 src/Parser/index.ts create mode 100644 src/commands/base.ts create mode 100644 src/commands/help.ts create mode 100644 src/commands/list.ts create mode 100644 src/errors.ts create mode 100644 src/formatters/argument.ts create mode 100644 src/formatters/command.ts create mode 100644 src/formatters/flag.ts create mode 100644 src/formatters/info.ts create mode 100644 src/formatters/list.ts create mode 100644 src/helpers.ts create mode 100644 src/kernel.ts create mode 100644 src/loaders/list.ts create mode 100644 src/parser.ts create mode 100644 src/types.ts delete mode 100644 src/utils/handleError.ts delete mode 100644 src/utils/help.ts delete mode 100644 src/utils/listDirectoryFiles.ts delete mode 100644 src/utils/sortAndGroupCommands.ts delete mode 100644 src/utils/template.ts delete mode 100644 src/utils/validateCommand.ts create mode 100644 src/yars_config.ts delete mode 100644 test-helpers/index.ts delete mode 100644 test/fixtures/template1.mustache delete mode 100644 test/fixtures/template1.txt delete mode 100644 test/generator-file.spec.ts delete mode 100644 test/generator.spec.ts delete mode 100644 test/kernel-flow.spec.ts delete mode 100644 test/kernel.spec.ts delete mode 100644 test/list-directory-files.spec.ts delete mode 100644 test/manifest-generator.spec.ts delete mode 100644 test/manifest-loader.spec.ts delete mode 100644 test/parser.spec.ts delete mode 100644 test/sort-commands.spec.ts delete mode 100644 test/template.spec.ts create mode 100644 tests/base_command/define_args.spec.ts create mode 100644 tests/base_command/define_flags.spec.ts create mode 100644 tests/base_command/exec.spec.ts create mode 100644 tests/base_command/main.spec.ts create mode 100644 tests/base_command/serialize.spec.ts create mode 100644 tests/base_command/validate.spec.ts create mode 100644 tests/commands/list.spec.ts create mode 100644 tests/decorators/args.spec.ts create mode 100644 tests/decorators/flags.spec.ts create mode 100644 tests/formatters/arg.spec.ts create mode 100644 tests/formatters/command.spec.ts create mode 100644 tests/formatters/flag.spec.ts create mode 100644 tests/formatters/list.spec.ts create mode 100644 tests/helpers.spec.ts create mode 100644 tests/kernel/boot.spec.ts create mode 100644 tests/kernel/default_command.spec.ts create mode 100644 tests/kernel/exec.spec.ts create mode 100644 tests/kernel/find.spec.ts create mode 100644 tests/kernel/flag_listeners.spec.ts create mode 100644 tests/kernel/gloal_flags.spec.ts create mode 100644 tests/kernel/handle.spec.ts create mode 100644 tests/kernel/loaders.spec.ts create mode 100644 tests/kernel/main.spec.ts create mode 100644 tests/kernel/terminate.spec.ts create mode 100644 tests/parser.spec.ts diff --git a/.github/COMMIT_CONVENTION.md b/.github/COMMIT_CONVENTION.md deleted file mode 100644 index fc852af..0000000 --- a/.github/COMMIT_CONVENTION.md +++ /dev/null @@ -1,70 +0,0 @@ -## Git Commit Message Convention - -> This is adapted from [Angular's commit convention](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular). - -Using conventional commit messages, we can automate the process of generating the CHANGELOG file. All commits messages will automatically be validated against the following regex. - -``` js -/^(revert: )?(feat|fix|docs|style|refactor|perf|test|workflow|ci|chore|types|build|improvement)((.+))?: .{1,50}/ -``` - -## Commit Message Format -A commit message consists of a **header**, **body** and **footer**. The header has a **type**, **scope** and **subject**: - -> The **scope** is optional - -``` -feat(router): add support for prefix - -Prefix makes it easier to append a path to a group of routes -``` - -1. `feat` is type. -2. `router` is scope and is optional -3. `add support for prefix` is the subject -4. The **body** is followed by a blank line. -5. The optional **footer** can be added after the body, followed by a blank line. - -## Types -Only one type can be used at a time and only following types are allowed. - -- feat -- fix -- docs -- style -- refactor -- perf -- test -- workflow -- ci -- chore -- types -- build - -If a type is `feat`, `fix` or `perf`, then the commit will appear in the CHANGELOG.md file. However if there is any BREAKING CHANGE, the commit will always appear in the changelog. - -### Revert -If the commit reverts a previous commit, it should begin with `revert:`, followed by the header of the reverted commit. In the body it should say: `This reverts commit `., where the hash is the SHA of the commit being reverted. - -## Scope -The scope could be anything specifying place of the commit change. For example: `router`, `view`, `querybuilder`, `database`, `model` and so on. - -## Subject -The subject contains succinct description of the change: - -- use the imperative, present tense: "change" not "changed" nor "changes". -- don't capitalize first letter -- no dot (.) at the end - -## Body - -Just as in the **subject**, use the imperative, present tense: "change" not "changed" nor "changes". -The body should include the motivation for the change and contrast this with previous behavior. - -## Footer - -The footer should contain any information about **Breaking Changes** and is also the place to -reference GitHub issues that this commit **Closes**. - -**Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines. The rest of the commit message is then used for this. - diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md deleted file mode 100644 index f0c5446..0000000 --- a/.github/CONTRIBUTING.md +++ /dev/null @@ -1,46 +0,0 @@ -# Contributing - -AdonisJS is a community driven project. You are free to contribute in any of the following ways. - -- [Coding style](coding-style) -- [Fix bugs by creating PR's](fix-bugs-by-creating-prs) -- [Share an RFC for new features or big changes](share-an-rfc-for-new-features-or-big-changes) -- [Report security issues](report-security-issues) -- [Be a part of the community](be-a-part-of-community) - -## Coding style - -Majority of AdonisJS core packages are written in Typescript. Having a brief knowledge of Typescript is required to contribute to the core. - -## Fix bugs by creating PR's - -We appreciate every time you report a bug in the framework or related libraries. However, taking time to submit a PR can help us in fixing bugs quickly and ensure a healthy and stable eco-system. - -Go through the following points, before creating a new PR. - -1. Create an issue discussing the bug or short-coming in the framework. -2. Once approved, go ahead and fork the REPO. -3. Make sure to start from the `develop`, since this is the upto date branch. -4. Make sure to keep commits small and relevant. -5. We follow [conventional-commits](https://github.com/conventional-changelog/conventional-changelog) to structure our commit messages. Instead of running `git commit`, you must run `npm commit`, which will show you prompts to create a valid commit message. -6. Once done with all the changes, create a PR against the `develop` branch. - -## Share an RFC for new features or big changes - -Sharing PR's for small changes works great. However, when contributing big features to the framework, it is required to go through the RFC process. - -### What is an RFC? - -RFC stands for **Request for Commits**, a standard process followed by many other frameworks including [Ember](https://github.com/emberjs/rfcs), [yarn](https://github.com/yarnpkg/rfcs) and [rust](https://github.com/rust-lang/rfcs). - -In brief, RFC process allows you to talk about the changes with everyone in the community and get a view of the core team before dedicating your time to work on the feature. - -The RFC proposals are created as Pull Request on [adonisjs/rfcs](https://github.com/adonisjs/rfcs) repo. Make sure to read the README to learn about the process in depth. - -## Report security issues - -All of the security issues, must be reported via [email](mailto:virk@adonisjs.com) and not using any of the public channels. - -## Be a part of community - -We welcome you to participate in [GitHub Discussion](https://github.com/adonisjs/core/discussions) and the AdonisJS [Discord Server](https://discord.gg/vDcEjq6). You are free to ask your questions and share your work or contributions made to AdonisJS eco-system. diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index e65000c..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -name: Bug report -about: Report identified bugs ---- - - - -## Prerequisites - -We do our best to reply to all the issues on time. If you will follow the given guidelines, the turn around time will be faster. - -- Lots of raised issues are directly not bugs but instead are design decisions taken by us. -- Make use of our [GH discussions](https://github.com/adonisjs/core/discussions), or [discord server](https://discord.me/adonisjs), if you are not sure that you are reporting a bug. -- Ensure the issue isn't already reported. -- Ensure you are reporting the bug in the correct repo. - -*Delete the above section and the instructions in the sections below before submitting* - -## Package version - - -## Node.js and npm version - - -## Sample Code (to reproduce the issue) - - -## BONUS (a sample repo to reproduce the issue) - diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index abd44a5..0000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -name: Feature request -about: Propose changes for adding a new feature ---- - - - -## Prerequisites - -We do our best to reply to all the issues on time. If you will follow the given guidelines, the turn around time will be faster. - -## Consider an RFC - -Please create an [RFC](https://github.com/adonisjs/rfcs) instead, if - -- Feature introduces a breaking change -- Demands lots of time and changes in the current code base. - -*Delete the above section and the instructions in the sections below before submitting* - -## Why this feature is required (specific use-cases will be appreciated)? - - -## Have you tried any other work arounds? - - -## Are you willing to work on it with little guidance? - diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 1c38707..0000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,28 +0,0 @@ - - -## Proposed changes - -Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. If it fixes a bug or resolves a feature request, be sure to link to that issue. - -## Types of changes - -What types of changes does your code introduce? - -_Put an `x` in the boxes that apply_ - -- [ ] Bugfix (non-breaking change which fixes an issue) -- [ ] New feature (non-breaking change which adds functionality) -- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - -## Checklist - -_Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code._ - -- [ ] I have read the [CONTRIBUTING](https://github.com/adonisjs/ace/blob/master/.github/CONTRIBUTING.md) doc -- [ ] Lint and unit tests pass locally with my changes -- [ ] I have added tests that prove my fix is effective or that my feature works. -- [ ] I have added necessary documentation (if appropriate) - -## Further comments - -If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... diff --git a/.github/labels.json b/.github/labels.json new file mode 100644 index 0000000..ba001c6 --- /dev/null +++ b/.github/labels.json @@ -0,0 +1,170 @@ +[ + { + "name": "Priority: Critical", + "color": "ea0056", + "description": "The issue needs urgent attention", + "aliases": [] + }, + { + "name": "Priority: High", + "color": "5666ed", + "description": "Look into this issue before picking up any new work", + "aliases": [] + }, + { + "name": "Priority: Medium", + "color": "f4ff61", + "description": "Try to fix the issue for the next patch/minor release", + "aliases": [] + }, + { + "name": "Priority: Low", + "color": "87dfd6", + "description": "Something worth considering, but not a top priority for the team", + "aliases": [] + }, + { + "name": "Semver: Alpha", + "color": "008480", + "description": "Will make it's way to the next alpha version of the package", + "aliases": [] + }, + { + "name": "Semver: Major", + "color": "ea0056", + "description": "Has breaking changes", + "aliases": [] + }, + { + "name": "Semver: Minor", + "color": "fbe555", + "description": "Mainly new features and improvements", + "aliases": [] + }, + { + "name": "Semver: Next", + "color": "5666ed", + "description": "Will make it's way to the bleeding edge version of the package", + "aliases": [] + }, + { + "name": "Semver: Patch", + "color": "87dfd6", + "description": "A bug fix", + "aliases": [] + }, + { + "name": "Status: Abandoned", + "color": "ffffff", + "description": "Dropped and not into consideration", + "aliases": ["wontfix"] + }, + { + "name": "Status: Accepted", + "color": "e5fbf2", + "description": "The proposal or the feature has been accepted for the future versions", + "aliases": [] + }, + { + "name": "Status: Blocked", + "color": "ea0056", + "description": "The work on the issue or the PR is blocked. Check comments for reasoning", + "aliases": [] + }, + { + "name": "Status: Completed", + "color": "008672", + "description": "The work has been completed, but not released yet", + "aliases": [] + }, + { + "name": "Status: In Progress", + "color": "73dbc4", + "description": "Still banging the keyboard", + "aliases": ["in progress"] + }, + { + "name": "Status: On Hold", + "color": "f4ff61", + "description": "The work was started earlier, but is on hold now. Check comments for reasoning", + "aliases": ["On Hold"] + }, + { + "name": "Status: Review Needed", + "color": "fbe555", + "description": "Review from the core team is required before moving forward", + "aliases": [] + }, + { + "name": "Status: Awaiting More Information", + "color": "89f8ce", + "description": "Waiting on the issue reporter or PR author to provide more information", + "aliases": [] + }, + { + "name": "Status: Need Contributors", + "color": "7057ff", + "description": "Looking for contributors to help us move forward with this issue or PR", + "aliases": [] + }, + { + "name": "Type: Bug", + "color": "ea0056", + "description": "The issue has indentified a bug", + "aliases": ["bug"] + }, + { + "name": "Type: Security", + "color": "ea0056", + "description": "Spotted security vulnerability and is a top priority for the core team", + "aliases": [] + }, + { + "name": "Type: Duplicate", + "color": "00837e", + "description": "Already answered or fixed previously", + "aliases": ["duplicate"] + }, + { + "name": "Type: Enhancement", + "color": "89f8ce", + "description": "Improving an existing feature", + "aliases": ["enhancement"] + }, + { + "name": "Type: Feature Request", + "color": "483add", + "description": "Request to add a new feature to the package", + "aliases": [] + }, + { + "name": "Type: Invalid", + "color": "dbdbdb", + "description": "Doesn't really belong here. Maybe use discussion threads?", + "aliases": ["invalid"] + }, + { + "name": "Type: Question", + "color": "eceafc", + "description": "Needs clarification", + "aliases": ["help wanted", "question"] + }, + { + "name": "Type: Documentation Change", + "color": "7057ff", + "description": "Documentation needs some improvements", + "aliases": ["documentation"] + }, + { + "name": "Type: Dependencies Update", + "color": "00837e", + "description": "Bump dependencies", + "aliases": ["dependencies"] + }, + { + "name": "Good First Issue", + "color": "008480", + "description": "Want to contribute? Just filter by this label", + "aliases": ["good first issue"] + } +] diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 788df91..2d9bc9e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,37 +3,5 @@ on: - push - pull_request jobs: - linux: - runs-on: ubuntu-latest - strategy: - matrix: - node-version: - - 14.15.4 - - 17.x - steps: - - uses: actions/checkout@v2 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node-version }} - - name: Install - run: npm install - - name: Run tests - run: npm test - windows: - runs-on: windows-latest - strategy: - matrix: - node-version: - - 14.15.4 - - 17.x - steps: - - uses: actions/checkout@v2 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node-version }} - - name: Install - run: npm install - - name: Run tests - run: npm test + test: + uses: adonisjs/.github/.github/workflows/test.yml@main diff --git a/.gitignore b/.gitignore index 69c8fe6..13aacb6 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ yarn.lock shrinkwrap.yaml package-lock.json test/__app +backup diff --git a/.husky/commit-msg b/.husky/commit-msg index 4654c12..4002db7 100755 --- a/.husky/commit-msg +++ b/.husky/commit-msg @@ -1,3 +1,4 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" -HUSKY_GIT_PARAMS=$1 node ./node_modules/@adonisjs/mrm-preset/validate-commit/conventional/validate.js +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +npx --no -- commitlint --edit diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 7c38a2d..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,256 +0,0 @@ - -## [5.0.8](https://github.com/adonisjs/ace/compare/v5.0.7...v5.0.8) (2018-10-01) - - -### Bug Fixes - -* **command:** disable kluer when no ansi is true ([6ec44e8](https://github.com/adonisjs/ace/commit/6ec44e8)) -* **command:** properly handle no-ansi flag ([3b68504](https://github.com/adonisjs/ace/commit/3b68504)) - - - - -## [5.0.7](https://github.com/adonisjs/ace/compare/v5.0.6...v5.0.7) (2018-10-01) - - -### Bug Fixes - -* **package:** remove bin paths ([d529e97](https://github.com/adonisjs/ace/commit/d529e97)) - - - - -## [5.0.6](https://github.com/adonisjs/ace/compare/v5.0.5...v5.0.6) (2018-10-01) - - - - -## [5.0.5](https://github.com/adonisjs/ace/compare/v5.0.4...v5.0.5) (2018-09-05) - - -### Bug Fixes - -* **help:** doesn't display similar command when none is found ([#81](https://github.com/adonisjs/ace/issues/81)) ([3a3eb26](https://github.com/adonisjs/ace/commit/3a3eb26)) - - - - -## [5.0.4](https://github.com/adonisjs/ace/compare/v5.0.3...v5.0.4) (2018-08-03) - - -### Features - -* **colors:** use kleur instead of chalk ([#80](https://github.com/adonisjs/ace/issues/80)) ([193f30f](https://github.com/adonisjs/ace/commit/193f30f)) -* **kernel:** display help when command isn't found ([#79](https://github.com/adonisjs/ace/issues/79)) ([343cdcf](https://github.com/adonisjs/ace/commit/343cdcf)) - - - - -## [5.0.3](https://github.com/adonisjs/ace/compare/v5.0.1...v5.0.3) (2018-07-16) - - -### Bug Fixes - -* **package:** update fs-extra to version 6.0.0 ([#76](https://github.com/adonisjs/ace/issues/76)) ([1065807](https://github.com/adonisjs/ace/commit/1065807)) - - - - -## [5.0.2](https://github.com/adonisjs/ace/compare/v5.0.1...v5.0.2) (2018-06-04) - - - - -## [5.0.1](https://github.com/adonisjs/ace/compare/v5.0.0...v5.0.1) (2018-03-04) - - - - -# [5.0.0](https://github.com/adonisjs/ace/compare/v4.0.8...v5.0.0) (2018-02-07) - - -### Chores - -* **prompts:** use enquirer over inquirer ([13eae0f](https://github.com/adonisjs/ace/commit/13eae0f)) - - -### BREAKING CHANGES - -* **prompts:** removed openEditor fn - - - - -## [4.0.8](https://github.com/adonisjs/ace/compare/v4.0.7...v4.0.8) (2018-01-19) - - -### Features - -* **kernel:** expose method to listen for command errors ([ca34085](https://github.com/adonisjs/ace/commit/ca34085)) - - -## [4.0.7](https://github.com/adonisjs/ace/compare/v4.0.5...v4.0.7) (2017-10-29) - - -### Bug Fixes - -* **package:** update fs-extra to version 4.0.2 ([#65](https://github.com/adonisjs/ace/issues/65)) ([3bc49a2](https://github.com/adonisjs/ace/commit/3bc49a2)) - - - -## [4.0.6](https://github.com/adonisjs/ace/compare/v4.0.5...v4.0.6) (2017-09-24) - - -### Features - -* **inquirer:** expose inquirer to be extended ([4415f32](https://github.com/adonisjs/ace/commit/4415f32)) -* **question:** allow to pass extra inquirer options ([3d672ed](https://github.com/adonisjs/ace/commit/3d672ed)) - - - - -## [4.0.5](https://github.com/adonisjs/ace/compare/v4.0.4...v4.0.5) (2017-08-18) - - -### Bug Fixes - -* **readme-example:** arrow function are not allowed here ([#63](https://github.com/adonisjs/ace/issues/63)) ([1551ce9](https://github.com/adonisjs/ace/commit/1551ce9)) - - - - -## [4.0.4](https://github.com/adonisjs/ace/compare/v4.0.3...v4.0.4) (2017-08-02) - - - - -## [4.0.3](https://github.com/adonisjs/ace/compare/v4.0.2...v4.0.3) (2017-07-28) - - - - -## [4.0.2](https://github.com/adonisjs/ace/compare/v4.0.1...v4.0.2) (2017-07-17) - - -### Bug Fixes - -* **kernel:** use commander.command over commander.on ([09a25ad](https://github.com/adonisjs/ace/commit/09a25ad)) - - -### Features - -* **kernel:** show help when no command is executed ([0e65c76](https://github.com/adonisjs/ace/commit/0e65c76)) - - - - -## [4.0.1](https://github.com/adonisjs/ace/compare/v4.0.0...v4.0.1) (2017-07-16) - - -### Bug Fixes - -* **command:** look instance properties on command ([f9cb1e4](https://github.com/adonisjs/ace/commit/f9cb1e4)) - - - - -# [4.0.0](https://github.com/adonisjs/ace/compare/v3.0.8...v4.0.0) (2017-07-16) - - -### Bug Fixes - -* **test:** fix breaking test ([5a07228](https://github.com/adonisjs/ace/commit/5a07228)) - - -### Features - -* add basic functionality for commands ([02a9178](https://github.com/adonisjs/ace/commit/02a9178)) -* **command:** add methods to copy and move files ([7216597](https://github.com/adonisjs/ace/commit/7216597)) -* **command:** add readfile method ([af2b67e](https://github.com/adonisjs/ace/commit/af2b67e)) -* **command:** use command.opts() over command._events ([e0bcade](https://github.com/adonisjs/ace/commit/e0bcade)) -* **kernel:** add support for inline commands ([8ce9fdc](https://github.com/adonisjs/ace/commit/8ce9fdc)) -* **question:** add support for questions ([24fd3ce](https://github.com/adonisjs/ace/commit/24fd3ce)) -* **scaffolding:** add scaffolding commands ([257b09d](https://github.com/adonisjs/ace/commit/257b09d)) - - - - -## [3.0.8](https://github.com/adonisjs/ace/compare/v3.0.7...v3.0.8) (2017-07-11) - - -### Bug Fixes - -* **command:** use command.opts() over command._events ([da5e4e8](https://github.com/adonisjs/ace/commit/da5e4e8)), closes [#604](https://github.com/adonisjs/ace/issues/604) -* **package:** update node-exceptions to version 2.0.0 ([#49](https://github.com/adonisjs/ace/issues/49)) ([89dab62](https://github.com/adonisjs/ace/commit/89dab62)) - - - - -## [3.0.7](https://github.com/adonisjs/ace/compare/v3.0.6...v3.0.7) (2017-02-25) - - -### Bug Fixes - -* **package:** update inquirer to version 3.0.2 ([#45](https://github.com/adonisjs/ace/issues/45)) ([725c528](https://github.com/adonisjs/ace/commit/725c528)) - - -### Features - -* **console:** add extra help on help screen ([444e6e8](https://github.com/adonisjs/ace/commit/444e6e8)) - - - - -## [3.0.6](https://github.com/adonisjs/ace/compare/v3.0.5...v3.0.6) (2017-01-26) - - -### Bug Fixes - -* **kernel:** listen event for unknown commands ([25fbb2d](https://github.com/adonisjs/ace/commit/25fbb2d)) -* **options:** Check if the option exists in camelCase ([#40](https://github.com/adonisjs/ace/issues/40)) ([f04cd46](https://github.com/adonisjs/ace/commit/f04cd46)) - - - - -## [3.0.5](https://github.com/adonisjs/ace/compare/v3.0.4...v3.0.5) (2016-12-12) - - - - -## [3.0.4](https://github.com/adonisjs/ace/compare/v3.0.3...v3.0.4) (2016-09-26) - - -### Features - -* **table:** add support for passing styles ([121d864](https://github.com/adonisjs/ace/commit/121d864)) - - - - -## [3.0.3](https://github.com/adonisjs/ace/compare/v3.0.2...v3.0.3) (2016-09-26) - - - - -## 3.0.2 (2016-08-26) - -* Update adonis-fold. - - -## 3.0.1 (2016-06-26) - - -### Bug Fixes - -* Add missing dependency to package.json([effcba8](https://github.com/adonisjs/ace/commit/effcba8)) - - -### Features - -* Initiate 3.0([d4abbd7](https://github.com/adonisjs/ace/commit/d4abbd7)) -* **ansi:** Add table support to command ansi([a6f111c](https://github.com/adonisjs/ace/commit/a6f111c)) -* **command:** Add run method to execute commands within a command([55beb4e](https://github.com/adonisjs/ace/commit/55beb4e)) -* **kernel:** add call method to kernel, add missing tests([bbe842f](https://github.com/adonisjs/ace/commit/bbe842f)) - - - diff --git a/bin/japaTypes.ts b/bin/japa_types.ts similarity index 100% rename from bin/japaTypes.ts rename to bin/japa_types.ts diff --git a/bin/test.ts b/bin/test.ts index 5aba7ce..5b398e1 100644 --- a/bin/test.ts +++ b/bin/test.ts @@ -1,4 +1,5 @@ import { assert } from '@japa/assert' +import { expectTypeOf } from '@japa/expect-type' import { specReporter } from '@japa/spec-reporter' import { runFailedTests } from '@japa/run-failed-tests' import { processCliArgs, configure, run } from '@japa/runner' @@ -19,8 +20,8 @@ import { processCliArgs, configure, run } from '@japa/runner' configure({ ...processCliArgs(process.argv.slice(2)), ...{ - files: ['test/**/*.spec.ts'], - plugins: [assert(), runFailedTests()], + files: ['tests/**/*.spec.ts'], + plugins: [assert(), runFailedTests(), expectTypeOf()], reporters: [specReporter()], importer: (filePath: string) => import(filePath), }, diff --git a/example/index.ts b/example/index.ts deleted file mode 100644 index 19ba5be..0000000 --- a/example/index.ts +++ /dev/null @@ -1,117 +0,0 @@ -/* - * @adonisjs/ace - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { Application } from '@adonisjs/application' - -import { Kernel } from '../src/Kernel' -import { args } from '../src/Decorators/args' -import { flags } from '../src/Decorators/flags' -import { BaseCommand } from '../src/BaseCommand' -import { handleError } from '../src/utils/handleError' - -class Greet extends BaseCommand { - public static commandName = 'greet' - public static description = 'Greet a user with their name' - public static aliases = ['gr', 'sayhi'] - - @args.string({ description: 'The name of the person you want to greet' }) - public name: string - - @args.string() - public age?: string - - @args.spread() - public files: string[] - - @flags.string({ - description: 'The environment to use to specialize certain commands', - alias: 'e', - }) - public env: 'development' | 'production' - - @flags.string({ - description: 'The main HTML file that will be requested', - }) - public entrypoint: string - - @flags.numArray({ description: 'HTML fragments loaded on demand', alias: 'f' }) - public fragment: string - - public async prepare() { - if (this.env === undefined) { - this.env = await this.prompt.choice('Select one of the given environments', [ - 'development', - 'production', - ]) - } - - if (this.entrypoint === undefined) { - this.entrypoint = await this.prompt.ask('Define entrypoint as we detected multiple?') - } - } - - public async run() { - this.logger.success('Operation successful') - this.logger.error('Unable to acquire lock') - this.logger.info('Hello') - this.logger.action('write').succeeded('Release notes') - this.logger.action('write').skipped('Release notes') - this.logger.info('Please get it done') - } -} - -class MakeController extends BaseCommand { - public static commandName = 'make:controller' - public static description = 'Create a HTTP controller' - - public async run() {} -} - -class MakeModel extends BaseCommand { - public static commandName = 'make:model' - public static description = 'Create database model' - - public async run() { - console.log(process.env.NODE_ENV) - } -} - -const app = new Application(__dirname, 'web', {}) -const kernel = new Kernel(app) -kernel.register([Greet, MakeController, MakeModel]) - -kernel.flag( - 'help', - (value, _, command) => { - if (!value) { - return - } - - kernel.printHelp(command) - process.exit(0) - }, - { type: 'boolean' } -) - -kernel.flag( - 'env', - (value) => { - process.env.NODE_ENV = value - }, - { type: 'string' } -) - -kernel.onExit(() => { - if (kernel.error) { - handleError(kernel.error) - } - process.exit(kernel.exitCode) -}) - -kernel.handle(process.argv.splice(2)) diff --git a/examples/main.ts b/examples/main.ts new file mode 100644 index 0000000..56cbb14 --- /dev/null +++ b/examples/main.ts @@ -0,0 +1,125 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Kernel } from '../src/kernel.js' +import { args } from '../src/decorators/args.js' +import { flags } from '../src/decorators/flags.js' +import { BaseCommand } from '../src/commands/base.js' +import { CommandsList } from '../src/loaders/list.js' +import { HelpCommand } from '../src/commands/help.js' + +const kernel = new Kernel() + +class Configure extends BaseCommand { + static commandName: string = 'configure' + static aliases: string[] = ['invoke'] + static description: string = 'Configure one or more AdonisJS packages' +} + +class Build extends BaseCommand { + static commandName: string = 'build' + static description: string = + 'Compile project from Typescript to Javascript. Also compiles the frontend assets if using webpack encore' +} + +class Repl extends BaseCommand { + static commandName: string = 'repl' + static description: string = 'Start a new REPL session' +} + +class Serve extends BaseCommand { + static commandName: string = 'serve' + static description: string = + 'Start the AdonisJS HTTP server, along with the file watcher. Also starts the webpack dev server when webpack encore is installed' +} + +class MakeController extends BaseCommand { + @args.string({ description: 'Name of the controller' }) + name!: string + + @flags.boolean({ description: 'Add resourceful methods', default: false }) + resource!: boolean + + static commandName: string = 'make:controller' + static aliases: string[] = ['mc'] + static description: string = 'Make a new HTTP controller' +} + +class MakeModel extends BaseCommand { + static commandName: string = 'make:model' + static aliases: string[] = ['mm'] + static description: string = 'Make a new Lucid model' +} + +class DbSeed extends BaseCommand { + static commandName: string = 'db:seed' + static description: string = 'Execute database seeders' +} + +class DbTruncate extends BaseCommand { + static commandName: string = 'db:truncate' + static description: string = 'Truncate all tables in database' +} + +class DbWipe extends BaseCommand { + static commandName: string = 'db:wipe' + static description: string = 'Drop all tables, views and types in database' +} + +kernel.addLoader( + new CommandsList([ + HelpCommand, + Configure, + Build, + Serve, + Repl, + MakeController, + MakeModel, + DbSeed, + DbTruncate, + DbWipe, + ]) +) + +kernel.addAlias('md', 'make:model') +kernel.addAlias('start', 'serve') + +kernel.defineFlag('help', { + type: 'boolean', + alias: 'h', + description: + 'Display help for the given command. When no command is given display help for the list command', +}) + +kernel.defineFlag('env', { + type: 'string', + description: 'The environment the command should run under', +}) + +kernel.defineFlag('ansi', { + type: 'boolean', + showNegatedVariantInHelp: true, + description: 'Enable/disable colorful output', +}) + +kernel.on('ansi', (_, $kernel, options) => { + if (options.ansi === false) { + $kernel.ui.switchMode('silent') + } + + if (options.ansi === true) { + $kernel.ui.switchMode('normal') + } +}) + +kernel.info.set('binary', 'node ace') +kernel.info.set('Framework version', '9.1') +kernel.info.set('App version', '1.1.1') + +await kernel.handle(process.argv.splice(2)) diff --git a/examples/parent.ts b/examples/parent.ts new file mode 100644 index 0000000..1522b29 --- /dev/null +++ b/examples/parent.ts @@ -0,0 +1,5 @@ +import { exec } from 'node:child_process' +exec('node --loader=ts-node/esm examples/main.ts --ansi', {}, (_, stdout, stderr) => { + console.log(stderr) + console.log(stdout) +}) diff --git a/index.ts b/index.ts deleted file mode 100644 index 2f8ab1f..0000000 --- a/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * @adonisjs/ace - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -export { Kernel } from './src/Kernel' -export { args } from './src/Decorators/args' -export { flags } from './src/Decorators/flags' -export { BaseCommand } from './src/BaseCommand' -export { handleError } from './src/utils/handleError' -export { ManifestLoader } from './src/Manifest/Loader' -export { ManifestGenerator } from './src/Manifest/Generator' -export { listDirectoryFiles } from './src/utils/listDirectoryFiles' diff --git a/package.json b/package.json index da4d426..d7650a9 100644 --- a/package.json +++ b/package.json @@ -3,103 +3,75 @@ "version": "11.3.1", "description": "Commandline apps framework used by AdonisJs", "main": "build/index.js", + "type": "module", "files": [ "build/src", "build/index.d.ts", "build/index.js" ], + "exports": { + ".": "./build/index.js", + "./types": "./build/src/types.js" + }, "scripts": { - "mrm": "mrm --preset=@adonisjs/mrm-preset", "pretest": "npm run lint", - "test": "node -r @adonisjs/require-ts/build/register ./bin/test.ts", - "lint": "eslint . --ext=.ts", + "test": "cross-env NODE_DEBUG=adonisjs:core c8 npm run vscode:test", "clean": "del-cli build", "compile": "npm run lint && npm run clean && tsc", "build": "npm run compile", - "commit": "git-cz", - "release": "FORCE_COLOR=true np --message=\"chore(release): %s\"", + "release": "np", "version": "npm run build", - "sync-labels": "github-label-sync --labels ./node_modules/@adonisjs/mrm-preset/gh-labels.json adonisjs/ace", + "prepublishOnly": "npm run build", + "lint": "eslint . --ext=.ts", "format": "prettier --write .", - "prepublishOnly": "npm run build" + "sync-labels": "github-label-sync --labels .github/labels.json adonisjs/ace", + "vscode:test": "node --loader=ts-node/esm bin/test.ts" }, "keywords": [ "adonisjs", + "commandline", + "cli", "commander" ], - "author": "virk", - "homepage": "https://adonisjs.com", + "author": "virk,adonisjs", "license": "MIT", "dependencies": { - "@poppinss/cliui": "^3.0.2", - "@poppinss/prompts": "^2.0.2", - "@poppinss/utils": "^5.0.0", - "fs-extra": "^10.1.0", - "getopts": "^2.3.0", - "leven": "^3.1.0", - "mustache": "^4.2.0", - "slash": "^3.0.0", - "term-size": "^2.2.1" - }, - "peerDependencies": { - "@adonisjs/application": "^5.0.0" - }, - "nyc": { - "exclude": [ - "test" - ], - "extension": [ - ".ts" - ] - }, - "publishConfig": { - "access": "public", - "tag": "latest" + "@poppinss/cliui": "^6.1.1-0", + "@poppinss/hooks": "^7.1.0-0", + "@poppinss/utils": "^6.3.0-0", + "string-similarity": "^4.0.4", + "string-width": "^5.1.2", + "yargs-parser": "^21.1.1" }, "devDependencies": { - "@adonisjs/application": "^5.2.5", - "@adonisjs/mrm-preset": "^5.0.3", - "@adonisjs/require-ts": "^2.0.12", + "@commitlint/cli": "^17.3.0", + "@commitlint/config-conventional": "^17.3.0", "@japa/assert": "^1.3.5", + "@japa/expect-type": "^1.0.2", "@japa/run-failed-tests": "^1.0.8", "@japa/runner": "^2.1.1", "@japa/spec-reporter": "^1.2.0", "@poppinss/dev-utils": "^2.0.3", + "@swc/core": "^1.3.22", "@types/node": "^18.7.15", - "commitizen": "^4.2.5", - "cz-conventional-changelog": "^3.3.0", + "@types/sinon": "^10.0.13", + "@types/string-similarity": "^4.0.0", + "c8": "^7.12.0", + "cross-env": "^7.0.3", "del-cli": "^5.0.0", "eslint": "^8.23.0", "eslint-config-prettier": "^8.5.0", - "eslint-plugin-adonis": "^2.1.0", + "eslint-plugin-adonis": "^3.0.3", "eslint-plugin-prettier": "^4.2.1", "github-label-sync": "^2.2.0", - "husky": "^8.0.1", - "mrm": "^4.1.0", - "np": "^7.6.2", + "husky": "^8.0.2", + "np": "^7.6.3", "prettier": "^2.7.1", "reflect-metadata": "^0.1.13", + "sinon": "^15.0.1", + "ts-node": "^10.9.1", "typescript": "^4.8.2" }, - "husky": { - "hooks": { - "commit-msg": "node ./node_modules/@adonisjs/mrm-preset/validateCommit/conventional/validate.js" - } - }, - "config": { - "commitizen": { - "path": "cz-conventional-changelog" - } - }, - "np": { - "contents": ".", - "anyBranch": false - }, - "directories": { - "doc": "docs", - "example": "example", - "test": "test" - }, "repository": { "type": "git", "url": "git+https://github.com/adonisjs/ace.git" @@ -107,19 +79,7 @@ "bugs": { "url": "https://github.com/adonisjs/ace/issues" }, - "mrmConfig": { - "core": true, - "license": "MIT", - "services": [ - "github-actions" - ], - "minNodeVersion": "14.15.4", - "probotApps": [ - "stale", - "lock" - ], - "runGhActionsOnWindows": true - }, + "homepage": "https://adonisjs.com", "eslintConfig": { "extends": [ "plugin:adonis/typescriptPackage", @@ -138,7 +98,8 @@ } }, "eslintIgnore": [ - "build" + "build", + "backup" ], "prettier": { "trailingComma": "es5", @@ -149,5 +110,29 @@ "bracketSpacing": true, "arrowParens": "always", "printWidth": 100 + }, + "commitlint": { + "extends": [ + "@commitlint/config-conventional" + ] + }, + "publishConfig": { + "access": "public", + "tag": "next" + }, + "np": { + "message": "chore(release): %s", + "tag": "next", + "branch": "main", + "anyBranch": false + }, + "c8": { + "reporter": [ + "text", + "html" + ], + "exclude": [ + "tests/**" + ] } } diff --git a/src/BaseCommand/index.ts b/src/BaseCommand/index.ts deleted file mode 100644 index 14fc648..0000000 --- a/src/BaseCommand/index.ts +++ /dev/null @@ -1,323 +0,0 @@ -/* - * @adonisjs/ace - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { ParsedOptions } from 'getopts' -import { Prompt, FakePrompt } from '@poppinss/prompts' -import { string } from '@poppinss/utils/build/helpers' -import { instantiate } from '@poppinss/cliui/build/api' -import { ApplicationContract } from '@ioc:Adonis/Core/Application' -import { defineStaticProperty, Exception } from '@poppinss/utils' - -import { Generator } from '../Generator' -import { - CommandArg, - CommandFlag, - KernelContract, - CommandSettings, - CommandContract, - GeneratorContract, -} from '../Contracts' - -/** - * Abstract base class other classes must extend - */ -export abstract class BaseCommand implements CommandContract { - /** - * Reference to the exit handler - */ - protected exitHandler?: () => void | Promise - - /** - * Accepting AdonisJs application instance and kernel instance - */ - constructor(public application: ApplicationContract, public kernel: KernelContract) {} - - /** - * Is the current command the main command executed from the - * CLI - */ - public get isMain(): boolean { - return this.kernel.isMain(this) - } - - /** - * Terminal is interactive - */ - public get isInteractive(): boolean { - return this.kernel.isInteractive - } - - /** - * Command arguments - */ - public static args: CommandArg[] - - /** - * Command aliases - */ - public static aliases: string[] - - /** - * Command flags - */ - public static flags: CommandFlag[] - - /** - * Command name. The command will be registered using this name only. Make - * sure their aren't any spaces inside the command name. - */ - public static commandName: string - - /** - * The description of the command displayed on the help screen. - * A good command will always have some description. - */ - public static description: string - - /** - * Any settings a command wants to have. Helpful for third party - * tools to read the settings in lifecycle hooks and make - * certain decisions - */ - public static settings: CommandSettings - - /** - * Whether or not the command has been booted - */ - public static booted: boolean - - /** - * Boots the command by defining required static properties - */ - public static boot() { - if (this.booted) { - return - } - - this.booted = true - - defineStaticProperty(this, BaseCommand, { - propertyName: 'args', - defaultValue: [], - strategy: 'inherit', - }) - - defineStaticProperty(this, BaseCommand, { - propertyName: 'aliases', - defaultValue: [], - strategy: 'inherit', - }) - - defineStaticProperty(this, BaseCommand, { - propertyName: 'flags', - defaultValue: [], - strategy: 'inherit', - }) - - defineStaticProperty(this, BaseCommand, { - propertyName: 'settings', - defaultValue: {}, - strategy: 'inherit', - }) - - defineStaticProperty(this, BaseCommand, { - propertyName: 'commandName', - defaultValue: '', - strategy: 'define', - }) - - defineStaticProperty(this, BaseCommand, { - propertyName: 'description', - defaultValue: '', - strategy: 'define', - }) - } - - /** - * Define an argument directly on the command without using the decorator - */ - public static $addArgument(options: Partial) { - if (!options.propertyName) { - throw new Exception( - '"propertyName" is required to register a command argument', - 500, - 'E_MISSING_ARGUMENT_NAME' - ) - } - - const arg: CommandArg = Object.assign( - { - type: options.type || 'string', - propertyName: options.propertyName, - name: options.name || options.propertyName, - required: options.required === false ? false : true, - }, - options - ) - - this.args.push(arg) - } - - /** - * Define a flag directly on the command without using the decorator - */ - public static $addFlag(options: Partial) { - if (!options.propertyName) { - throw new Exception( - '"propertyName" is required to register command flag', - 500, - 'E_MISSING_FLAG_NAME' - ) - } - - const flag: CommandFlag = Object.assign( - { - name: options.name || string.snakeCase(options.propertyName).replace(/_/g, '-'), - propertyName: options.propertyName, - type: options.type || 'boolean', - }, - options - ) - - this.flags.push(flag) - } - - /** - * Reference to cli ui - */ - public ui = instantiate(this.kernel.isMockingConsoleOutput) - - /** - * Parsed options on the command. They only exist when the command - * is executed via kernel. - */ - public parsed?: ParsedOptions - - /** - * The prompt for the command - */ - public prompt: Prompt | FakePrompt = this.kernel.isMockingConsoleOutput - ? new FakePrompt() - : new Prompt() - - /** - * Returns the instance of logger to log messages - */ - public logger = this.ui.logger - - /** - * Reference to the colors - */ - public colors: ReturnType['logger']['colors'] = this.logger.colors - - /** - * Generator instance to generate entity files - */ - public generator: GeneratorContract = new Generator(this) - - /** - * Error raised by the command - */ - public error?: any - - /** - * Command exit code - */ - public exitCode?: number - - public async run?(...args: any[]): Promise - public async prepare?(...args: any[]): Promise - public async completed?(...args: any[]): Promise - - /** - * Execute the command - */ - public async exec() { - const hasRun = typeof this.run === 'function' - const hasHandle = typeof this.handle === 'function' - let commandResult: any - - /** - * Print depreciation warning - */ - if (hasHandle) { - process.emitWarning( - 'DeprecationWarning', - `${this.constructor.name}.handle() is deprecated. Define run() method instead` - ) - } - - /** - * Run command and catch any raised exceptions - */ - try { - /** - * Run prepare method when exists on the command instance - */ - if (typeof this.prepare === 'function') { - await this.application.container.callAsync(this, 'prepare' as any, []) - } - - /** - * Execute the command handle or run method - */ - commandResult = await this.application.container.callAsync( - this, - hasRun ? 'run' : ('handle' as any), - [] - ) - } catch (error) { - this.error = error - } - - let errorHandled = false - - /** - * Run completed method when exists - */ - if (typeof this.completed === 'function') { - errorHandled = await this.application.container.callAsync(this, 'completed' as any, []) - } - - /** - * Throw error when error exists and the completed method didn't - * handled it - */ - if (this.error && !errorHandled) { - throw this.error - } - - return commandResult - } - - /** - * Register an onExit handler - */ - public onExit(handler: () => void | Promise) { - this.exitHandler = handler - return this - } - - /** - * Trigger exit - */ - public async exit() { - if (typeof this.exitHandler === 'function') { - await this.exitHandler() - } - - await this.kernel.exit(this) - } - - /** - * Must be defined by the parent class - */ - // @depreciated - public async handle?(...args: any[]): Promise -} diff --git a/src/Contracts/index.ts b/src/Contracts/index.ts deleted file mode 100644 index fc5be83..0000000 --- a/src/Contracts/index.ts +++ /dev/null @@ -1,445 +0,0 @@ -/* - * @adonisjs/ace - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import * as ui from '@poppinss/cliui' -import { ParsedOptions } from 'getopts' -import { PromptContract } from '@poppinss/prompts' -import { ApplicationContract, AppEnvironments } from '@ioc:Adonis/Core/Application' - -/** - * Settings excepted by the command - */ -export type CommandSettings = { - loadApp?: boolean - stayAlive?: boolean - environment?: AppEnvironments -} & { [key: string]: any } - -/** - * The types of flags can be defined on a command. - */ -export type FlagTypes = 'string' | 'number' | 'boolean' | 'array' | 'numArray' - -/** - * The types of arguments can be defined on a command. - */ -export type ArgTypes = 'string' | 'spread' - -/** - * The shape of command argument - */ -export type CommandArg = { - propertyName: string - name: string - type: ArgTypes - required: boolean - description?: string -} - -/** - * The shape of a command flag - */ -export type CommandFlag = { - propertyName: string - name: string - type: FlagTypes - description?: string - alias?: string -} - -/** - * The handler that handles the global - * flags - */ -export type GlobalFlagHandler = ( - value: any, - parsed: ParsedOptions, - command?: CommandConstructorContract -) => any - -/** - * Shape of grouped commands. Required when displaying - * help - */ -export type CommandsGroup = { - group: string - commands: SerializedCommand[] -}[] - -/** - * The shared properties that exists on the command implementation - * as well as it's serialized version - */ -export type SerializedCommand = { - args: CommandArg[] - aliases: string[] - settings: CommandSettings - flags: CommandFlag[] - commandName: string - description: string -} - -/** - * Command constructor shape with it's static properties - */ -export interface CommandConstructorContract extends SerializedCommand { - new (application: ApplicationContract, kernel: KernelContract, ...args: any[]): CommandContract - - /** - * A boolean to know if the command has been booted or not. We initialize some - * static properties to the class during the boot process. - */ - booted: boolean - - /** - * Boot the command. You won't have to run this method by yourself. Ace will internally - * boot the commands by itself. - */ - boot(): void - - /** - * Add an argument directly on the command without using the decorator - */ - $addArgument(options: Partial): void - - /** - * Add a flag directly on the command without using the decorator - */ - $addFlag(options: Partial): void -} - -/** - * The shape of command class - */ -export interface CommandContract { - parsed?: ParsedOptions - error?: any - exitCode?: number - logger: typeof ui.logger - prompt: PromptContract - colors: typeof ui.logger.colors - ui: typeof ui - generator: GeneratorContract - kernel: KernelContract - - /** - * The flag is set to true, when the command is executed as the main command - * from the terminal. - * - * However, set to false when command is executed programmatically. - */ - readonly isMain: boolean - - /** - * The flag is set to true, when the commandline is in interactive mode. - * Can be disabled manually via the kernel - */ - readonly isInteractive: boolean - - onExit(callback: () => Promise | void): this - exit(): Promise - - exec(): Promise - handle?(...args: any[]): Promise - run?(...args: any[]): Promise - prepare?(...args: any[]): Promise - completed?(...args: any[]): Promise -} - -/** - * Shape of the serialized command inside the manifest JSON file. - */ -export type ManifestCommand = SerializedCommand & { commandPath: string } - -/** - * Shape of defined aliases - */ -export type Aliases = { [key: string]: string } - -/** - * Shape of the manifest JSON file - */ -export type ManifestNode = { - commands: { [command: string]: ManifestCommand } - aliases: Aliases -} - -/** - * Manifest loader interface - */ -export interface ManifestLoaderContract { - booted: boolean - boot(): Promise - - /** - * Returns the base path for a given command. Helps in loading - * the command relative from that path - */ - getCommandBasePath(commandName: string): string | undefined - - /** - * Returns manifest command node. One must load the command - * in order to use it - */ - getCommand(commandName: string): { basePath: string; command: ManifestCommand } | undefined - - /** - * Find if a command exists or not - */ - hasCommand(commandName: string): boolean - - /** - * Load command from the disk. Make sure to use [[hasCommand]] before - * calling this method - */ - loadCommand(commandName: string): Promise - - /** - * Returns an array of manifest commands by concatenating the - * commands and aliases from all the manifest files - */ - getCommands(): { commands: ManifestCommand[]; aliases: Aliases } -} - -/** - * Callbacks for different style of hooks - */ -export type FindHookCallback = (command: SerializedCommand | null) => Promise | any -export type RunHookCallback = (command: CommandContract) => Promise | any - -/** - * Shape of ace kernel - */ -export interface KernelContract { - /** - * The exit code to be used for exiting the process. One should use - * this to exit the process - */ - exitCode?: number - - /** - * Reference to the process error. It can come from the command, flags - * or any other intermediate code. - */ - error?: Error - - /** - * Reference to the default command. Feel free to overwrite it - */ - defaultCommand: CommandConstructorContract - - /** - * A map of locally registered commands - */ - commands: { [name: string]: CommandConstructorContract } - - /** - * Registered command aliases - */ - aliases: Aliases - - /** - * A map of global flags - */ - flags: { [name: string]: CommandFlag & { handler: GlobalFlagHandler } } - - /** - * Register before hooks - */ - before(action: 'run', callback: RunHookCallback): this - before(action: 'find', callback: FindHookCallback): this - before(action: 'run' | 'find', callback: RunHookCallback | FindHookCallback): this - - /** - * Register after hooks - */ - after(action: 'run', callback: RunHookCallback): this - after(action: 'find', callback: FindHookCallback): this - after(action: 'run' | 'find', callback: RunHookCallback | FindHookCallback): this - - /** - * Register a command directly via class - */ - register(commands: CommandConstructorContract[]): this - - /** - * Register a global flag - */ - flag( - name: string, - handler: GlobalFlagHandler, - options: Partial> - ): this - - /** - * Register the manifest loader - */ - useManifest(manifestLoacder: ManifestLoaderContract): this - - /** - * Register an on exit callback listener. It should always - * exit the process - */ - onExit(callback: (kernel: this) => void | Promise): this - - /** - * Preload the manifest file - */ - preloadManifest(): void - - /** - * Get command suggestions - */ - getSuggestions(name: string, distance?: number): string[] - - /** - * Find a command using the command line `argv` - */ - find(argv: string[]): Promise - - /** - * Run the default command - */ - runDefaultCommand(): Promise - - /** - * Handle the command line argv to execute commands - */ - handle(args: string[]): Promise - - /** - * Execute a command by its name and args - */ - exec(commandName: string, args: string[]): Promise - - /** - * Print help for all commands or a given command - */ - printHelp( - command?: CommandConstructorContract, - commandsToAppend?: ManifestCommand[], - aliasesToAppend?: Record - ): void - - /** - * Find if a command is the main command. Main commands are executed - * directly from the terminal - */ - isMain(command: CommandContract): boolean - - /** - * Find if CLI process is interactive. - */ - isInteractive: boolean - - /** - * Toggle isInteractive state - */ - interactive(state: boolean): this - - /** - * Find if console output is mocked - */ - isMockingConsoleOutput: boolean - - /** - * Enforce mocking the console output. Command logs, tables, prompts - * will be mocked - */ - mockConsoleOutput(): this - - /** - * Trigger exit flow - */ - exit(command: CommandContract, error?: any): Promise -} - -/** - * Template generator options - */ -export type GeneratorFileOptions = { - pattern?: 'pascalcase' | 'camelcase' | 'snakecase' | 'dashcase' - form?: 'singular' | 'plural' - formIgnoreList?: string[] - suffix?: string - prefix?: string - extname?: string -} - -/** - * Shape of the individual generator file - */ -export interface GeneratorFileContract { - state: 'persisted' | 'removed' | 'pending' - - /** - * Define path to the stub template. You can also define inline text instead - * of relying on a template file, but do make sure to set `raw=true` inside - * the options when using inline text. - */ - stub(fileOrContents: string, options?: { raw: boolean }): this - - /** - * Instruct to use mustache templating syntax, instead of template literals - */ - useMustache(): this - - /** - * The relative path to the destination directory. - */ - destinationDir(directory: string): this - - /** - * Define a custom application root. Otherwise `process.cwd()` is used. - */ - appRoot(directory: string): this - - /** - * Apply data to the stub - */ - apply(contents: any): this - - /** - * Get file properties as a JSON object - */ - toJSON(): { - filename: string - filepath: string - extension: string - contents: string - relativepath: string - state: 'persisted' | 'removed' | 'pending' - } -} - -/** - * Shape of the files generator - */ -export interface GeneratorContract { - /** - * Add a new file to the files generator. You can add multiple files - * together and they will be created when `run` is invoked. - */ - addFile(name: string, options?: GeneratorFileOptions): GeneratorFileContract - - /** - * Run the generator and create all files registered using `addFiles` - */ - run(): Promise - - /** - * Clear the registered files from the generator - */ - clear(): void -} - -/** - * Filter function for filtering files during the `readdir` scan - */ -export type CommandsListFilterFn = ((name: string) => boolean) | string[] diff --git a/src/Decorators/args.ts b/src/Decorators/args.ts index d60c02c..b03a3e9 100644 --- a/src/Decorators/args.ts +++ b/src/Decorators/args.ts @@ -1,45 +1,42 @@ /* * @adonisjs/ace * - * (c) Harminder Virk + * (c) AdonisJS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -import { CommandArg, ArgTypes, CommandConstructorContract } from '../Contracts' +import type { BaseCommand } from '../commands/base.js' +import type { SpreadArgument, StringArgument } from '../types.js' /** - * Adds arg to the list of command arguments with pre-defined - * type. + * Namespace for defining arguments using decorators. */ -function addArg( - type: ArgTypes, - options: Partial> -) { - return function arg( - target: TTarget, - propertyName: TKey - ) { - const Command = target.constructor as CommandConstructorContract - Command.boot() - Command.$addArgument(Object.assign({ type, propertyName }, options)) - } -} - export const args = { /** - * Define argument that accepts string value + * Define argument that accepts a string value */ - string(options?: Partial>) { - return addArg('string', options || {}) + string(options?: Partial, 'type'>>) { + return function addArg( + target: Target, + propertyName: Key + ) { + const Command = target.constructor as typeof BaseCommand + Command.defineArgument(propertyName, { ...options, type: 'string' }) + } }, /** - * Define argument that accepts multiple values. Must be - * the last argument. + * Define argument that accepts a spread value */ - spread(options?: Partial>) { - return addArg('spread', options || {}) + spread(options?: Partial, 'type'>>) { + return function addArg( + target: Target, + propertyName: Key + ) { + const Command = target.constructor as typeof BaseCommand + Command.defineArgument(propertyName, { ...options, type: 'spread' }) + } }, } diff --git a/src/Decorators/flags.ts b/src/Decorators/flags.ts index 3b8e35d..b80eccf 100644 --- a/src/Decorators/flags.ts +++ b/src/Decorators/flags.ts @@ -1,65 +1,68 @@ /* * @adonisjs/ace * - * (c) Harminder Virk + * (c) AdonisJS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -import { CommandFlag, FlagTypes, CommandConstructorContract } from '../Contracts' +import type { BaseCommand } from '../commands/base.js' +import type { ArrayFlag, NumberFlag, StringFlag, BooleanFlag } from '../types.js' /** - * Pushes flag to the list of command flags with predefined - * types. + * Namespace for defining flags using decorators. */ -function addFlag( - type: FlagTypes, - options: Partial> -) { - return function flag( - target: TTarget, - propertyName: TKey - ) { - const Command = target.constructor as CommandConstructorContract - Command.boot() - Command.$addFlag(Object.assign({ type, propertyName }, options)) - } -} - export const flags = { /** - * Create a flag that excepts string values - */ - string(options?: Partial>) { - return addFlag('string', options || {}) - }, - - /** - * Create a flag that excepts numeric values + * Define option that accepts a string value */ - number(options?: Partial>) { - return addFlag('number', options || {}) + string(options?: Partial, 'type'>>) { + return function addArg( + target: Target, + propertyName: Key + ) { + const Command = target.constructor as typeof BaseCommand + Command.defineFlag(propertyName, { type: 'string', ...options }) + } }, /** - * Create a flag that excepts boolean values + * Define option that accepts a boolean value */ - boolean(options?: Partial>) { - return addFlag('boolean', options || {}) + boolean(options?: Partial, 'type'>>) { + return function addArg( + target: Target, + propertyName: Key + ) { + const Command = target.constructor as typeof BaseCommand + Command.defineFlag(propertyName, { type: 'boolean', ...options }) + } }, /** - * Create a flag that excepts array of string values + * Define option that accepts a number value */ - array(options?: Partial>) { - return addFlag('array', options || {}) + number(options?: Partial, 'type'>>) { + return function addArg( + target: Target, + propertyName: Key + ) { + const Command = target.constructor as typeof BaseCommand + Command.defineFlag(propertyName, { type: 'number', ...options }) + } }, /** - * Create a flag that excepts array of numeric values + * Define option that accepts an array of values */ - numArray(options?: Partial>) { - return addFlag('numArray', options || {}) + array(options?: Partial, 'type'>>) { + return function addArg( + target: Target, + propertyName: Key + ) { + const Command = target.constructor as typeof BaseCommand + Command.defineFlag(propertyName, { type: 'array', ...options }) + } }, } diff --git a/src/Exceptions/index.ts b/src/Exceptions/index.ts deleted file mode 100644 index 0be61b4..0000000 --- a/src/Exceptions/index.ts +++ /dev/null @@ -1,154 +0,0 @@ -/* - * @adonisjs/ace - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { logger, sticker } from '@poppinss/cliui' -import { Exception } from '@poppinss/utils' -import { CommandConstructorContract } from '../Contracts' - -/** - * Raised when a required argument is missing - */ -export class MissingArgumentException extends Exception { - public command: CommandConstructorContract - public argumentName: string - - /** - * A required argument is missing - */ - public static invoke( - name: string, - command: CommandConstructorContract - ): MissingArgumentException { - const exception = new this(`Missing required argument "${name}"`, 500, 'E_MISSING_ARGUMENT') - - exception.argumentName = name - exception.command = command - return exception - } - - /** - * Handle itself - */ - public handle(error: MissingArgumentException) { - logger.error(logger.colors.red(`Missing required argument "${error.argumentName}"`)) - } -} - -/** - * Raised when an the type of a flag is not as one of the excepted type - */ -export class InvalidFlagException extends Exception { - public command?: CommandConstructorContract - public flagName: string - public expectedType: string - - /** - * Flag type validation failed. - */ - public static invoke( - prop: string, - expected: string, - command?: CommandConstructorContract - ): InvalidFlagException { - let article: string = 'a' - - if (expected === 'number') { - expected = 'numeric' - } - - if (expected === 'array') { - article = 'an' - expected = 'array of strings' - } - - if (expected === 'numArray') { - article = 'an' - expected = 'array of numbers' - } - - const exception = new this( - `"${prop}" flag expects ${article} "${expected}" value`, - 500, - 'E_INVALID_FLAG' - ) - - exception.flagName = prop - exception.command = command - exception.expectedType = expected - return exception - } - - /** - * Handle itself - */ - public handle(error: InvalidFlagException) { - logger.error( - logger.colors.red(`Expected "--${error.flagName}" to be a valid "${error.expectedType}"`) - ) - } -} - -/** - * Raised when command is not registered with kernel - */ -export class InvalidCommandException extends Exception { - public commandName: string - public suggestions: string[] = [] - - public static invoke(commandName: string, suggestions: string[]): InvalidCommandException { - const exception = new this( - `"${commandName}" is not a registered command`, - 500, - 'E_INVALID_COMMAND' - ) - - exception.commandName = commandName - exception.suggestions = suggestions - return exception - } - - /** - * Handle itself - */ - public handle(error: InvalidCommandException) { - logger.error(`"${error.commandName}" command not found`) - - if (!error.suggestions.length) { - return - } - - logger.log('') - const suggestionLog = sticker().heading('Did you mean one of these?') - error.suggestions.forEach((commandName) => { - suggestionLog.add(logger.colors.yellow(commandName)) - }) - - suggestionLog.render() - } -} - -/** - * Raised when an unknown flag is defined - */ -export class UnknownFlagException extends Exception { - /** - * Unknown flag - */ - public static invoke(prop: string): UnknownFlagException { - const exception = new this(`Unknown flag "${prop}"`, 500, 'E_INVALID_FLAG') - return exception - } - - /** - * Handle itself - */ - public handle(error: UnknownFlagException) { - logger.error(logger.colors.red(error.message)) - } -} diff --git a/src/Generator/File.ts b/src/Generator/File.ts deleted file mode 100644 index da726b1..0000000 --- a/src/Generator/File.ts +++ /dev/null @@ -1,147 +0,0 @@ -/* - * @adonisjs/ace - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { basename, join, isAbsolute, sep } from 'path' - -import { StringTransformer } from './StringTransformer' -import { template, templateFromFile } from '../utils/template' -import { GeneratorFileOptions, GeneratorFileContract } from '../Contracts' - -/** - * Exposes the API to construct the output file content, path - * and template source. - */ -export class GeneratorFile implements GeneratorFileContract { - private stubContents: string - private isStubRaw: boolean - private templateData: any = {} - private customDestinationPath?: string - private customAppRoot?: string - private mustache: boolean = false - - public state: 'persisted' | 'removed' | 'pending' = 'pending' - - constructor(private name: string, private options: GeneratorFileOptions = {}) {} - - /** - * Returns relative path for the file. Useful for - * printing log info - */ - private getFileRelativePath(filepath: string) { - if (this.customAppRoot) { - return filepath.replace(`${this.customAppRoot}${sep}`, '') - } - - return filepath - } - - /** - * Set stub for the contents source. When `raw` is true, then string - * is considered as the raw content and not the file path. - */ - public stub(fileOrContents: string, options?: { raw: boolean }): this { - this.stubContents = fileOrContents - this.isStubRaw = !!(options && options.raw) - return this - } - - /** - * Optionally define destination directory from the project root. - */ - public destinationDir(directory: string): this { - this.customDestinationPath = directory - return this - } - - /** - * Define `appRoot`. This is just to shorten the logged - * file names. For example: - */ - public appRoot(directory: string): this { - this.customAppRoot = directory - return this - } - - /** - * Instruct to use mustache - */ - public useMustache() { - this.mustache = true - return this - } - - /** - * Variables for stub subsitution - */ - public apply(contents: any): this { - this.templateData = contents - return this - } - - /** - * Returns the file json - */ - public toJSON() { - const extension = this.options.extname || '.ts' - - const filename = new StringTransformer(basename(this.name)) - .dropExtension() - .cleanSuffix(this.options.suffix) - .cleanPrefix(this.options.prefix) - .changeForm(this.options.form, this.options.formIgnoreList) - .addSuffix(this.options.suffix) - .addPrefix(this.options.prefix) - .changeCase(this.options.pattern) - .toValue() - - const initialFilePath = this.name.replace(basename(this.name), filename) - - const appRoot = this.customAppRoot || process.cwd() - - /** - * Computes the file absolute path, where the file will be created. - * - * 1. If `customDestinationPath` is not defined, we will merge the - * `appRoot` + `initialFilePath`. - * - * 2. If `customDestinationPath` is absolute, then we ignore the appRoot - * and merge `customDestinationPath` + `initialFilePath` - * - * 3. Otherwise we merge `appRoot` + `customDestinationPath` + `initialFilePath`. - */ - const filepath = this.customDestinationPath - ? isAbsolute(this.customDestinationPath) - ? join(this.customDestinationPath, initialFilePath) - : join(appRoot, this.customDestinationPath, initialFilePath) - : join(appRoot, initialFilePath) - - /** - * Passing user values + the filename and extension - */ - const templateContents = Object.assign({ extension, filename }, this.templateData) - - /** - * Contents of the template file - */ - const contents = this.stubContents - ? this.isStubRaw - ? template(this.stubContents, templateContents, undefined, this.mustache) - : templateFromFile(this.stubContents, templateContents, this.mustache) - : '' - - return { - filename, - filepath: `${filepath}${extension}`, - relativepath: this.getFileRelativePath(`${filepath}${extension}`), - extension, - contents, - state: this.state, - } - } -} diff --git a/src/Generator/StringTransformer.ts b/src/Generator/StringTransformer.ts deleted file mode 100644 index 7bda1dd..0000000 --- a/src/Generator/StringTransformer.ts +++ /dev/null @@ -1,124 +0,0 @@ -/* - * @adonisjs/ace - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { extname } from 'path' -import { string } from '@poppinss/utils/build/helpers' - -/** - * Exposes the API to transform a string - */ -export class StringTransformer { - constructor(private input: string) {} - - /** - * Cleans suffix from the input. - */ - public cleanSuffix(suffix?: string): this { - if (!suffix) { - return this - } - - this.input = this.input.replace(new RegExp(`[-_]?${suffix}$`, 'i'), '') - return this - } - - /** - * Cleans prefix from the input. - */ - public cleanPrefix(prefix?: string): this { - if (!prefix) { - return this - } - - this.input = this.input.replace(new RegExp(`^${prefix}[-_]?`, 'i'), '') - return this - } - - /** - * Add suffix to the file name - */ - public addSuffix(suffix?: string): this { - if (!suffix) { - return this - } - - this.input = `${this.input}_${suffix}` - return this - } - - /** - * Add prefix to the file name - */ - public addPrefix(prefix?: string): this { - if (!prefix) { - return this - } - - this.input = `${prefix}_${this.input}` - return this - } - - /** - * Changes the name form by converting it to singular - * or plural case - */ - public changeForm(form?: 'singular' | 'plural', ignoreList?: string[]): this { - if (!form) { - return this - } - - /** - * Do not change form when word is in ignore list - */ - if ((ignoreList || []).find((word) => word.toLowerCase() === this.input.toLowerCase())) { - return this - } - - this.input = form === 'singular' ? string.singularize(this.input) : string.pluralize(this.input) - return this - } - - /** - * Changes the input case - */ - public changeCase(pattern?: 'pascalcase' | 'camelcase' | 'snakecase' | 'dashcase'): this { - switch (pattern) { - case 'camelcase': - this.input = string.camelCase(this.input) - return this - case 'pascalcase': - const camelCase = string.camelCase(this.input) - this.input = `${camelCase.charAt(0).toUpperCase()}${camelCase.slice(1)}` - return this - case 'snakecase': - this.input = string.snakeCase(this.input) - return this - case 'dashcase': - this.input = string.dashCase(this.input) - return this - default: - return this - } - } - - /** - * Drops the extension from the input - */ - public dropExtension(): this { - this.input = this.input.replace(new RegExp(`${extname(this.input)}$`), '') - return this - } - - /** - * Returns the transformed value - */ - public toValue() { - return this.input - } -} diff --git a/src/Generator/index.ts b/src/Generator/index.ts deleted file mode 100644 index f567bd8..0000000 --- a/src/Generator/index.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * @adonisjs/ace - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { outputFile, pathExists } from 'fs-extra' - -import { GeneratorFile } from './File' -import { GeneratorFileOptions, GeneratorContract, CommandContract } from '../Contracts' - -/** - * Exposes the API to generate entity files, like project - * `Controllers`, `Models` and so on. - */ -export class Generator implements GeneratorContract { - private files: GeneratorFile[] = [] - - constructor(private command: CommandContract, private destinationDir?: string) {} - - /** - * Add a new file to the files generator. You can add multiple files - * together and they will be created when `run` is invoked. - */ - public addFile(name: string, options?: GeneratorFileOptions) { - const file = new GeneratorFile(name, options) - - if (this.destinationDir) { - file.destinationDir(this.destinationDir) - } - - this.files.push(file) - return file - } - - /** - * Run the generator and create all files registered using `addFiles` - */ - public async run() { - for (let file of this.files) { - const fileJSON = file.toJSON() - const exists = await pathExists(fileJSON.filepath) - - if (exists) { - this.command.logger.action('create').skipped(fileJSON.relativepath, 'File already exists') - } else { - await outputFile(fileJSON.filepath, fileJSON.contents) - file.state = 'persisted' - this.command.logger.action('create').succeeded(fileJSON.relativepath) - } - } - - return this.files - } - - /** - * Clear the registered files from the generator - */ - public clear() { - this.files = [] - } -} diff --git a/src/HelpCommand/index.ts b/src/HelpCommand/index.ts deleted file mode 100644 index 9592c62..0000000 --- a/src/HelpCommand/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * @adonisjs/ace - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { BaseCommand } from '../BaseCommand' -import { CommandContract } from '../Contracts' - -/** - * The help command for print the help output - */ -export class HelpCommand extends BaseCommand implements CommandContract { - public static commandName = 'help' - public static description = 'See help for all the commands' - - public async run() { - this.kernel.printHelp() - } -} diff --git a/src/Hooks/index.ts b/src/Hooks/index.ts deleted file mode 100644 index f6019cf..0000000 --- a/src/Hooks/index.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * @adonisjs/ace - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -/** - * Exposes the API to register and execute async hooks - */ -export class Hooks { - private hooks: { - before: Map void | Promise)[]> - after: Map void | Promise)[]> - } = { - before: new Map(), - after: new Map(), - } - - /** - * Register hook for a given action and lifecycle - */ - public add( - lifecycle: 'before' | 'after', - action: string, - handler: (...args: any[]) => void | Promise - ): this { - const handlers = this.hooks[lifecycle].get(action) - if (handlers) { - handlers.push(handler) - } else { - this.hooks[lifecycle].set(action, [handler]) - } - - return this - } - - /** - * Execute hooks for a given action and lifecycle - */ - public async execute(lifecycle: 'before' | 'after', action: string, data: any): Promise { - const handlers = this.hooks[lifecycle].get(action) - if (!handlers) { - return - } - - for (let handler of handlers) { - await handler(data as any) - } - } -} diff --git a/src/Kernel/index.ts b/src/Kernel/index.ts deleted file mode 100644 index d2497ba..0000000 --- a/src/Kernel/index.ts +++ /dev/null @@ -1,755 +0,0 @@ -/* - * @adonisjs/ace - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { ApplicationContract } from '@ioc:Adonis/Core/Application' - -import { Hooks } from '../Hooks' -import { Parser } from '../Parser' -import { HelpCommand } from '../HelpCommand' -import { ManifestLoader } from '../Manifest/Loader' -import { InvalidCommandException } from '../Exceptions' -import { printHelp, printHelpFor } from '../utils/help' -import { validateCommand } from '../utils/validateCommand' - -import { - CommandFlag, - KernelContract, - CommandContract, - ManifestCommand, - RunHookCallback, - FindHookCallback, - GlobalFlagHandler, - CommandConstructorContract, -} from '../Contracts' -import { logger, isInteractive } from '@poppinss/cliui' - -/** - * Ace kernel class is used to register, find and invoke commands by - * parsing `process.argv.splice(2)` value. - */ -export class Kernel implements KernelContract { - /** - * Reference to hooks class to execute lifecycle - * hooks - */ - private hooks = new Hooks() - - /** - * Reference to the manifest loader. If defined, we will give preference - * to the manifest files. - */ - private manifestLoader: ManifestLoader - - /** - * The command that started the process - */ - private entryCommand?: CommandContract - - /** - * The state of the kernel - */ - private state: 'idle' | 'running' | 'completed' = 'idle' - - /** - * Exit handler for gracefully exiting the process - */ - private exitHandler: (callback: this) => void | Promise = (kernel) => { - if (kernel.error && typeof kernel.error.handle === 'function') { - kernel.error.handle(kernel.error) - } else if (kernel.error) { - logger.fatal(kernel.error) - } - - process.exit(kernel.exitCode === undefined ? 0 : kernel.exitCode) - } - - /** - * Find if CLI process is interactive. This flag can be - * toggled programmatically - */ - public isInteractive: boolean = isInteractive - - /** - * Find if console output is mocked - */ - public isMockingConsoleOutput: boolean = false - - /** - * The default command that will be invoked when no command is - * defined - */ - public defaultCommand: CommandConstructorContract = HelpCommand - - /** - * List of registered commands - */ - public commands: { [name: string]: CommandConstructorContract } = {} - public aliases: { [alias: string]: string } = this.application.rcFile.commandsAliases - - /** - * List of registered flags - */ - public flags: { [name: string]: CommandFlag & { handler: GlobalFlagHandler } } = {} - - /** - * The exit code for the process - */ - public exitCode?: number - - /** - * The error collected as part of the running commands or executing - * flags - */ - public error?: any - - constructor(public application: ApplicationContract) {} - - /** - * Executing global flag handlers. The global flag handlers are - * not async as of now, but later we can look into making them - * async. - */ - private executeGlobalFlagsHandlers(argv: string[], command?: CommandConstructorContract) { - const globalFlags = Object.keys(this.flags) - const parsedOptions = new Parser(this.flags).parse(argv, command) - - globalFlags.forEach((name) => { - const value = parsedOptions[name] - - /** - * Flag was not specified - */ - if (value === undefined) { - return - } - - /** - * Calling the handler - */ - this.flags[name].handler(parsedOptions[name], parsedOptions, command) - }) - } - - /** - * Returns an array of all registered commands - */ - private getAllCommandsAndAliases() { - let commands: (ManifestCommand | CommandConstructorContract)[] = Object.keys(this.commands).map( - (name) => this.commands[name] - ) - - let aliases = {} - - /** - * Concat manifest commands when they exists - */ - if (this.manifestLoader && this.manifestLoader.booted) { - const { commands: manifestCommands, aliases: manifestAliases } = - this.manifestLoader.getCommands() - - commands = commands.concat(manifestCommands) - aliases = Object.assign(aliases, manifestAliases) - } - - return { - commands, - aliases: Object.assign(aliases, this.aliases), - } - } - - /** - * Processes the args and sets values on the command instance - */ - private async processCommandArgsAndFlags(commandInstance: CommandContract, args: string[]) { - const parser = new Parser(this.flags) - const command = commandInstance.constructor as CommandConstructorContract - - /** - * Parse the command arguments. The `parse` method will raise exception if flag - * or arg is not - */ - const parsedOptions = parser.parse(args, command) - - /** - * We validate the command arguments after the global flags have been - * executed. It is required, since flags may have nothing to do - * with the validaty of command itself - */ - command.args.forEach((arg, index) => { - parser.validateArg(arg, index, parsedOptions, command) - }) - - /** - * Creating a new command instance and setting - * parsed options on it. - */ - commandInstance.parsed = parsedOptions - - /** - * Setup command instance argument and flag - * properties. - */ - for (let i = 0; i < command.args.length; i++) { - const arg = command.args[i] - const defaultValue = commandInstance[arg.propertyName] - - if (arg.type === 'spread') { - const value = parsedOptions._.slice(i) - - /** - * Set the property value to arguments defined via the CLI - * If no arguments are supplied, then use the default value assigned to the class property - * If the default value is undefined, then assign an empty array - */ - commandInstance[arg.propertyName] = value.length - ? value - : defaultValue !== undefined - ? defaultValue - : [] - - break - } else { - const value = parsedOptions._[i] - commandInstance[arg.propertyName] = value !== undefined ? value : defaultValue - } - } - - /** - * Set flag value on the command instance - */ - for (let flag of command.flags) { - const flagValue = parsedOptions[flag.name] - if (flagValue !== undefined) { - commandInstance[flag.propertyName] = flagValue - } - } - } - - /** - * Execute the main command. For calling commands within commands, - * one must call "kernel.exec". - */ - private async execMain(commandName: string, args: string[]) { - const command = await this.find([commandName]) - - /** - * Command not found. So execute global flags handlers and - * raise an exception - */ - if (!command) { - this.executeGlobalFlagsHandlers(args) - throw InvalidCommandException.invoke(commandName, this.getSuggestions(commandName)) - } - - /** - * Make an instance of the command - */ - const commandInstance = await this.application.container.makeAsync(command, [ - this.application, - this, - ]) - - /** - * Execute global flags - */ - this.executeGlobalFlagsHandlers(args, command) - - /** - * Process the arguments and flags for the command - */ - await this.processCommandArgsAndFlags(commandInstance, args) - - /** - * Keep a reference to the entry command. So that we know if we - * want to entertain `.exit` or not - */ - this.entryCommand = commandInstance - - /** - * Execute before run hooks - */ - await this.hooks.execute('before', 'run', commandInstance) - - /** - * Execute command - */ - return commandInstance.exec() - } - - /** - * Handles exiting the process - */ - private async exitProcess(error?: any) { - /** - * Check for state to avoid exiting the process multiple times - */ - if (this.state === 'completed') { - return - } - - this.state = 'completed' - - /** - * Re-assign error if entry command exists and has error - */ - if (!error && this.entryCommand && this.entryCommand.error) { - error = this.entryCommand.error - } - - /** - * Execute the after run hooks. Wrapping inside try/catch since this is the - * cleanup handler for the process and must handle all exceptions - */ - try { - if (this.entryCommand) { - await this.hooks.execute('after', 'run', this.entryCommand) - } - } catch (hookError) { - error = hookError - } - - /** - * Assign error to the kernel instance - */ - if (error) { - this.error = error - } - - /** - * Figure out the exit code for the process - */ - const exitCode = error ? 1 : 0 - const commandExitCode = this.entryCommand && this.entryCommand.exitCode - - this.exitCode = commandExitCode === undefined ? exitCode : commandExitCode - - try { - await this.exitHandler(this) - } catch (exitHandlerError) { - logger.warning('Expected exit handler to exit the process. Instead it raised an exception') - logger.fatal(exitHandlerError) - } - } - - /** - * Register a before hook - */ - public before(action: 'run', callback: RunHookCallback): this - public before(action: 'find', callback: FindHookCallback): this - public before(action: 'run' | 'find', callback: RunHookCallback | FindHookCallback): this { - this.hooks.add('before', action, callback) - return this - } - - /** - * Register an after hook - */ - public after(action: 'run', callback: RunHookCallback): this - public after(action: 'find', callback: FindHookCallback): this - public after(action: 'run' | 'find', callback: RunHookCallback | FindHookCallback): this { - this.hooks.add('after', action, callback) - return this - } - - /** - * Register an array of command constructors - */ - public register(commands: CommandConstructorContract[]): this { - commands.forEach((command) => { - command.boot() - validateCommand(command) - this.commands[command.commandName] = command - - /** - * Registering command aliaes - */ - command.aliases.forEach((alias) => (this.aliases[alias] = command.commandName)) - }) - - return this - } - - /** - * Register a global flag. It can be defined in combination with - * any command. - */ - public flag( - name: string, - handler: GlobalFlagHandler, - options: Partial> - ): this { - this.flags[name] = Object.assign( - { - name, - propertyName: name, - handler, - type: 'boolean', - }, - options - ) - - return this - } - - /** - * Use manifest instance to lazy load commands - */ - public useManifest(manifestLoader: ManifestLoader): this { - this.manifestLoader = manifestLoader - return this - } - - /** - * Register an exit handler - */ - public onExit(callback: (kernel: this) => void | Promise): this { - this.exitHandler = callback - return this - } - - /** - * Returns an array of command names suggestions for a given name. - */ - public getSuggestions(name: string, distance = 3): string[] { - const leven = require('leven') - const { commands, aliases } = this.getAllCommandsAndAliases() - - const suggestions = commands - .filter(({ commandName }) => leven(name, commandName) <= distance) - .map(({ commandName }) => commandName) - - return suggestions.concat( - Object.keys(aliases).filter((alias) => leven(name, alias) <= distance) - ) - } - - /** - * Preload the manifest file. Re-running this method twice will - * result in a noop - */ - public async preloadManifest() { - /** - * Load manifest commands when instance of manifest loader exists. - */ - if (this.manifestLoader) { - await this.manifestLoader.boot() - } - } - - /** - * Finds the command from the command line argv array. If command for - * the given name doesn't exists, then it will return `null`. - * - * Does executes the before and after hooks regardless of whether the - * command has been found or not - */ - public async find(argv: string[]): Promise { - /** - * ---------------------------------------------------------------------------- - * Even though in `Unix` the command name may appear in between or at last, with - * ace we always want the command name to be the first argument. However, the - * arguments to the command itself can appear in any sequence. For example: - * - * Works - * - node ace make:controller foo - * - node ace make:controller --http foo - * - * Doesn't work - * - node ace foo make:controller - * ---------------------------------------------------------------------------- - */ - const [commandName] = argv - - /** - * Command name from the registered aliases - */ - const aliasCommandName = this.aliases[commandName] - - /** - * Manifest commands gets preference over manually registered commands. - * - * - We check the manifest loader is register - * - The manifest loader has the command - * - Or the manifest loader has the alias command - */ - const commandNode = this.manifestLoader - ? this.manifestLoader.hasCommand(commandName) - ? this.manifestLoader.getCommand(commandName) - : this.manifestLoader.hasCommand(aliasCommandName) - ? this.manifestLoader.getCommand(aliasCommandName) - : undefined - : undefined - - if (commandNode) { - commandNode.command.aliases = commandNode.command.aliases || [] - if (aliasCommandName && !commandNode.command.aliases.includes(commandName)) { - commandNode.command.aliases.push(commandName) - } - - await this.hooks.execute('before', 'find', commandNode.command) - const command = await this.manifestLoader.loadCommand(commandNode.command.commandName) - await this.hooks.execute('after', 'find', command) - return command - } else { - /** - * Try to find command inside manually registered command or fallback - * to null - */ - const command = this.commands[commandName] || this.commands[aliasCommandName] || null - - /** - * Share main command name as an alias with the command - */ - if (command) { - command.aliases = command.aliases || [] - if (aliasCommandName && !command.aliases.includes(commandName)) { - command.aliases.push(commandName) - } - } - - /** - * Executing before and after together to be compatible - * with the manifest find before and after hooks - */ - await this.hooks.execute('before', 'find', command) - await this.hooks.execute('after', 'find', command) - - return command - } - } - - /** - * Run the default command. The default command doesn't accept - * and args or flags. - */ - public async runDefaultCommand() { - this.defaultCommand.boot() - validateCommand(this.defaultCommand) - - /** - * Execute before/after find hooks - */ - await this.hooks.execute('before', 'find', this.defaultCommand) - await this.hooks.execute('after', 'find', this.defaultCommand) - - /** - * Make the command instance using the container - */ - const commandInstance = await this.application.container.makeAsync(this.defaultCommand, [ - this.application, - this, - ]) - - /** - * Execute before run hook - */ - await this.hooks.execute('before', 'run', commandInstance) - - /** - * Keep a reference to the entry command - */ - this.entryCommand = commandInstance - - /** - * Execute the command - */ - return commandInstance.exec() - } - - /** - * Find if a command is the main command. Main commands are executed - * directly from the terminal. - */ - public isMain(command: CommandContract): boolean { - return !!this.entryCommand && this.entryCommand === command - } - - /** - * Enforce mocking the console output. Command logs, tables, prompts - * will be mocked - */ - public mockConsoleOutput(): this { - this.isMockingConsoleOutput = true - return this - } - - /** - * Toggle interactive state - */ - public interactive(state: boolean): this { - this.isInteractive = state - return this - } - - /** - * Execute a command as a sub-command. Do not call "handle" and - * always use this method to invoke command programatically - */ - public async exec(commandName: string, args: string[]) { - const command = await this.find([commandName]) - - /** - * Command not found. - */ - if (!command) { - throw InvalidCommandException.invoke(commandName, this.getSuggestions(commandName)) - } - - /** - * Make an instance of command and keep a reference of it as `this.entryCommand` - */ - const commandInstance = await this.application.container.makeAsync(command, [ - this.application, - this, - ]) - - /** - * Process args and flags for the command - */ - await this.processCommandArgsAndFlags(commandInstance, args) - - let commandError: any - - /** - * Wrapping the command execution inside a try/catch, so that - * we can run the after hooks regardless of success or - * failure - */ - try { - await this.hooks.execute('before', 'run', commandInstance) - await commandInstance.exec() - } catch (error) { - commandError = error - } - - /** - * Execute after hooks - */ - await this.hooks.execute('after', 'run', commandInstance) - - /** - * Re-throw error (if any) - */ - if (commandError) { - throw commandError - } - - return commandInstance - } - - /** - * Makes instance of a given command by processing command line arguments - * and setting them on the command instance - */ - public async handle(argv: string[]) { - if (this.state !== 'idle') { - return - } - - this.state = 'running' - - try { - /** - * Preload the manifest file to load the manifest files - */ - this.preloadManifest() - - /** - * Branch 1 - * Run default command and invoke the exit handler - */ - if (!argv.length) { - await this.runDefaultCommand() - await this.exitProcess() - return - } - - /** - * Branch 2 - * No command has been mentioned and hence execute all the global flags - * invoke the exit handler - */ - const hasMentionedCommand = !argv[0].startsWith('-') - if (!hasMentionedCommand) { - this.executeGlobalFlagsHandlers(argv) - await this.exitProcess() - return - } - - /** - * Branch 3 - * Execute the given command as the main command - */ - const [commandName, ...args] = argv - await this.execMain(commandName, args) - - /** - * Exit the process if there isn't any entry command - */ - if (!this.entryCommand) { - await this.exitProcess() - return - } - - const entryCommandConstructor = this.entryCommand.constructor as CommandConstructorContract - - /** - * Exit the process if entry command isn't a stayalive command. Stayalive - * commands should call `this.exit` to exit the process. - */ - if (!entryCommandConstructor.settings.stayAlive) { - await this.exitProcess() - } - } catch (error) { - await this.exitProcess(error) - } - } - - /** - * Print the help screen for a given command or all commands/flags - */ - public printHelp( - command?: CommandConstructorContract, - commandsToAppend?: ManifestCommand[], - aliasesToAppend?: Record - ) { - let { commands, aliases } = this.getAllCommandsAndAliases() - - /** - * Append additional commands and aliases for help screen only - */ - if (commandsToAppend) { - commands = commands.concat(commandsToAppend) - } - if (aliasesToAppend) { - aliases = Object.assign({}, aliases, aliasesToAppend) - } - - if (command) { - printHelpFor(command, aliases) - } else { - const flags = Object.keys(this.flags).map((name) => this.flags[name]) - printHelp(commands, flags, aliases) - } - } - - /** - * Trigger kernel to exit the process. The call to this method - * is ignored when command is not same the `entryCommand`. - * - * In other words, subcommands cannot trigger exit - */ - public async exit(command: CommandContract, error?: any) { - if (command !== this.entryCommand) { - return - } - - await this.exitProcess(error) - } -} diff --git a/src/Manifest/Generator.ts b/src/Manifest/Generator.ts deleted file mode 100644 index 28f0f40..0000000 --- a/src/Manifest/Generator.ts +++ /dev/null @@ -1,117 +0,0 @@ -/* - * @adonisjs/ace - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { outputJSON } from 'fs-extra' -import { isAbsolute, extname, join } from 'path' -import { esmRequire, Exception } from '@poppinss/utils' -import { resolveFrom } from '@poppinss/utils/build/helpers' - -import { validateCommand } from '../utils/validateCommand' -import { CommandConstructorContract, ManifestNode } from '../Contracts' - -/** - * Exposes the API to generate the ace manifest file. The manifest file - * contains the meta data of all the registered commands. This speeds - * up the boot cycle of ace - */ -export class ManifestGenerator { - /** - * Here we keep track of processed command files to prevent loops. Mainly when using - * listDirectoryFiles to export command paths from directory and not excluding current file. - */ - private processedFiles: Set = new Set() - - constructor(private basePath: string, private commands: string[]) {} - - /** - * Loads a given command from the disk. A command line can recursively - * exposed sub command paths. But they should be resolvable using - * the base path - */ - private async loadCommand( - commandPath: string - ): Promise<{ command: CommandConstructorContract; commandPath: string }[]> { - if (isAbsolute(commandPath)) { - throw new Exception( - 'Absolute path to a command is not allowed when generating the manifest file' - ) - } - - const resolvedPath = resolveFrom(this.basePath, commandPath) - - if (this.processedFiles.has(resolvedPath)) { - return [] - } - - const commandOrSubCommandsPaths = esmRequire(resolvedPath) - - this.processedFiles.add(resolvedPath) - - if (Array.isArray(commandOrSubCommandsPaths)) { - return this.loadCommands(commandOrSubCommandsPaths) - } - - /** - * File export has command constructor - */ - validateCommand(commandOrSubCommandsPaths, commandPath) - - return [ - { - command: commandOrSubCommandsPaths, - commandPath, - }, - ] - } - - /** - * Loads all the commands from the disk recursively. - */ - private async loadCommands(commandPaths: string[]) { - let commands: { command: CommandConstructorContract; commandPath: string }[] = [] - - for (const commandPath of commandPaths) { - const command = await this.loadCommand(commandPath) - commands = commands.concat(command) - } - - return commands - } - - /** - * Generates and writes the ace manifest file to the base path - */ - public async generate() { - const commands = await this.loadCommands(this.commands) - - const manifest = commands.reduce( - (result, { command, commandPath }) => { - const manifestNode = { - settings: command.settings || {}, - commandPath: commandPath.replace(new RegExp(`${extname(commandPath)}$`), ''), - commandName: command.commandName, - description: command.description, - args: command.args, - aliases: command.aliases, - flags: command.flags, - } - - result.commands[command.commandName] = manifestNode - command.aliases.forEach((alias) => { - result.aliases[alias] = command.commandName - }) - - return result - }, - { commands: {}, aliases: {} } - ) - - await outputJSON(join(this.basePath, 'ace-manifest.json'), manifest, { spaces: 2 }) - } -} diff --git a/src/Manifest/Loader.ts b/src/Manifest/Loader.ts deleted file mode 100644 index 87f83a4..0000000 --- a/src/Manifest/Loader.ts +++ /dev/null @@ -1,149 +0,0 @@ -/* - * @adonisjs/ace - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { readJSON } from 'fs-extra' -import { esmRequire } from '@poppinss/utils' -import { resolveFrom } from '@poppinss/utils/build/helpers' - -import { - Aliases, - ManifestNode, - ManifestCommand, - ManifestLoaderContract, - CommandConstructorContract, -} from '../Contracts' - -import { validateCommand } from '../utils/validateCommand' - -/** - * The manifest loader exposes the API to load ace commands from one - * or more manifest files. - */ -export class ManifestLoader implements ManifestLoaderContract { - /** - * An array of defined manifest files - */ - private manifestFiles: (ManifestNode & { - basePath: string - })[] = [] - - public booted: boolean = false - - constructor(private files: { basePath: string; manifestAbsPath: string }[]) {} - - /** - * Loads the manifest file from the disk - */ - private async loadManifestFile(file: { basePath: string; manifestAbsPath: string }): Promise< - ManifestNode & { - basePath: string - } - > { - const manifestCommands = await readJSON(file.manifestAbsPath) - - /** - * Find if we are dealing with an old or the new manifest file - */ - const isNewManifestFile = manifestCommands['commands'] && manifestCommands['aliases'] - - const commands = isNewManifestFile ? manifestCommands['commands'] : manifestCommands - const aliases = isNewManifestFile ? manifestCommands['aliases'] : {} - - return { basePath: file.basePath, commands, aliases } - } - - /** - * Returns the command manifest node for a give command - */ - private getCommandManifest(commandName: string) { - return this.manifestFiles.find(({ commands, aliases }) => { - const aliasCommandName = aliases[commandName] - return commands[commandName] || commands[aliasCommandName] - }) - } - - /** - * Boot manifest loader to read all manifest files from the disk - */ - public async boot() { - if (this.booted) { - return - } - - this.booted = true - this.manifestFiles = await Promise.all(this.files.map((file) => this.loadManifestFile(file))) - } - - /** - * Returns base path for a given command - */ - public getCommandBasePath(commandName: string): string | undefined { - return this.getCommandManifest(commandName)?.basePath - } - - /** - * Returns manifest command node. One must load the command - * in order to use it - */ - public getCommand( - commandName: string - ): { basePath: string; command: ManifestCommand } | undefined { - const manifestCommands = this.getCommandManifest(commandName) - if (!manifestCommands) { - return - } - - const aliasCommandName = manifestCommands.aliases[commandName] - const command = - manifestCommands.commands[commandName] || manifestCommands.commands[aliasCommandName] - - return { - basePath: manifestCommands.basePath, - command: command, - } - } - - /** - * Find if a command exists or not - */ - public hasCommand(commandName: string): boolean { - return !!this.getCommandBasePath(commandName) - } - - /** - * Load command from the disk. Make sure to use [[hasCommand]] before - * calling this method - */ - public async loadCommand(commandName: string): Promise { - const { basePath, command } = this.getCommand(commandName)! - const commandConstructor = esmRequire(resolveFrom(basePath, command.commandPath)) - validateCommand(commandConstructor) - return commandConstructor - } - - /** - * Returns an array of manifest commands - */ - public getCommands() { - return this.manifestFiles.reduce<{ - commands: ManifestCommand[] - aliases: Aliases - }>( - (result, { commands, aliases }) => { - Object.keys(commands).forEach((commandName) => { - result.commands = result.commands.concat(commands[commandName]) - }) - - Object.assign(result.aliases, aliases) - return result - }, - { commands: [], aliases: {} } - ) - } -} diff --git a/src/Parser/index.ts b/src/Parser/index.ts deleted file mode 100644 index 3c58a33..0000000 --- a/src/Parser/index.ts +++ /dev/null @@ -1,342 +0,0 @@ -/* - * @adonisjs/ace - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import getopts from 'getopts' - -import { - CommandArg, - CommandFlag, - GlobalFlagHandler, - CommandConstructorContract, -} from '../Contracts' - -import { InvalidFlagException, MissingArgumentException, UnknownFlagException } from '../Exceptions' - -/** - * The job of the parser is to parse the command line values by taking - * the command `args`, `flags` and `globalFlags` into account. - */ -export class Parser { - constructor( - private registeredFlags: { - [name: string]: CommandFlag & { handler: GlobalFlagHandler } - } - ) {} - - /** - * Validate all the flags against the flags registered by the command - * or as global flags and disallow unknown flags. - */ - private scanForUnknownFlags(parsed: getopts.ParsedOptions, flagsAndAliases: string[]) { - Object.keys(parsed).forEach((key) => { - if (key === '_') { - return - } - - const hasFlag = flagsAndAliases.find((value) => value === key) - if (!hasFlag) { - throw UnknownFlagException.invoke(key) - } - }) - } - - /** - * Processes ace command flag to set the options for `getopts`. - * We just define the `alias` with getopts coz their default, - * string and boolean options produces the behavior we don't - * want. - */ - private preProcessFlag(flag: CommandFlag, options: getopts.Options) { - /** - * Register alias (when exists) - */ - if (flag.alias) { - options.alias![flag.alias] = flag.name - } - } - - /** - * Casts a flag value to a boolean. The casting logic is driven - * by the behavior of "getopts" - */ - private castToBoolean(value: any) { - if (typeof value === 'boolean') { - return value - } - - if (value === 'true' || value === '=true') { - return true - } - - return undefined - } - - /** - * Cast the value to a string. The casting logic is driven - * by the behavior of "getopts" - * - * - Convert numbers to string - * - Do not convert boolean to a string, since a flag without a value - * gets a boolean value, which is invalid - */ - private castToString(value: any) { - if (typeof value === 'number') { - value = String(value) - } - - if (typeof value === 'string' && value.trim()) { - return value - } - - return undefined - } - - /** - * Cast value to an array of string. The casting logic is driven - * by the behavior of "getopts" - * - * - Numeric values are converted to string of array - * - A string value is splitted by comma and trimmed. - * - An array is casted to an array of string values - */ - private castToArray(value: any) { - if (typeof value === 'number') { - value = String(value) - } - - if (typeof value === 'string') { - return value.split(',').filter((prop) => prop.trim()) - } - - if (Array.isArray(value)) { - /** - * This will also convert numeric values to a string. The behavior - * is same as string flag type. - */ - return value.map((prop) => String(prop)) - } - - return undefined - } - - /** - * Cast value to an array of numbers. The casting logic is driven - * by the behavior of "getopts". - * - * - Numeric values are wrapped to an array. - * - String is splitted by comma and each value is casted to a number - * - Each array value is casted to a number. - */ - private castToNumArray(value: any) { - if (typeof value === 'number') { - return [value] - } - - if (typeof value === 'string') { - return value.split(',').map((one) => Number(one)) - } - - if (Array.isArray(value)) { - return value.map((prop) => Number(prop)) - } - - return undefined - } - - /** - * Cast value to a number. The casting logic is driven - * by the behavior of "getopts" - * - * - Boolean values are not allowed - * - A string is converted to a number - */ - private castToNumer(value: any): number | undefined { - if (typeof value === 'number') { - return value - } - - if (typeof value === 'string') { - // Possibility of NaN here - return Number(value) - } - - return undefined - } - - /** - * Casts value of a flag to it's expected data type. These values - * are then later validated to ensure that casting was successful. - */ - public processFlag( - flag: CommandFlag, - parsed: getopts.ParsedOptions, - command?: CommandConstructorContract - ) { - let value = parsed[flag.name] - - /** - * Check for the value with the alias, if it undefined - * by the name - */ - if (value === undefined && flag.alias) { - value = parsed[flag.alias] - } - - /** - * Still undefined?? - * - * It is fine. Flags are optional anyways - */ - if (value === undefined) { - return - } - - /** - * Handle boolean values. It should be a valid boolean - * data type or a string value of `'true'`. - */ - if (flag.type === 'boolean') { - value = this.castToBoolean(value) - if (value === undefined) { - throw InvalidFlagException.invoke(flag.name, flag.type, command) - } - } - - /** - * Handle string value. It should be a valid and not empty. - * Either remove the flag or provide a value - */ - if (flag.type === 'string') { - value = this.castToString(value) - if (value === undefined) { - throw InvalidFlagException.invoke(flag.name, flag.type, command) - } - } - - /** - * Handle numeric values. The flag should have a value and - * a valid number. - */ - if (flag.type === 'number') { - value = this.castToNumer(value) - if (value === undefined || isNaN(value)) { - throw InvalidFlagException.invoke(flag.name, flag.type, command) - } - } - - /** - * Parse the value to be an array of strings - */ - if (flag.type === 'array') { - value = this.castToArray(value) - if (!value || !value.length) { - throw InvalidFlagException.invoke(flag.name, flag.type, command) - } - } - - /** - * Parse the value to be an array of numbers - */ - if (flag.type === 'numArray') { - value = this.castToNumArray(value) - if (!value || !value.length) { - throw InvalidFlagException.invoke(flag.name, flag.type, command) - } - - /** - * Find if array has NaN values - */ - if (value.findIndex((one: any) => isNaN(one)) > -1) { - throw InvalidFlagException.invoke(flag.name, flag.type, command) - } - } - - parsed[flag.name] = value - if (flag.alias) { - parsed[flag.alias] = value - } - } - - /** - * Validates the value to ensure that values are defined for - * required arguments. - */ - public validateArg( - arg: CommandArg, - index: number, - parsed: getopts.ParsedOptions, - command: CommandConstructorContract - ) { - const value = parsed._[index] - - if (value === undefined && arg.required) { - throw MissingArgumentException.invoke(arg.name, command) - } - } - - /** - * Parses argv and executes the command and global flags handlers - */ - public parse(argv: string[], command?: CommandConstructorContract): getopts.ParsedOptions { - let options = { alias: {}, boolean: [], default: {}, string: [] } - const flagsAndAliases: string[] = [] - - const globalFlags = Object.keys(this.registeredFlags).map((name) => this.registeredFlags[name]) - - /** - * Build options from global flags - */ - globalFlags.forEach((flag) => { - this.preProcessFlag(flag, options) - flagsAndAliases.push(flag.name) - flag.alias && flagsAndAliases.push(flag.alias) - }) - - /** - * Build options from command flags - */ - if (command) { - command.flags.forEach((flag) => { - this.preProcessFlag(flag, options) - flagsAndAliases.push(flag.name) - flag.alias && flagsAndAliases.push(flag.alias) - }) - } - - /** - * Parsing argv with the previously built options - */ - const parsed = getopts(argv, options) - - /** - * Scan and report unknown flag as exception - */ - if (command) { - this.scanForUnknownFlags(parsed, flagsAndAliases) - } - - /** - * Validating global flags (if any) - */ - globalFlags.forEach((flag) => { - this.processFlag(flag, parsed) - }) - - /** - * Validating command flags (if command is defined) - */ - if (command) { - command.flags.forEach((flag) => { - this.processFlag(flag, parsed) - }) - } - - return parsed - } -} diff --git a/src/commands/base.ts b/src/commands/base.ts new file mode 100644 index 0000000..ff22ce1 --- /dev/null +++ b/src/commands/base.ts @@ -0,0 +1,509 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import string from '@poppinss/utils/string' +import lodash from '@poppinss/utils/lodash' +import { defineStaticProperty } from '@poppinss/utils' + +import * as errors from '../errors.js' +import type { Kernel } from '../kernel.js' +import type { + Flag, + Argument, + ParsedOutput, + UIPrimitives, + CommandOptions, + CommandMetaData, + FlagsParserOptions, + ArgumentsParserOptions, +} from '../types.js' + +/** + * The base command sets the foundation for defining ace commands. + * Every command should inherit from the base command. + */ +export class BaseCommand { + static booted: boolean = false + + /** + * Configuration options accepted by the command + */ + static options: CommandOptions + + /** + * A collection of aliases for the command + */ + static aliases: string[] + + /** + * The command name one can type to run the command + */ + static commandName: string + + /** + * The command description + */ + static description: string + + /** + * The help text for the command. Help text can be a multiline + * string explaining the usage of command + */ + static help?: string | string[] + + /** + * Registered arguments + */ + static args: Argument[] + + /** + * Registered flags + */ + static flags: Flag[] + + /** + * Define static properties on the class. During inheritance, certain + * properties must inherit from the parent. + */ + static boot() { + if (Object.hasOwn(this, 'booted') && this.booted === true) { + return + } + + this.booted = true + defineStaticProperty(this, 'args', { initialValue: [], strategy: 'inherit' }) + defineStaticProperty(this, 'flags', { initialValue: [], strategy: 'inherit' }) + defineStaticProperty(this, 'aliases', { initialValue: [], strategy: 'define' }) + defineStaticProperty(this, 'commandName', { initialValue: '', strategy: 'define' }) + defineStaticProperty(this, 'description', { initialValue: '', strategy: 'define' }) + defineStaticProperty(this, 'help', { initialValue: '', strategy: 'define' }) + defineStaticProperty(this, 'options', { + initialValue: { staysAlive: false, handlesSignals: false, allowUnknownFlags: false }, + strategy: 'inherit', + }) + } + + /** + * Specify the argument the command accepts. The arguments will be accepted + * in the same order as they are defined. + * + * Mostly, you will be using the `@args` decorator to define the arguments. + * + * ```ts + * Command.defineArgument('entity', { type: 'string' }) + * ``` + */ + static defineArgument(name: string, options: Partial & { type: 'string' | 'spread' }) { + this.boot() + const arg = { name, argumentName: string.dashCase(name), required: true, ...options } + const lastArg = this.args[this.args.length - 1] + + /** + * Ensure the arg type is specified + */ + if (!arg.type) { + throw new errors.E_MISSING_ARG_TYPE([`${this.name}.${name}`]) + } + + /** + * Ensure we are not adding arguments after a spread argument + */ + if (lastArg && lastArg.type === 'spread') { + throw new errors.E_CANNOT_DEFINE_ARG([`${this.name}.${name}`, `${this.name}.${lastArg.name}`]) + } + + /** + * Ensure we are not adding a required argument after an optional + * argument + */ + if (arg.required && lastArg && lastArg.required === false) { + throw new errors.E_CANNOT_DEFINE_REQUIRED_ARG([ + `${this.name}.${name}`, + `${this.name}.${lastArg.name}`, + ]) + } + + this.args.push(arg) + } + + /** + * Specify a flag the command accepts. + * + * Mostly, you will be using the `@flags` decorator to define a flag. + * + * ```ts + * Command.defineFlag('connection', { type: 'string', required: true }) + * ``` + */ + static defineFlag( + name: string, + options: Partial & { type: 'string' | 'boolean' | 'array' | 'number' } + ) { + this.boot() + const flag = { name, flagName: string.dashCase(name), required: false, ...options } + + /** + * Ensure the arg type is specified + */ + if (!flag.type) { + throw new errors.E_MISSING_FLAG_TYPE([`${this.name}.${name}`]) + } + + this.flags.push(flag) + } + + /** + * Returns the options for parsing flags and arguments + */ + static getParserOptions(options?: FlagsParserOptions): { + flagsParserOptions: Required + argumentsParserOptions: ArgumentsParserOptions[] + } { + this.boot() + + const argumentsParserOptions: ArgumentsParserOptions[] = this.args.map((arg) => { + return { + type: arg.type, + default: arg.default, + parse: arg.parse, + } + }) + + const flagsParserOptions: Required = lodash.merge( + { + all: [], + string: [], + boolean: [], + array: [], + number: [], + alias: {}, + count: [], + coerce: {}, + default: {}, + }, + options + ) + + this.flags.forEach((flag) => { + flagsParserOptions.all.push(flag.flagName) + + if (flag.alias) { + flagsParserOptions.alias[flag.flagName] = flag.alias + } + if (flag.parse) { + flagsParserOptions.coerce[flag.flagName] = flag.parse + } + if (flag.default !== undefined) { + flagsParserOptions.default[flag.flagName] = flag.default + } + + switch (flag.type) { + case 'string': + flagsParserOptions.string.push(flag.flagName) + break + case 'boolean': + flagsParserOptions.boolean.push(flag.flagName) + break + case 'number': + flagsParserOptions.number.push(flag.flagName) + break + case 'array': + flagsParserOptions.array.push(flag.flagName) + break + } + }) + + return { + flagsParserOptions, + argumentsParserOptions, + } + } + + /** + * Serializes the command to JSON. The return value satisfies the + * {@link CommandMetaData} + */ + static serialize(): CommandMetaData { + this.boot() + if (!this.commandName) { + throw new errors.E_MISSING_COMMAND_NAME([this.name]) + } + + const [namespace, name] = this.commandName.split(':') + + return { + commandName: this.commandName, + description: this.description, + help: this.help, + namespace: name ? namespace : null, + aliases: this.aliases, + flags: this.flags.map((flag) => { + const { parse, ...rest } = flag + return rest + }), + args: this.args.map((arg) => { + const { parse, ...rest } = arg + return rest + }), + options: this.options, + } + } + + /** + * Validate the yargs parsed output againts the command. + */ + static validate(parsedOutput: ParsedOutput) { + this.boot() + + /** + * Validates args and their values + */ + this.args.forEach((arg, index) => { + const value = parsedOutput.args[index] as string + const hasDefinedArgument = value !== undefined + + if (arg.required && !hasDefinedArgument) { + throw new errors.E_MISSING_ARG([arg.name]) + } + + if (hasDefinedArgument && !arg.allowEmptyValue && (value === '' || !value.length)) { + throw new errors.E_MISSING_ARG_VALUE([arg.name]) + } + }) + + /** + * Disallow unknown flags + */ + if (!this.options.allowUnknownFlags && parsedOutput.unknownFlags.length) { + const unknowFlag = parsedOutput.unknownFlags[0] + const unknowFlagName = unknowFlag.length === 1 ? `-${unknowFlag}` : `--${unknowFlag}` + throw new errors.E_UNKNOWN_FLAG([unknowFlagName]) + } + + /** + * Validate flags + */ + this.flags.forEach((flag) => { + const hasMentionedFlag = Object.hasOwn(parsedOutput.flags, flag.flagName) + const value = parsedOutput.flags[flag.flagName] + + /** + * Validate the value by flag type + */ + switch (flag.type) { + case 'boolean': + /** + * If flag is required, then it should be mentioned + */ + if (flag.required && !hasMentionedFlag) { + throw new errors.E_MISSING_FLAG([flag.flagName]) + } + break + case 'number': + /** + * If flag is required, then it should be mentioned + */ + if (flag.required && !hasMentionedFlag) { + throw new errors.E_MISSING_FLAG([flag.flagName]) + } + + /** + * Regardless of whether flag is required or not. If it is mentioned, + * then some value should be provided. + * + * In case of number input, yargs sends undefined + */ + if (hasMentionedFlag && value === undefined) { + throw new errors.E_MISSING_FLAG_VALUE([flag.flagName]) + } + + if (Number.isNaN(value)) { + throw new errors.E_INVALID_FLAG([flag.flagName, 'numeric']) + } + break + case 'string': + case 'array': + /** + * If flag is required, then it should be mentioned + */ + if (flag.required && !hasMentionedFlag) { + throw new errors.E_MISSING_FLAG([flag.flagName]) + } + + /** + * Regardless of whether flag is required or not. If it is mentioned, + * then some value should be provided, unless empty values are + * allowed. + * + * In case of string, flag with no value receives an empty string + * In case of array, flag with no value receives an empty array + */ + if (hasMentionedFlag && !flag.allowEmptyValue && (value === '' || !value.length)) { + throw new errors.E_MISSING_FLAG_VALUE([flag.flagName]) + } + } + }) + } + + /** + * Create the command instance by validating the parsed input. It is + * recommended to use this method over create a new instance + * directly. + */ + static create( + this: T, + kernel: Kernel, + parsed: ParsedOutput, + ui: UIPrimitives + ): InstanceType { + this.validate(parsed) + + /** + * Type casting is needed because of this issue + * https://github.com/microsoft/TypeScript/issues/5863 + */ + return new this(kernel, parsed, ui) as InstanceType + } + + /** + * The exit code for the command + */ + exitCode?: number + + /** + * The error raised at the time of the executing the command. + * The value is undefined if no error is raised. + */ + error?: any + + /** + * The result property stores the return value of the "run" + * method (unless commands sets it explicitly) + */ + result?: any + + /** + * Logger to log messages + */ + get logger() { + return this.ui.logger + } + + /** + * Add colors to console messages + */ + get colors() { + return this.ui.colors + } + + constructor(protected kernel: Kernel, protected parsed: ParsedOutput, public ui: UIPrimitives) { + this.#consumeParsedOutput() + } + + /** + * Consume the parsed output and set property values on the command + */ + #consumeParsedOutput() { + const CommandConstructor = this.constructor as typeof BaseCommand + + /** + * Set args as properties on the command instance + */ + CommandConstructor.args.forEach((arg, index) => { + Object.defineProperty(this, arg.name, { + value: this.parsed.args[index], + enumerable: true, + writable: true, + configurable: true, + }) + }) + + /** + * Set flags as properties on the command instance + */ + CommandConstructor.flags.forEach((flag) => { + Object.defineProperty(this, flag.name, { + value: this.parsed.flags[flag.flagName], + enumerable: true, + writable: true, + configurable: true, + }) + }) + } + + /** + * The prepare template method is used to prepare the + * state for the command. This is the first method + * executed on a given command instance. + */ + async prepare() {} + + /** + * The interact template method is used to display the prompts + * to the user. The method is called after the prepare + * method. + */ + async interact() {} + + /** + * The run method should include the implementation for the + * command. + */ + async run(): Promise {} + + /** + * The completed method is the method invoked after the command + * finishes or results in an error. + * + * You can access the command error using the `this.error` property. + * Returning `true` from completed method supresses the error + * reporting to the kernel layer. + */ + async completed(): Promise {} + + /** + * Executes the commands by running the command template methods. + * The following methods are executed in order they are mentioned. + * + * - prepare + * - interact + * - run + * - completed (runs regardless of error) + */ + async exec() { + try { + await this.prepare() + await this.interact() + this.result = await this.run() + this.exitCode = this.exitCode ?? 0 + } catch (error) { + this.error = error + this.exitCode = this.exitCode ?? 1 + } + + const errorHandled = await this.completed() + + /** + * Print the error if the completed method has not + * handled it already + */ + if (!errorHandled && this.error) { + this.logger.fatal(this.error) + } + + return this.result + } + + /** + * Invokes the terminate method on the kernel + */ + async terminate() { + this.kernel.terminate(this) + } +} diff --git a/src/commands/help.ts b/src/commands/help.ts new file mode 100644 index 0000000..67c621f --- /dev/null +++ b/src/commands/help.ts @@ -0,0 +1,173 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { TERMINAL_SIZE, wrap } from '@poppinss/cliui/helpers' + +import { BaseCommand } from './base.js' +import { args } from '../decorators/args.js' +import { ListFormatter } from '../formatters/list.js' +import { FlagFormatter } from '../formatters/flag.js' +import { renderErrorWithSuggestions } from '../helpers.js' +import { CommandFormatter } from '../formatters/command.js' +import { ArgumentFormatter } from '../formatters/argument.js' +import type { CommandMetaData, ListTable } from '../types.js' + +/** + * The Help command is used to view help for a given command + */ +export class HelpCommand extends BaseCommand { + /** + * Command metadata + */ + static commandName: string = 'help' + static description: string = 'View help for a given command' + + /** + * The command name argument + */ + @args.string({ description: 'Command name', argumentName: 'command' }) + commandName!: string + + /** + * Returns the command arguments table + */ + #makeArgumentsTable(heading: string, command: CommandMetaData): ListTable[] { + if (!command.args.length) { + return [] + } + + return [ + { + heading: this.colors.yellow(heading), + columns: command.args.map((arg) => { + const formatter = new ArgumentFormatter(arg, this.colors) + return { + option: formatter.formatListOption(), + description: formatter.formatDescription(), + } + }), + }, + ] + } + + /** + * Returns the commands options table + */ + #makeOptionsTable(heading: string, command: CommandMetaData): ListTable[] { + if (!command.flags.length) { + return [] + } + + return [ + { + heading: this.colors.yellow(heading), + columns: command.flags.map((flag) => { + const formatter = new FlagFormatter(flag, this.colors) + return { + option: formatter.formatOption(), + description: formatter.formatDescription(), + } + }), + }, + ] + } + + /** + * Validates the command name to ensure it exists + */ + #validateCommandName(): boolean { + const command = this.kernel.getCommand(this.commandName) + if (!command) { + renderErrorWithSuggestions( + this.ui, + `Command "${this.colors}" is not defined`, + this.kernel.getNamespaceSuggestions(this.commandName) + ) + return false + } + + return true + } + + /** + * Logs command description + */ + protected renderDescription(command: CommandMetaData) { + const formatter = new CommandFormatter(command, this.colors) + const description = wrap([formatter.formatDescription()], { + startColumn: 2, + trimStart: false, + endColumn: TERMINAL_SIZE, + }).join('\n') + + this.logger.log('') + this.logger.log(this.colors.yellow('Description:')) + this.logger.log(description) + } + + /** + * Logs command usage + */ + protected renderUsage(command: CommandMetaData) { + const aliases = this.kernel.getCommandAliases(command.commandName) + const formatter = new CommandFormatter(command, this.colors) + const usage = formatter.formatUsage(aliases, this.kernel.info.get('binary')).join('\n') + + this.logger.log('') + this.logger.log(this.colors.yellow('Usage:')) + this.logger.log(usage) + } + + /** + * Logs commands arguments and options tables + */ + protected renderList(command: CommandMetaData) { + const tables = this.#makeArgumentsTable('Arguments:', command).concat( + this.#makeOptionsTable('Options:', command) + ) + + new ListFormatter(tables).format().forEach((table) => { + this.logger.log('') + this.logger.log(table.heading) + this.logger.log(table.rows.join('\n')) + }) + } + + /** + * Logs command help text + */ + protected renderHelp(command: CommandMetaData) { + const formatter = new CommandFormatter(command, this.colors) + const help = formatter.formatHelp(this.kernel.info.get('binary')) + if (!help) { + return + } + + this.logger.log('') + this.logger.log(this.colors.yellow('Help:')) + this.logger.log(help) + } + + /** + * Executed by ace directly + */ + async run() { + const isValidCommand = this.#validateCommandName() + if (!isValidCommand) { + this.exitCode = 1 + return + } + + const command = this.kernel.getCommand(this.commandName)! + this.renderDescription(command) + this.renderUsage(command) + this.renderList(command) + this.renderHelp(command) + } +} diff --git a/src/commands/list.ts b/src/commands/list.ts new file mode 100644 index 0000000..794010c --- /dev/null +++ b/src/commands/list.ts @@ -0,0 +1,163 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { BaseCommand } from './base.js' +import { args } from '../decorators/args.js' +import { FlagFormatter } from '../formatters/flag.js' +import { ListFormatter } from '../formatters/list.js' +import { renderErrorWithSuggestions } from '../helpers.js' +import { CommandFormatter } from '../formatters/command.js' +import type { CommandMetaData, Flag, ListTable } from '../types.js' + +/** + * The list command is used to view a list of commands + */ +export class ListCommand extends BaseCommand { + /** + * Command metadata + */ + static commandName: string = 'list' + static description: string = 'View list of available commands' + static help = [ + 'The list command displays a list of all the commands:', + ' {{ binaryName }}list', + '', + 'You can also display the commands for a specific namespace:', + ' {{ binaryName }}list ', + ] + + /** + * Optional flag to filter list by namespace + */ + @args.spread({ + description: 'Filter list by namespace', + required: false, + }) + namespaces?: string[] + + /** + * Returns a table for an array of commands. + */ + #makeCommandsTable(heading: string, commands: CommandMetaData[]): ListTable { + return { + heading: this.colors.yellow(heading), + columns: commands.map((command) => { + const aliases = this.kernel.getCommandAliases(command.commandName) + const commandFormatter = new CommandFormatter(command, this.colors) + + return { + option: commandFormatter.formatListName(aliases), + description: commandFormatter.formatListDescription(), + } + }), + } + } + + /** + * Returns a table for an array of global options + */ + #makeOptionsTable(heading: string, flagsList: Flag[]): ListTable { + return { + heading: this.colors.yellow(heading), + columns: flagsList.map((flag) => { + const flagFormatter = new FlagFormatter(flag, this.colors) + + return { + option: flagFormatter.formatOption(), + description: flagFormatter.formatDescription(), + } + }), + } + } + + /** + * Returns an array of tables for all the commands or for mentioned + * namespaces only + */ + #getCommandsTables(namespaces?: string[]) { + if (namespaces && namespaces.length) { + return namespaces.map((namespace) => { + return this.#makeCommandsTable(namespace, this.kernel.getNamespaceCommands(namespace)) + }) + } + + return [ + this.#makeCommandsTable('Available commands:', this.kernel.getNamespaceCommands()), + ...this.kernel + .getNamespaces() + .map((namespace) => + this.#makeCommandsTable(namespace, this.kernel.getNamespaceCommands(namespace)) + ), + ] + } + + /** + * Returns table for the global flags + */ + #getOptionsTable() { + if (!this.kernel.flags.length) { + return [] + } + + return [this.#makeOptionsTable('Options:', this.kernel.flags)] + } + + /** + * Validates the namespaces mentioned via the "namespaces" + * flag + */ + #validateNamespace(): boolean { + if (!this.namespaces) { + return true + } + + const namespaces = this.kernel.getNamespaces() + const unknownNamespace = this.namespaces.find((namespace) => !namespaces.includes(namespace)) + + /** + * Show error when the namespace is not known + */ + if (unknownNamespace) { + renderErrorWithSuggestions( + this.ui, + `Namespace "${unknownNamespace}" is not defined`, + this.kernel.getNamespaceSuggestions(unknownNamespace) + ) + return false + } + + return true + } + + /** + * The method is used to render a list of options and commands + */ + protected renderList() { + const tables = this.#getOptionsTable().concat(this.#getCommandsTables(this.namespaces)) + + new ListFormatter(tables).format().forEach((table) => { + this.logger.log('') + this.logger.log(table.heading) + this.logger.log(table.rows.join('\n')) + }) + } + + /** + * Executed by ace directly + */ + async run() { + const hasValidNamespaces = this.#validateNamespace() + if (!hasValidNamespaces) { + this.exitCode = 1 + return + } + + this.renderList() + } +} diff --git a/src/errors.ts b/src/errors.ts new file mode 100644 index 0000000..eaff070 --- /dev/null +++ b/src/errors.ts @@ -0,0 +1,109 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { createError, Exception } from '@poppinss/utils' + +/** + * Cannot define required argument after an optional argument + */ +export const E_CANNOT_DEFINE_REQUIRED_ARG = createError<[arg: string, optionalArg: string]>( + 'Cannot define required argument "%s" after optional argument "%s"', + 'E_CANNOT_DEFINE_REQUIRED_ARG' +) + +/** + * Cannot define another argument after a spread argument + */ +export const E_CANNOT_DEFINE_ARG = createError<[arg: string, spreadArg: string]>( + 'Cannot define argument "%s" after spread argument "%s". Spread argument should be the last one', + 'E_CANNOT_DEFINE_ARG' +) + +/** + * Cannot define a flag because it is missing the flag type + */ +export const E_MISSING_FLAG_TYPE = createError<[flag: string]>( + 'Cannot define flag "%s". Specify the flag type', + 'E_MISSING_FLAG_TYPE' +) + +/** + * Command is missing the static property command name + */ +export const E_MISSING_COMMAND_NAME = createError<[command: string]>( + 'Cannot serialize command "%s". Missing static property "commandName"', + 'E_MISSING_COMMAND_NAME' +) + +/** + * Cannot define an argument because it is missing the arg type + */ +export const E_MISSING_ARG_TYPE = createError<[arg: string]>( + 'Cannot define argument "%s". Specify the argument type', + 'E_MISSING_ARG_TYPE' +) + +/** + * Cannot find a command for the given name + */ +export const E_COMMAND_NOT_FOUND = class CommandNotFound extends Exception { + commandName: string + constructor(args: [command: string]) { + super(`Command "${args[0]}" is not defined`, { code: 'E_COMMAND_NOT_FOUND' }) + this.commandName = args[0] + } +} + +/** + * Missing a required flag when running the command + */ +export const E_MISSING_FLAG = createError<[flag: string]>( + 'Missing required option "%s"', + 'E_MISSING_FLAG' +) + +/** + * Missing value for a flag that accepts values + */ +export const E_MISSING_FLAG_VALUE = createError<[flag: string]>( + 'Missing value for option "%s"', + 'E_MISSING_FLAG_VALUE' +) + +/** + * Missing a required argument when running the command + */ +export const E_MISSING_ARG = createError<[arg: string]>( + 'Missing required argument "%s"', + 'E_MISSING_ARG' +) + +/** + * Missing value for an argument + */ +export const E_MISSING_ARG_VALUE = createError<[arg: string]>( + 'Missing value for argument "%s"', + 'E_MISSING_ARG_VALUE' +) + +/** + * An unknown flag was mentioned + */ +export const E_UNKNOWN_FLAG = createError<[flag: string]>( + 'Unknown flag "%s". The mentioned flag is not accepted by the command', + 'E_UNKNOWN_FLAG' +) + +/** + * Invalid value provided for the flag + */ +export const E_INVALID_FLAG = createError<[flag: string, expectedDataType: string]>( + 'Invalid value. The "%s" flag accepts a "%s" value', + 'E_INVALID_FLAG' +) diff --git a/src/formatters/argument.ts b/src/formatters/argument.ts new file mode 100644 index 0000000..c878064 --- /dev/null +++ b/src/formatters/argument.ts @@ -0,0 +1,73 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Argument, UIPrimitives } from '../types.js' + +/** + * The argument formatter formats an argument as per the http://docopt.org/ specification. + */ +export class ArgumentFormatter { + #argument: Argument + #colors: UIPrimitives['colors'] + + constructor(argument: Argument, colors: UIPrimitives['colors']) { + this.#argument = argument + this.#colors = colors + } + + /** + * Wraps the optional placeholder on option arguments + */ + #formatArgument(argument: Argument, valuePlaceholder: string) { + return argument.required ? `${valuePlaceholder}` : `[${valuePlaceholder}]` + } + + /** + * Returns formatted description for the argument + */ + formatDescription(): string { + const defaultValue = this.#argument.default ? `[default: ${this.#argument.default}]` : '' + const separator = defaultValue && this.#argument.description ? ' ' : '' + return this.#colors.dim(`${this.#argument.description || ''}${separator}${defaultValue}`) + } + + /** + * Returns a formatted version of the argument name to be displayed + * inside a list + */ + formatListOption(): string { + switch (this.#argument.type) { + case 'spread': + return ` ${this.#colors.green( + this.#formatArgument(this.#argument, `${this.#argument.argumentName}...`) + )} ` + case 'string': + return ` ${this.#colors.green( + this.#formatArgument(this.#argument, `${this.#argument.argumentName}`) + )} ` + } + } + + /** + * Returns a formatted version of the argument name to + * be displayed next to usage + */ + formatOption(): string { + switch (this.#argument.type) { + case 'spread': + return this.#colors.dim( + `${this.#formatArgument(this.#argument, `<${this.#argument.argumentName}...>`)}` + ) + case 'string': + return this.#colors.dim( + `${this.#formatArgument(this.#argument, `<${this.#argument.argumentName}>`)}` + ) + } + } +} diff --git a/src/formatters/command.ts b/src/formatters/command.ts new file mode 100644 index 0000000..a2caeac --- /dev/null +++ b/src/formatters/command.ts @@ -0,0 +1,111 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import string from '@poppinss/utils/string' +import { TERMINAL_SIZE, wrap } from '@poppinss/cliui/helpers' + +import { ArgumentFormatter } from './argument.js' +import type { AllowedInfoValues, CommandMetaData, UIPrimitives } from '../types.js' + +/** + * The command formatter exposes API to format command data for the + * commands list and the command help. + */ +export class CommandFormatter { + #command: CommandMetaData + #colors: UIPrimitives['colors'] + + constructor(command: CommandMetaData, colors: UIPrimitives['colors']) { + this.#command = command + this.#colors = colors + } + + /** + * Returns the formatted command name to be displayed in the list + * of commands + */ + formatListName(aliases: string[]) { + const formattedAliases = aliases.length ? ` ${this.#colors.dim(`(${aliases.join(', ')})`)}` : '' + return ` ${this.#colors.green(this.#command.commandName)}${formattedAliases} ` + } + + /** + * Returns the formatted description of the command + */ + formatDescription() { + return this.#command.description || '' + } + + /** + * Returns multiline command help + */ + formatHelp(binaryName?: AllowedInfoValues, terminalWidth: number = TERMINAL_SIZE): string { + const binary = binaryName ? `${binaryName} ` : '' + if (!this.#command.help) { + return '' + } + + /** + * Normalize help text to be an array of rows + */ + const help = Array.isArray(this.#command.help) ? this.#command.help : [this.#command.help] + + /** + * Wrap text when goes over the terminal size + */ + return wrap( + help.map((line) => string.interpolate(line, { binaryName: binary })), + { + startColumn: 2, + trimStart: false, + endColumn: terminalWidth, + } + ).join('\n') + } + + /** + * Returns the formatted description to be displayed in the list + * of commands + */ + formatListDescription() { + if (!this.#command.description) { + return '' + } + return this.#colors.dim(this.#command.description) + } + + /** + * Returns an array of strings, each line contains an individual usage + */ + formatUsage(aliases: string[], binaryName?: AllowedInfoValues): string[] { + const binary = binaryName ? `${binaryName} ` : '' + + /** + * Display options placeholder for flags + */ + const flags = this.#command.flags.length ? this.#colors.dim('[options]') : '' + + /** + * Display a list of named args + */ + const args = this.#command.args + .map((arg) => new ArgumentFormatter(arg, this.#colors).formatOption()) + .join(' ') + + /** + * Separator between options placeholder and args + */ + const separator = flags && args ? ` ${this.#colors.dim('[--]')} ` : '' + + const mainUsage = [` ${binary}${this.#command.commandName} ${flags}${separator}${args}`] + return mainUsage.concat( + aliases.map((alias) => ` ${binary}${alias} ${flags}${separator}${args}`) + ) + } +} diff --git a/src/formatters/flag.ts b/src/formatters/flag.ts new file mode 100644 index 0000000..b0a3905 --- /dev/null +++ b/src/formatters/flag.ts @@ -0,0 +1,133 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { ArrayFlag, BooleanFlag, Flag, NumberFlag, StringFlag, UIPrimitives } from '../types.js' + +/** + * The flag formatter formats a flag as per the http://docopt.org/ specification. + */ +export class FlagFormatter { + #flag: Flag + #colors: UIPrimitives['colors'] + + constructor(flag: Flag, colors: UIPrimitives['colors']) { + this.#flag = flag + this.#colors = colors + } + + /** + * Formats the value flag + */ + #formatValueFlag(flag: Flag, valuePlaceholder: string) { + return flag.required ? `=${valuePlaceholder}` : `[=${valuePlaceholder}]` + } + + /** + * Formats the aliases for the flag + */ + #formatAliases(flag: Flag): string[] { + if (!flag.alias) { + return [] + } + + if (typeof flag.alias === 'string') { + return [`-${flag.alias}`] + } + + return flag.alias.map((alias) => `-${alias}`) + } + + /** + * Formats the array flag by appending ellipsis `...` and wrapping + * the value to indicate if it is required or not + */ + #formatArrayFlag(flag: ArrayFlag) { + const value = this.#formatValueFlag(flag, `${flag.flagName.toUpperCase()}...`) + const aliases = this.#formatAliases(flag) + const flagWithValue = `--${flag.flagName}${value}` + + if (aliases.length) { + return ` ${this.#colors.green(`${aliases.join(',')}, ${flagWithValue}`)} ` + } + + return ` ${this.#colors.green(flagWithValue)} ` + } + + /** + * Formats the string flag by wrapping the value to indicate + * if it is required or not + */ + #formatStringFlag(flag: StringFlag) { + const value = this.#formatValueFlag(flag, `${flag.flagName.toUpperCase()}`) + const aliases = this.#formatAliases(flag) + const flagWithValue = `--${flag.flagName}${value}` + + if (aliases.length) { + return ` ${this.#colors.green(`${aliases.join(',')}, ${flagWithValue}`)} ` + } + + return ` ${this.#colors.green(flagWithValue)} ` + } + + /** + * Formats the numeric flag by wrapping the value to indicate + * if it is required or not + */ + #formatNumericFlag(flag: NumberFlag) { + const value = this.#formatValueFlag(flag, `${flag.flagName.toUpperCase()}`) + const aliases = this.#formatAliases(flag) + const flagWithValue = `--${flag.flagName}${value}` + + if (aliases.length) { + return ` ${this.#colors.green(`${aliases.join(',')}, ${flagWithValue}`)} ` + } + + return ` ${this.#colors.green(flagWithValue)} ` + } + + /** + * Formats the boolean flag. Boolean flags needs no wrapping + */ + #formatBooleanFlag(flag: BooleanFlag) { + const aliases = this.#formatAliases(flag) + const negatedVariant = flag.showNegatedVariantInHelp ? `|--no-${flag.flagName}` : '' + const flagWithVariant = `--${flag.flagName}${negatedVariant}` + + if (aliases.length) { + return ` ${this.#colors.green(`${aliases.join(',')}, ${flagWithVariant}`)} ` + } + + return ` ${this.#colors.green(flagWithVariant)} ` + } + + /** + * Returns formatted description for the flag + */ + formatDescription(): string { + const defaultValue = this.#flag.default !== undefined ? `[default: ${this.#flag.default}]` : '' + const separator = defaultValue && this.#flag.description ? ' ' : '' + return this.#colors.dim(`${this.#flag.description || ''}${separator}${defaultValue}`) + } + + /** + * Returns a formatted version of the flag name and aliases + */ + formatOption(): string { + switch (this.#flag.type) { + case 'array': + return this.#formatArrayFlag(this.#flag) + case 'string': + return this.#formatStringFlag(this.#flag) + case 'number': + return this.#formatNumericFlag(this.#flag) + case 'boolean': + return this.#formatBooleanFlag(this.#flag) + } + } +} diff --git a/src/formatters/info.ts b/src/formatters/info.ts new file mode 100644 index 0000000..d7a739f --- /dev/null +++ b/src/formatters/info.ts @@ -0,0 +1,67 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import stringWidth from 'string-width' +import { justify, TERMINAL_SIZE } from '../cli_helpers.js' +import type { AllowedInfoValues, UIPrimitives } from '../types.js' + +/** + * Info formatter is used to format the kernel info key-value pair + * into a table. + */ +export default class InfoFormatter { + #info: Map + #colors: UIPrimitives['colors'] + + constructor(info: Map, colors: UIPrimitives['colors']) { + this.#info = info + this.#colors = colors + } + + /** + * Formats the info map into an array of columns. + */ + #createFormattedColumns() { + return [...this.#info.keys()].map((key) => { + const value = this.#info.get(key) + const formattedValue = Array.isArray(value) + ? value.map((item) => String(item)).join(',') + : String(value) + + return { + key: `${key} `, + value: this.#colors.green(` ${formattedValue}`), + } + }) + } + + /** + * Formats the info map into an array of rows + */ + format(): string[] { + const columns = this.#createFormattedColumns() + const largestOptionColumnWidth = Math.max(...columns.map((column) => stringWidth(column.key))) + + const keys = justify( + columns.map(({ key }) => key), + { maxWidth: largestOptionColumnWidth, paddingChar: this.#colors.dim('─') } + ) + + const values = justify( + columns.map(({ value }) => value), + { + maxWidth: TERMINAL_SIZE - largestOptionColumnWidth, + paddingChar: this.#colors.dim('─'), + align: 'right', + } + ) + + return columns.map((_, index) => `${keys[index]}${values[index]}`) + } +} diff --git a/src/formatters/list.ts b/src/formatters/list.ts new file mode 100644 index 0000000..c1ccdbc --- /dev/null +++ b/src/formatters/list.ts @@ -0,0 +1,64 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import stringWidth from 'string-width' +import { justify, TERMINAL_SIZE, wrap } from '@poppinss/cliui/helpers' + +import type { ListTable } from '../types.js' + +/** + * The list formatter formats the list of commands and flags. The option column + * is justified to have same width accross all the rows. + */ +export class ListFormatter { + #tables: ListTable[] + #largestOptionColumnWidth: number + + constructor(tables: ListTable[]) { + this.#tables = tables + this.#largestOptionColumnWidth = Math.max( + ...this.#tables + .map((table) => table.columns.map((column) => stringWidth(column.option))) + .flat() + ) + } + + /** + * Formats the table to an array of plain text rows. + */ + #formatTable(table: ListTable, terminalWidth: number): string[] { + const options = justify( + table.columns.map(({ option }) => option), + { maxWidth: this.#largestOptionColumnWidth } + ) + + const descriptions = wrap( + table.columns.map(({ description }) => description), + { + startColumn: this.#largestOptionColumnWidth, + endColumn: terminalWidth, + trimStart: true, + } + ) + + return table.columns.map((_, index) => `${options[index]}${descriptions[index]}`) + } + + /** + * Format tables list into an array of rows + */ + format(terminalWidth: number = TERMINAL_SIZE) { + return this.#tables.map((table) => { + return { + heading: table.heading, + rows: this.#formatTable(table, terminalWidth), + } + }) + } +} diff --git a/src/helpers.ts b/src/helpers.ts new file mode 100644 index 0000000..1f229bf --- /dev/null +++ b/src/helpers.ts @@ -0,0 +1,47 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import type { UIPrimitives } from './types.js' + +/** + * Helper to sort array of strings alphabetically. + */ +export function sortAlphabetically(prev: string, curr: string) { + if (curr > prev) { + return -1 + } + + if (curr < prev) { + return 1 + } + + return 0 +} + +/** + * Renders an error message and lists suggestions. + */ +export function renderErrorWithSuggestions( + ui: UIPrimitives, + message: string, + suggestions: string[] +) { + const instructions = ui + .sticker() + .fullScreen() + .drawBorder((borderChar, colors) => colors.red(borderChar)) + + instructions.add(ui.colors.red(message)) + if (suggestions.length) { + instructions.add('') + instructions.add(`${ui.colors.dim('Did you mean?')} ${suggestions.slice(0, 4).join(', ')}`) + } + + instructions.getRenderer().logError(instructions.prepare()) +} diff --git a/src/kernel.ts b/src/kernel.ts new file mode 100644 index 0000000..b4e28ab --- /dev/null +++ b/src/kernel.ts @@ -0,0 +1,752 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import Hooks from '@poppinss/hooks' +import { cliui } from '@poppinss/cliui' +import { findBestMatch } from 'string-similarity' +import { RuntimeException } from '@poppinss/utils' + +import { Parser } from './parser.js' +import * as errors from './errors.js' +import { ListCommand } from './commands/list.js' +import { BaseCommand } from './commands/base.js' +import { CommandsList } from './loaders/list.js' +import { sortAlphabetically, renderErrorWithSuggestions } from './helpers.js' + +import type { + Flag, + UIPrimitives, + FlagListener, + FoundHookArgs, + CommandMetaData, + LoadersContract, + FindingHookArgs, + ExecutedHookArgs, + ExecutorContract, + FoundHookHandler, + AllowedInfoValues, + ExecutingHookArgs, + FindingHookHandler, + TerminatingHookArgs, + ExecutedHookHandler, + ExecutingHookHandler, + TerminatingHookHandler, +} from './types.js' + +const knowErrorCodes = Object.keys(errors) + +/** + * The Ace kernel manages the registration and execution of commands. + * + * The kernel is the main entry point of a console application, and + * is tailored for a standard CLI environment. + */ +export class Kernel { + /** + * Listeners for CLI options. Executed for main command + * only + */ + #optionListeners: Map = new Map() + + /** + * The global command is used to register global flags applicable + * on all the commands + */ + #globalCommand: typeof BaseCommand = class extends BaseCommand { + static options = { + allowUnknownFlags: true, + } + } + + /** + * The default command to run when no command is mentioned. The default + * command will also run when only flags are mentioned. + */ + #defaultCommand: typeof BaseCommand = ListCommand + + /** + * Available hooks + */ + #hooks: Hooks<{ + finding: FindingHookArgs + found: FoundHookArgs + executing: ExecutingHookArgs + executed: ExecutedHookArgs + terminating: TerminatingHookArgs + }> = new Hooks() + + /** + * Executors are used to instantiate a command and execute + * the run method. + */ + #executor: ExecutorContract = { + create(command, parsedArgs, kernel) { + return new command(kernel, parsedArgs, kernel.ui) + }, + run(command) { + return command.exec() + }, + } + + /** + * Keep a track of the main command. There are some action (like termination) + * that only the main command can perform + */ + #mainCommand?: BaseCommand + + /** + * The current state of kernel. The `running` and `completed` + * states are only set when kernel takes over the process. + */ + #state: 'idle' | 'booted' | 'running' | 'terminated' = 'idle' + + /** + * Collection of loaders to use for loading commands + */ + #loaders: LoadersContract[] = [] + + /** + * An array of registered namespaces. Sorted alphabetically + */ + #namespaces: string[] = [] + + /** + * A collection of aliases for the commands. The key is the alias name + * and the value is the command name. + * + * In case of duplicate aliases, the most recent alias will override + * the previous existing alias. + */ + #aliases: Map = new Map() + + /** + * A collection of commands by the command name. This allows us to keep only + * the unique commands and also keep the loader reference to know which + * loader to ask for loading the command. + */ + #commands: Map = new Map() + + /** + * The exit code for the kernel. The exit code is inferred + * from the main code when not set explicitly. + */ + exitCode?: number + + /** + * The UI primitives to use within commands + */ + ui: UIPrimitives = cliui() + + /** + * CLI info map + */ + info: Map = new Map() + + /** + * List of global flags + */ + get flags(): ({ name: string } & Flag)[] { + return this.#globalCommand.flags + } + + /** + * Creates an instance of a command by parsing and validating + * the command line arguments. + */ + async #create( + Command: T, + argv: string[] + ): Promise> { + /** + * Parse CLI argv without global flags. When running commands directly, we + * should not be using global flags anyways + */ + const parsed = new Parser(Command.getParserOptions()).parse(argv) + + /** + * Validate the parsed output + */ + Command.validate(parsed) + + /** + * Construct command instance using the executor + */ + const commandInstance = await this.#executor.create(Command, parsed, this) + return commandInstance as InstanceType + } + + /** + * Executes a given command. The main commands are executed using the + * "execMain" method. + */ + async #exec( + commandName: string, + argv: string[] + ): Promise> { + const Command = await this.find(commandName) + const commandInstance = await this.#create(Command, argv) + + /** + * Execute the command using the executor + */ + await this.#hooks.runner('executing').run(commandInstance, false) + await this.#executor.run(commandInstance, this) + await this.#hooks.runner('executed').run(commandInstance, false) + + return commandInstance + } + + /** + * Executes the main command and handles the exceptions by + * reporting them + */ + async #execMain(commandName: string, argv: string[]) { + try { + const Command = await this.find(commandName) + + /** + * Parse CLI argv and also merge global flags parser options. + */ + const parsed = new Parser( + Command.getParserOptions(this.#globalCommand.getParserOptions().flagsParserOptions) + ).parse(argv) + + /** + * Validate the flags against the global list as well + */ + this.#globalCommand.validate(parsed) + + /** + * Validate the parsed output + */ + Command.validate(parsed) + + /** + * Run options listeners. Option listeners can terminate + * the process early + */ + let shortcircuit = false + for (let [option, listener] of this.#optionListeners) { + if (parsed.flags[option] !== undefined) { + shortcircuit = await listener(Command, this, parsed) + if (shortcircuit) { + break + } + } + } + + /** + * Terminate if a flag listener ends the process + */ + if (shortcircuit) { + await this.terminate() + return + } + + /** + * Keep a note of the main command + */ + this.#mainCommand = await this.#executor.create(Command, parsed, this) + + /** + * Execute the command either using the executor + */ + await this.#hooks.runner('executing').run(this.#mainCommand, true) + await this.#executor.run(this.#mainCommand, this) + await this.#hooks.runner('executed').run(this.#mainCommand, true) + + /** + * Terminate the process unless command wants to stay alive + */ + if (!Command.options.staysAlive) { + await this.terminate(this.#mainCommand) + } + } catch (error) { + await this.#handleError(error) + } + } + + /** + * Handles the error raised during the main command execution. + * + * @note: Do not use this error handler for anything other than + * the handle method + */ + async #handleError(error: any) { + /** + * Exit code will always be 1 if a hard exception was raised + * during the command executed. + */ + this.exitCode = 1 + + /** + * Reporting errors with the best UI possible based upon the error + * type + */ + if (error instanceof errors.E_COMMAND_NOT_FOUND) { + renderErrorWithSuggestions( + this.ui, + error.message, + this.getCommandSuggestions(error.commandName) + ) + } else if (knowErrorCodes.includes(error.code)) { + this.ui.logger.logError(`${this.ui.colors.bgRed().white(' ERROR ')} ${error.message}`) + } else { + console.log(error.stack) + } + + /** + * Start termination + */ + await this.terminate(this.#mainCommand) + } + + /** + * Listen for CLI options and execute an action. Only one listener + * can be defined per aption. + * + * The callbacks are only executed for the main command + */ + on(option: string, callback: FlagListener): this { + this.#optionListeners.set(option, callback) + return this + } + + /** + * Define a global flag that is applicable for all the + * commands. + */ + defineFlag( + name: string, + options: Partial & { type: 'string' | 'boolean' | 'array' | 'number' } + ) { + if (this.#state !== 'idle') { + throw new RuntimeException(`Cannot register global flag in "${this.#state}" state`) + } + + this.#globalCommand.defineFlag(name, options) + } + + /** + * Register a custom default command. Default command runs + * when no command is mentioned + */ + registerDefaultCommand(command: typeof BaseCommand): this { + if (this.#state !== 'idle') { + throw new RuntimeException(`Cannot register default command in "${this.#state}" state`) + } + + this.#defaultCommand = command + return this + } + + /** + * Register a custom executor to execute the command + */ + registerExecutor(executor: ExecutorContract): this { + if (this.#state !== 'idle') { + throw new RuntimeException(`Cannot register commands executor in "${this.#state}" state`) + } + + this.#executor = executor + return this + } + + /** + * Register a commands loader. The commands will be collected by + * all the loaders. + * + * Incase multiple loaders returns a single command, the command from the + * most recent loader will be used. + */ + addLoader(loader: LoadersContract): this { + if (this.#state !== 'idle') { + throw new RuntimeException(`Cannot add loader in "${this.#state}" state`) + } + + this.#loaders.push(loader) + return this + } + + /** + * Register alias for a comamnd name. + */ + addAlias(alias: string, commandName: string): this { + this.#aliases.set(alias, commandName) + return this + } + + /** + * Get the current state of the kernel. + */ + getState() { + return this.#state + } + + /** + * Returns a flat list of commands metadata registered with the kernel. + * The list is sorted alphabetically by the command name. + */ + getCommands(): CommandMetaData[] { + return [...this.#commands.keys()] + .sort(sortAlphabetically) + .map((name) => this.#commands.get(name)!.metaData) + } + + /** + * Get a list of commands for a specific namespace. All non-namespaces + * commands will be returned if no namespace is defined. + */ + getNamespaceCommands(namespace?: string) { + let commandNames = [...this.#commands.keys()] + + /** + * Filter a list of commands by the namespace + */ + if (namespace) { + commandNames = commandNames.filter( + (name) => this.#commands.get(name)!.metaData.namespace === namespace + ) + } else { + commandNames = commandNames.filter((name) => !this.#commands.get(name)!.metaData.namespace) + } + + return commandNames.sort(sortAlphabetically).map((name) => this.#commands.get(name)!.metaData) + } + + /** + * Returns the command metadata by its name. Returns null when the + * command is missing. + */ + getCommand(commandName: string): CommandMetaData | null { + return this.#commands.get(commandName)?.metaData || null + } + + /** + * Returns a reference for the default command. The return value + * is the default command constructor + */ + getDefaultCommand() { + return this.#defaultCommand + } + + /** + * Returns an array of aliases registered. + * + * - Call `getCommandAliases` method to get aliases for a given command + * - Call `getAliasCommand` to get the command or a given alias + */ + getAliases() { + return [...this.#aliases.keys()] + } + + /** + * Returns the command metata for a given alias. Returns null + * if alias is not recognized. + */ + getAliasCommand(alias: string): CommandMetaData | null { + const aliasCommand = this.#aliases.get(alias) + if (!aliasCommand) { + return null + } + + return this.#commands.get(aliasCommand)?.metaData || null + } + + /** + * Returns an array of aliases for a given command + */ + getCommandAliases(commandName: string) { + return [...this.#aliases.entries()] + .filter(([, command]) => { + return command === commandName + }) + .map(([alias]) => alias) + } + + /** + * Returns a list of namespaces. The list is sorted alphabetically + * by the namespace name + */ + getNamespaces(): string[] { + return this.#namespaces + } + + /** + * Returns an array of command and aliases name suggestions for + * a given keyword. + */ + getCommandSuggestions(keyword: string): string[] { + /** + * Priortize namespace commands when the keyword matches the + * namespace + */ + if (this.#namespaces.includes(keyword)) { + return this.getNamespaceCommands(keyword).map((command) => command.commandName) + } + + const commandsAndAliases = [...this.#commands.keys()].concat([...this.#aliases.keys()]) + + return findBestMatch(keyword, commandsAndAliases) + .ratings.sort((current, next) => next.rating - current.rating) + .filter((rating) => rating.rating > 0.4) + .map((rating) => rating.target) + } + + /** + * Returns an array of namespaces suggestions for a given keyword. + */ + getNamespaceSuggestions(keyword: string): string[] { + return findBestMatch(keyword, this.#namespaces) + .ratings.sort((current, next) => next.rating - current.rating) + .filter((rating) => rating.rating > 0.4) + .map((rating) => rating.target) + } + + /** + * Listen for the event before we begin the process of finding + * the command. + */ + finding(callback: FindingHookHandler) { + this.#hooks.add('finding', callback) + return this + } + + /** + * Listen for the event when a command is found + */ + found(callback: FoundHookHandler) { + this.#hooks.add('found', callback) + return this + } + + /** + * Listen for the event before we start to execute the command. + */ + executing(callback: ExecutingHookHandler) { + this.#hooks.add('executing', callback) + return this + } + + /** + * Listen for the event after the command has been executed + */ + executed(callback: ExecutedHookHandler) { + this.#hooks.add('executed', callback) + return this + } + + /** + * Listen for the event before we start to terminate the kernel + */ + terminating(callback: TerminatingHookHandler) { + this.#hooks.add('terminating', callback) + return this + } + + /** + * Loads commands from all the registered loaders. The "addLoader" method + * must be called before calling the "load" method. + */ + async boot() { + if (this.#state !== 'idle') { + return + } + + /** + * Boot global command is not already booted + */ + this.#globalCommand.boot() + + /** + * Registering the default command + */ + this.addLoader(new CommandsList([this.#defaultCommand])) + + /** + * Set state to booted + */ + this.#state = 'booted' + + /** + * A set of unique namespaces. Later, we will store them on kernel + * directly as an array sorted alphabetically. + */ + const namespaces: Set = new Set() + + /** + * Load metadata for all commands using the loaders + */ + for (let loader of this.#loaders) { + const commands = await loader.getMetaData() + + commands.forEach((command) => { + this.#commands.set(command.commandName, { metaData: command, loader }) + command.aliases.forEach((alias) => this.addAlias(alias, command.commandName)) + command.namespace && namespaces.add(command.namespace) + }) + } + + this.#namespaces = [...namespaces].sort(sortAlphabetically) + } + + /** + * Find a command by its name + */ + async find(commandName: string): Promise { + /** + * Get command name from the alias (if one exists) + */ + commandName = this.#aliases.get(commandName) || commandName + await this.#hooks.runner('finding').run(commandName) + + /** + * Find if we have a command registered + */ + const command = this.#commands.get(commandName) + if (!command) { + throw new errors.E_COMMAND_NOT_FOUND([commandName]) + } + + /** + * Find if the loader is able to load the command + */ + const commandConstructor = await command.loader.getCommand(command.metaData) + if (!commandConstructor) { + throw new errors.E_COMMAND_NOT_FOUND([commandName]) + } + + await this.#hooks.runner('found').run(commandConstructor) + return commandConstructor as T + } + + /** + * Execute a command. The second argument is an array of commandline + * arguments (without the command name) + */ + async exec(commandName: string, argv: string[]) { + /** + * Boot if not already booted + */ + if (this.#state === 'idle') { + await this.boot() + } + + /** + * Disallow calling commands if main commands was executed once and + * terminated + */ + if (this.#state === 'terminated') { + throw new RuntimeException( + 'The kernel has been terminated. Create a fresh instance to execute commands' + ) + } + + return this.#exec(commandName, argv) + } + + /** + * Creates a command instance by parsing and validating + * the command-line arguments. + */ + async create(command: T, argv: string[]): Promise> { + /** + * Boot if not already booted + */ + if (this.#state === 'idle') { + await this.boot() + } + + return this.#create(command, argv) + } + + /** + * Handle process argv and execute the command. Calling this method + * makes kernel own the process and register SIGNAL listeners + */ + async handle(argv: string[]) { + /** + * Cannot run multiple main commands from a single process + */ + if (this.#state === 'running') { + throw new RuntimeException('Cannot run multiple main commands from a single process') + } + + /** + * Cannot run main command once the kernel has already been terminated + */ + if (this.#state === 'terminated') { + throw new RuntimeException( + 'The kernel has been terminated. Create a fresh instance to execute commands' + ) + } + + /** + * Boot kernel + */ + if (this.#state === 'idle') { + await this.boot() + } + + this.#state = 'running' + + /** + * Run the default command when no argv are defined + * or if only flags are mentioned + */ + if (!argv.length || argv[0].startsWith('-')) { + return this.#execMain(this.#defaultCommand.commandName, argv) + } + + /** + * Run the mentioned command as the main command + */ + const [commandName, ...args] = argv + return this.#execMain(commandName, args) + } + + /** + * Trigger process termination. The terminate method needs the command + * instance to know if the main command is triggering the termination + * or not. + * + * Only main commands can trigger the termination. + */ + async terminate(command?: BaseCommand) { + /** + * Do not terminate when the state is not running. The state + * is always running when we execute the handle method + */ + if (this.#state !== 'running') { + return + } + + /** + * If we know about the command and the command trying + * to exit is not same as the main command, then + * do not terminate + */ + if (this.#mainCommand && command !== this.#mainCommand) { + return + } + + /** + * Started the termination process + */ + await this.#hooks.runner('terminating').run(this.#mainCommand) + this.#state = 'terminated' + + /** + * Set exit code if not already set. Also try to infer + * from the main command if exists + */ + this.exitCode = this.exitCode ?? this.#mainCommand?.exitCode ?? 0 + process.exitCode = this.exitCode + } +} diff --git a/src/loaders/list.ts b/src/loaders/list.ts new file mode 100644 index 0000000..809a7e0 --- /dev/null +++ b/src/loaders/list.ts @@ -0,0 +1,38 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { BaseCommand } from '../commands/base.js' +import type { CommandMetaData, LoadersContract } from '../types.js' + +/** + * The CommandsList loader registers commands classes with the kernel. + * The commands are kept within memory + */ +export class CommandsList implements LoadersContract { + #commands: typeof BaseCommand[] + + constructor(commands: typeof BaseCommand[]) { + this.#commands = commands + } + + /** + * Returns an array of command's metadata + */ + async getMetaData(): Promise { + return this.#commands.map((command) => command.serialize()) + } + + /** + * Returns the command class constructor for a given command. Null + * is returned when unable to lookup the command + */ + async getCommand(metaData: CommandMetaData): Promise { + return this.#commands.find((command) => command.commandName === metaData.commandName) || null + } +} diff --git a/src/parser.ts b/src/parser.ts new file mode 100644 index 0000000..03e7d54 --- /dev/null +++ b/src/parser.ts @@ -0,0 +1,141 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import yargsParser from 'yargs-parser' +import { yarsConfig } from './yars_config.js' + +import type { + YargsOutput, + ParsedOutput, + FlagsParserOptions, + ArgumentsParserOptions, +} from './types.js' + +/** + * Parses the command line arguments. The flags are parsed + * using yargs-parser + */ +export class Parser { + /** + * Parser options + */ + #options: { + flagsParserOptions: FlagsParserOptions + argumentsParserOptions: ArgumentsParserOptions[] + } + + constructor(options: { + flagsParserOptions: FlagsParserOptions + argumentsParserOptions: ArgumentsParserOptions[] + }) { + this.#options = options + } + + /** + * Parsers flags using yargs + */ + #parseFlags(argv: string | string[]) { + return yargsParser(argv, { ...this.#options.flagsParserOptions, configuration: yarsConfig }) + } + + /** + * Scans for unknown flags in yargs output. + */ + #scanUnknownFlags(parsed: { [key: string]: any }): string[] { + const unknownFlags: string[] = [] + + for (let key of Object.keys(parsed)) { + if (!this.#options.flagsParserOptions.all.includes(key)) { + unknownFlags.push(key) + } + } + + return unknownFlags + } + + /** + * Parsers arguments by mimicking the yargs behavior + */ + #parseArguments(parsedOutput: YargsOutput): ParsedOutput { + let lastParsedIndex = -1 + + const output = this.#options.argumentsParserOptions.map((option, index) => { + if (option.type === 'spread') { + let value: any[] | undefined = parsedOutput._.slice(index) + lastParsedIndex = parsedOutput._.length + + /** + * Step 1 + * + * Use default value when original value is not defined. + */ + if (!value.length) { + value = Array.isArray(option.default) + ? option.default + : option.default === undefined + ? undefined + : [option.default] + } + + /** + * Step 2 + * + * Call parse method when value is not undefined + */ + if (value !== undefined && option.parse) { + value = option.parse(value) + } + + return value + } + + let value = parsedOutput._[index] + lastParsedIndex = index + 1 + + /** + * Step 1: + * + * Use default value when original value is undefined + * Original value set to empty string will be used + * as real value. The behavior is same as yargs + * flags parser `--connection=` + */ + if (value === undefined) { + value = option.default + } + + /** + * Step 2 + * + * Call parse method when value is not undefined + */ + if (value !== undefined && option.parse) { + value = option.parse(value) + } + + return value + }) + + const { '_': args, '--': o, ...rest } = parsedOutput + + return { + args: output, + _: args.slice(lastParsedIndex === -1 ? 0 : lastParsedIndex), + unknownFlags: this.#scanUnknownFlags(rest), + flags: rest, + } + } + + /** + * Parse commandline arguments + */ + parse(argv: string | string[]) { + return this.#parseArguments(this.#parseFlags(argv)) + } +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..ed6fbe0 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,375 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import type { cliui } from '@poppinss/cliui' +import type { Arguments, Options } from 'yargs-parser' +import type { HookHandler } from '@poppinss/hooks/types' + +import type { Kernel } from './kernel.js' +import type { BaseCommand } from './commands/base.js' + +/** + * Parsed output of yargs + */ +export type YargsOutput = Arguments + +/** + * Parsed output from the parser + */ +export type ParsedOutput = YargsOutput & { + /** + * Parsed arguments + */ + args: (any | any[])[] + + /** + * Left over arguments after parsing flags + * and args + */ + _: Array + + /** + * An array of unknown flags that were parsed + */ + unknownFlags: string[] + + /** + * List of parsed flags + */ + flags: { + [argName: string]: any + } +} + +/** + * The UI primitives used by commands + */ +export type UIPrimitives = ReturnType + +/** + * All loaders must adhere to the LoadersContract + */ +export interface LoadersContract { + /** + * The method should return an array of commands metadata + */ + getMetaData(): Promise + + /** + * The method should return the command instance by command + * name + */ + getCommand(command: CommandMetaData): Promise +} + +/** + * Command executor is used to create a new instance of the command + * and run it. + */ +export interface ExecutorContract { + /** + * Create a new instance of the command + */ + create( + command: typeof BaseCommand, + parsedOutput: ParsedOutput, + kernel: Kernel + ): Promise | BaseCommand + + /** + * Run the command + */ + run(command: Command, kernel: Kernel): Promise +} + +/** + * Parser options accepted by the yargs to process + * flags + */ +export type FlagsParserOptions = { + all: string[] + array?: string[] + boolean?: Options['boolean'] + string?: Options['string'] + number?: Options['boolean'] + default?: Options['default'] + coerce?: Options['coerce'] + alias?: Options['alias'] + count?: Options['count'] +} + +/** + * The options accepted by the arguments parser + */ +export type ArgumentsParserOptions = { + type: 'string' | 'spread' + default?: any + parse?: (value: any) => any +} + +/** + * Options for defining an argument + */ +export type BaseArgument = { + name: string + argumentName: string + required?: boolean + description?: string + default?: T +} + +/** + * Type for a string argument + */ +export type StringArgument = BaseArgument & { + type: 'string' + + /** + * Whether or not to allow empty values. When set to false, + * the validation will fail if the argument is provided + * an empty string + * + * Defaults to false + */ + allowEmptyValue?: boolean + parse?: (input: T) => T +} + +/** + * Type for a spread argument + */ +export type SpreadArgument = BaseArgument & { + type: 'spread' + + /** + * Whether or not to allow empty values. When set to false, + * the validation will fail if the argument is provided + * an empty string + * + * Defaults to false + */ + allowEmptyValue?: boolean + parse?: (input: T extends any[] ? T : [T]) => T +} + +/** + * A union of known arguments + */ +export type Argument = StringArgument | SpreadArgument + +/** + * Base properties for a flag + */ +export type BaseFlag = { + name: string + flagName: string + required?: boolean + default?: T + description?: string + alias?: string | string[] +} + +/** + * String flag + */ +export type StringFlag = BaseFlag & { + type: 'string' + + /** + * Whether or not to allow empty values. When set to false, + * the validation will fail if the flag is mentioned but + * no value is provided + * + * Defaults to false + */ + allowEmptyValue?: boolean + parse?: (input: T) => T +} + +/** + * Boolean flag + */ +export type BooleanFlag = BaseFlag & { + type: 'boolean' + + /** + * Whether or not to display the negated variant in the + * help output. + * + * Applicable for boolean flags only + * + * Defaults to false + */ + showNegatedVariantInHelp?: boolean + parse?: (input: T) => T +} + +/** + * Number flag + */ +export type NumberFlag = BaseFlag & { + type: 'number' + parse?: (input: T) => T +} + +/** + * An array of string flag + */ +export type ArrayFlag = BaseFlag & { + type: 'array' + + /** + * Whether or not to allow empty values. When set to false, + * the validation will fail if the flag is mentioned but + * no value is provided + * + * Defaults to false + */ + allowEmptyValue?: boolean + + parse?: (input: T) => T +} + +/** + * A union of known flags + */ +export type Flag = StringFlag | BooleanFlag | NumberFlag | ArrayFlag + +/** + * Command metdata required to display command help. + */ +export type CommandMetaData = { + /** + * Help text for the command + */ + help?: string | string[] + + /** + * The name of the command + */ + commandName: string + + /** + * The command description to show on the help + * screen + */ + description: string + + /** + * Command namespace. The namespace is extracted + * from the command name + */ + namespace: string | null + + /** + * Command aliases. The same command can be run using + * these aliases as well. + */ + aliases: string[] + + /** + * Flags accepted by the command + */ + flags: Omit[] + + /** + * Args accepted by the command + */ + args: Omit[] + + /** + * Command configuration options + */ + options: CommandOptions +} + +/** + * Static set of command options + */ +export type CommandOptions = { + /** + * Whether or not to allow for unknown flags. If set to false, + * the command will not run when unknown flags are provided + * through the CLI + * + * Defaults to false + */ + allowUnknownFlags?: boolean + + /** + * When flag set to true, the kernel will not trigger the termination + * process unless the command explicitly calls the terminate method. + * + * Defaults to false + */ + staysAlive?: boolean + + /** + * When set to true, the kernel will not listen for process signals + * like SIGTERM or SIGINT + * + * Defaults to false + */ + handlesSignals?: boolean +} + +/** + * Finding hook handler and data + */ +export type FindingHookArgs = [[string], [string]] +export type FindingHookHandler = HookHandler + +/** + * Found hook handler and data + */ +export type FoundHookArgs = [[typeof BaseCommand], [typeof BaseCommand]] +export type FoundHookHandler = HookHandler + +/** + * Executing hook handler and data + */ +export type ExecutingHookArgs = [[BaseCommand, boolean], [BaseCommand, boolean]] +export type ExecutingHookHandler = HookHandler + +/** + * Executed hook handler and data + */ +export type ExecutedHookArgs = ExecutingHookArgs +export type ExecutedHookHandler = ExecutingHookHandler + +/** + * Terminating hook handler and data + */ +export type TerminatingHookArgs = [[BaseCommand?], [BaseCommand?]] +export type TerminatingHookHandler = HookHandler + +/** + * A listener that listeners for flags when they are mentioned. + */ +export type FlagListener = ( + command: typeof BaseCommand, + kernel: Kernel, + parsedOutput: ParsedOutput +) => any | Promise + +/** + * Commands and options list table + */ +export type ListTable = { + columns: { + option: string + description: string + }[] + heading: string +} + +/** + * A union of data types allowed for the info key-value pair + */ +export type AllowedInfoValues = number | boolean | string | string[] | number[] | boolean[] diff --git a/src/utils/handleError.ts b/src/utils/handleError.ts deleted file mode 100644 index 2892aca..0000000 --- a/src/utils/handleError.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * @adonisjs/ace - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { logger } from '@poppinss/cliui' - -/** - * Handles the command errors and prints them to the console. - */ -// eslint-disable-next-line no-shadow -export function handleError(error: any, callback?: (error: any) => void | Promise) { - if (typeof callback === 'function') { - callback(error) - } else if (typeof error.handle === 'function') { - error.handle(error) - } else { - logger.fatal(error) - } -} diff --git a/src/utils/help.ts b/src/utils/help.ts deleted file mode 100644 index dc3af87..0000000 --- a/src/utils/help.ts +++ /dev/null @@ -1,337 +0,0 @@ -/* - * @adonisjs/ace - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { logger } from '@poppinss/cliui' -import termSize from 'term-size' -import { sortAndGroupCommands } from './sortAndGroupCommands' -import { Aliases, CommandArg, CommandFlag, SerializedCommand } from '../Contracts' - -/** - * Converts a line to rows at a specific width - */ -function lineToRows(text: string, width: number) { - const rows: string[] = [] - let row: string[] = [] - let wordsCount = 0 - - text.split(' ').forEach((word) => { - if (wordsCount + (word.length + 1) > width) { - /** - * Push the number of whitespace left after the existing current - * and the terminal space. We need to do this, coz we at times - * have whitespace when the upcoming word may break into next - * lines - */ - row.push(new Array(width - wordsCount + 1).join(' ')) - - /** - * Push the existing row to the rows - */ - rows.push(row.join(' ')) - - /** - * Row is empty now - */ - row = [] - - /** - * Row has zero words - */ - wordsCount = 0 - } - - /** - * Increase the words count + 1. The extra one is for the - * whitspace between the words - */ - wordsCount += word.length + 1 - - /** - * Collect word inside the row - */ - row.push(word) - }) - - /** - * Handle the orphan row - */ - if (row.length) { - rows.push(row.join(' ')) - } - - return rows -} - -/** - * Converts the description to multiple lines fitting into - * a given column size - */ -function descriptionToRows( - description: string, - options: { - nameColumnSize: number - descriptionColumnsSize: number - } -): string { - return lineToRows(description, options.descriptionColumnsSize) - .map((column, index) => { - return index > 0 ? `${new Array(options.nameColumnSize + 1).join(' ')}${column}` : column - }) - .join('') -} - -/** - * Wraps the command arg inside `<>` or `[]` brackets based upon if it's - * required or not. - */ -function wrapArg(arg: CommandArg): string { - const displayName = arg.type === 'spread' ? `...${arg.name}` : arg.name - return arg.required ? `<${displayName}>` : `[${displayName}]` -} - -/** - * Returns an array of flags for displaying the help screen - */ -function getFlagsForDisplay(flags: CommandFlag[]) { - return flags.map(({ name, type, alias, description }) => { - /** - * Display name is the way we want to display a single flag in the - * list of flags - */ - const displayName = alias ? `-${alias}, --${name}` : `--${name}` - - /** - * The type hints the user about the expectation on the flag type. We only - * print the type, when flag is not a boolean. - */ - let displayType = '' - switch (type) { - case 'array': - displayType = 'string[]' - break - case 'numArray': - displayType = 'number[]' - break - case 'string': - displayType = 'string' - break - case 'boolean': - displayType = 'boolean' - break - case 'number': - displayType = 'number' - break - } - - return { - displayName, - displayType, - description, - width: displayName.length + displayType.length, - } - }) -} - -/** - * Returns an array of args for displaying the help screen - */ -function getArgsForDisplay(args: CommandArg[]) { - return args.map(({ name, description }) => { - return { - displayName: name, - description: description, - width: name.length, - } - }) -} - -/** - * Returns an array of commands for display - */ -function getCommandsForDisplay(commands: SerializedCommand[], aliases: Aliases) { - return commands.map(({ commandName, description }) => { - const commandAliases = getCommandAliases(commandName, aliases) - const aliasesString = commandAliases.length ? ` [${commandAliases.join(', ')}]` : '' - return { - displayName: `${commandName}${aliasesString}`, - description, - width: commandName.length + aliasesString.length, - } - }) -} - -/** - * Returns the aliases for a given command - */ -function getCommandAliases(commandName: string, aliases: Aliases) { - return Object.keys(aliases).reduce((commandAliases, alias) => { - if (aliases[alias] === commandName) { - commandAliases.push(alias) - } - return commandAliases - }, []) -} - -/** - * Prints help for all the commands by sorting them in alphabetical order - * and grouping them as per their namespace. - */ -export function printHelp( - commands: SerializedCommand[], - flags: CommandFlag[], - aliases: Aliases -): void { - const flagsList = getFlagsForDisplay(flags) - const commandsList = getCommandsForDisplay(commands, aliases) - - /** - * Get width of longest command name. - */ - const maxWidth = Math.max.apply( - Math, - flagsList.concat(commandsList as any).map(({ width }) => width) - ) - - /** - * Size of the terminal columns. Max width is the width of the command - * name and the extra four is whitespace around the command name. - * - * This gives the columns size for the description section - */ - const descriptionColumnsSize = termSize().columns - (maxWidth + 4) - - /** - * Sort commands and group them, so that we can print them as per - * the namespace they belongs to - */ - sortAndGroupCommands(commands).forEach(({ group, commands: groupCommands }) => { - console.log('') - - if (group === 'root') { - console.log(logger.colors.bold(logger.colors.yellow('Available commands'))) - } else { - console.log(logger.colors.bold(logger.colors.yellow(group))) - } - - groupCommands.forEach(({ commandName, description }) => { - const commandAliases = getCommandAliases(commandName, aliases) - const aliasesString = commandAliases.length ? ` [${commandAliases.join(', ')}]` : '' - const displayName = `${commandName}${aliasesString}` - - const whiteSpace = ''.padEnd(maxWidth - displayName.length, ' ') - const descriptionRows = descriptionToRows(description, { - nameColumnSize: maxWidth + 4, - descriptionColumnsSize, - }) - - console.log( - ` ${logger.colors.green(displayName)} ${whiteSpace} ${logger.colors.dim(descriptionRows)}` - ) - }) - }) - - if (flagsList.length) { - console.log('') - console.log(logger.colors.bold(logger.colors.yellow('Global Flags'))) - - flagsList.forEach(({ displayName, displayType, description = '', width }) => { - const whiteSpace = ''.padEnd(maxWidth - width, ' ') - const descriptionRows = descriptionToRows(description, { - nameColumnSize: maxWidth + 4, - descriptionColumnsSize, - }) - - console.log( - ` ${logger.colors.green(displayName)} ${logger.colors.dim( - displayType - )}${whiteSpace} ${logger.colors.dim(descriptionRows)}` - ) - }) - } -} - -/** - * Prints help for a single command - */ -export function printHelpFor(command: SerializedCommand, aliases: Aliases): void { - if (command.description) { - console.log('') - console.log(command.description) - } - - console.log('') - console.log( - `${logger.colors.yellow('Usage:')} ${command.commandName} ${logger.colors.dim( - command.args.map(wrapArg).join(' ') - )}` - ) - - const flags = getFlagsForDisplay(command.flags) - const args = getArgsForDisplay(command.args) - - /** - * Getting max width to keep flags and args symmetric - */ - const maxWidth = Math.max.apply( - Math, - flags.concat(args as any).map(({ width }) => width) - ) - - /** - * Size of the terminal columns. Max width is the width of the command - * name and the extra four is whitespace around the command name. - * - * This gives the columns size for the description section - */ - const descriptionColumnsSize = termSize().columns - (maxWidth + 5) - - const commandAliases = getCommandAliases(command.commandName, aliases) - if (commandAliases.length) { - console.log('') - console.log( - `${logger.colors.yellow('Aliases:')} ${logger.colors.green(commandAliases.join(', '))}` - ) - } - - if (args.length) { - console.log('') - console.log(logger.colors.bold(logger.colors.yellow('Arguments'))) - - args.forEach(({ displayName, description = '', width }) => { - const whiteSpace = ''.padEnd(maxWidth - width, ' ') - const descriptionRow = descriptionToRows(description, { - nameColumnSize: maxWidth + 5, - descriptionColumnsSize, - }) - - console.log( - ` ${logger.colors.green(displayName)} ${whiteSpace} ${logger.colors.dim(descriptionRow)}` - ) - }) - } - - if (flags.length) { - console.log('') - console.log(logger.colors.bold(logger.colors.yellow('Flags'))) - - flags.forEach(({ displayName, displayType, description = '', width }) => { - const whiteSpace = ''.padEnd(maxWidth - width, ' ') - const descriptionRow = descriptionToRows(description, { - nameColumnSize: maxWidth + 5, - descriptionColumnsSize, - }) - - console.log( - ` ${logger.colors.green(displayName)} ${logger.colors.dim( - displayType - )}${whiteSpace} ${logger.colors.dim(descriptionRow)}` - ) - }) - } -} diff --git a/src/utils/listDirectoryFiles.ts b/src/utils/listDirectoryFiles.ts deleted file mode 100644 index e9d24cf..0000000 --- a/src/utils/listDirectoryFiles.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* - * @adonisjs/ace - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import slash from 'slash' -import { join, relative, extname } from 'path' -import { fsReadAll } from '@poppinss/utils/build/helpers' - -import { CommandsListFilterFn } from '../Contracts' - -/** - * Checks if the file exists inside the array. Also an extension - * agnostic check is performed to handle `.ts` and `.js` files - * both - */ -function filesFilter(fileName: string, filesToIgnore: string[]) { - if (filesToIgnore.includes(fileName)) { - return true - } - - fileName = fileName.replace(extname(fileName), '') - return filesToIgnore.includes(fileName) -} - -/** - * Returns an array of Javascript files inside the current directory in - * relative to the application root. - */ -export function listDirectoryFiles( - scanDirectory: string, - appRoot: string, - filesToIgnore?: CommandsListFilterFn -): string[] { - return fsReadAll(scanDirectory) - .filter((name) => !name.endsWith('.json')) // remove .json files - .map((name) => { - const relativePath = relative(appRoot, join(scanDirectory, name)) - return slash(relativePath.startsWith('../') ? relativePath : `./${relativePath}`) - }) - .filter((name) => { - if (typeof filesToIgnore === 'function') { - return filesToIgnore(name) - } - - return Array.isArray(filesToIgnore) ? !filesFilter(name, filesToIgnore) : true - }) -} diff --git a/src/utils/sortAndGroupCommands.ts b/src/utils/sortAndGroupCommands.ts deleted file mode 100644 index f9de688..0000000 --- a/src/utils/sortAndGroupCommands.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* - * @adonisjs/ace - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { CommandsGroup, SerializedCommand } from '../Contracts' - -/** - * Loops over the commands and converts them to an array of sorted groups with - * nested commands inside them. The grouping is done using the command - * namespace seperated with `:`. Example: `make:controller` - */ -export function sortAndGroupCommands(commands: SerializedCommand[]): CommandsGroup { - /** - * Create a group of commands using it's namespace - */ - const groupsLiteral = commands.reduce((result, command) => { - const tokens = command.commandName.split(':') - - /** - * Use the command namespace or move it inside the `root` group when - * it is not namespaced. - */ - const group = tokens.length > 1 ? tokens.shift()! : 'root' - - result[group] = result[group] || [] - result[group].push(command) - - return result - }, {} as { [key: string]: SerializedCommand[] }) - - /** - * Convert the object literal groups and it's command to an - * array of sorted groups and commands - */ - return Object.keys(groupsLiteral) - .sort((prev, curr) => { - if (prev === 'root') { - return -1 - } - - if (curr === 'root') { - return 1 - } - - if (curr > prev) { - return -1 - } - - if (curr < prev) { - return 1 - } - - return 0 - }) - .map((name) => { - return { - group: name, - commands: groupsLiteral[name].sort((prev, curr) => { - if (curr.commandName > prev.commandName) { - return -1 - } - - if (curr.commandName < prev.commandName) { - return 1 - } - - return 0 - }), - } - }) -} diff --git a/src/utils/template.ts b/src/utils/template.ts deleted file mode 100644 index e7d0b05..0000000 --- a/src/utils/template.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* - * @adonisjs/ace - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { runInNewContext } from 'vm' -import Mustache from 'mustache' -import { readFileSync } from 'fs' - -const STACK_REGEXP = /evalmachine\.:(\d+)(?::(\d+))?\n/ -const STACK_REGEXP_ALL = new RegExp(STACK_REGEXP.source, 'g') - -/** - * Process string as a template literal string and processes - * data - */ -export function template( - tpl: string, - data: Object, - filename: string = 'eval', - isMustache: boolean = false -) { - if (isMustache) { - return Mustache.render(tpl, data) - } - - try { - return runInNewContext('`' + tpl + '`', data) - } catch (error) { - const positions = error.stack.match(STACK_REGEXP_ALL) - if (!positions) { - throw error - } - - const position: string[] = [filename] - const tokens = positions.pop().match(STACK_REGEXP) - if (tokens[1]) { - position.push(tokens[1]) - } - - if (tokens[2]) { - position.push(tokens[2]) - } - throw new Error(`Error in template ${position.join(':')}\n${error.message}`) - } -} - -/** - * Loads template file from the disk and process it contents - * using the [[template]] method - */ -export function templateFromFile(file: string, data: object, isMustache: boolean): string { - const contents = readFileSync(file, 'utf8') - return template(contents, data, file, isMustache) -} diff --git a/src/utils/validateCommand.ts b/src/utils/validateCommand.ts deleted file mode 100644 index 0af25ba..0000000 --- a/src/utils/validateCommand.ts +++ /dev/null @@ -1,77 +0,0 @@ -/* - * @adonisjs/ace - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { Exception } from '@poppinss/utils' -import { CommandConstructorContract, CommandArg } from '../Contracts' - -/** - * Validates the command static properties to ensure that all the - * values are correctly defined for a command to be executed. - */ -export function validateCommand( - command: any, - commandPath?: string -): asserts command is CommandConstructorContract { - if (!command.name) { - throw new Exception( - `Invalid command"${ - commandPath ? ` ${commandPath}` : '' - }". Make sure the command is exported using the "export default"` - ) - } - - /** - * Ensure command has a name, a boot method and args property - */ - if (!command.commandName || typeof command.boot !== 'function') { - throw new Exception( - `Invalid command "${command.name}". Make sure to define the static property "commandName"` - ) - } - - /** - * Boot command - */ - command.boot() - - /** - * Ensure command has args and flags after the boot method - */ - if (!Array.isArray(command.args) || !Array.isArray(command.flags)) { - throw new Exception(`Invalid command "${command.name}". Make sure it extends the BaseCommand`) - } - - let optionalArg: CommandArg - - /** - * Validate for optional args and spread args - */ - command.args.forEach((arg: CommandArg, index: number) => { - /** - * Ensure optional arguments comes after required - * arguments - */ - if (optionalArg && arg.required) { - throw new Exception( - `Optional argument "${optionalArg.name}" must be after the required argument "${arg.name}"` - ) - } - - /** - * Ensure spread arg is the last arg - */ - if (arg.type === 'spread' && command.args.length > index + 1) { - throw new Exception(`Spread argument "${arg.name}" must be at last position`) - } - - if (!arg.required) { - optionalArg = arg - } - }) -} diff --git a/src/yars_config.ts b/src/yars_config.ts new file mode 100644 index 0000000..d144514 --- /dev/null +++ b/src/yars_config.ts @@ -0,0 +1,30 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Configuration } from 'yargs-parser' + +/** + * The fixed config used to parse command line arguments using yargs. We + * do not allow changing these options, since some of the internal + * checks and features rely on this specific config + */ +export const yarsConfig: Partial = { + 'camel-case-expansion': false, + 'combine-arrays': true, + 'short-option-groups': true, + 'dot-notation': false, + 'parse-numbers': true, + 'parse-positional-numbers': false, + 'boolean-negation': true, + 'flatten-duplicate-arrays': true, + 'greedy-arrays': false, + 'strip-aliased': true, + 'nargs-eats-options': false, + 'unknown-options-as-args': false, +} diff --git a/test-helpers/index.ts b/test-helpers/index.ts deleted file mode 100644 index 7863ea9..0000000 --- a/test-helpers/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * @adonisjs/ace - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { join } from 'path' -import { Filesystem } from '@poppinss/dev-utils' -import { Application } from '@adonisjs/application' -import { Kernel } from '../src/Kernel' - -export const fs = new Filesystem(join(__dirname, 'app')) - -export function setupApp() { - const app = new Application(fs.basePath, 'console', {}) - return app -} - -export function getKernel(app: Application) { - return new Kernel(app) -} - -export const info = process.env.CI ? '[ info ]' : '[ blue(info) ]' -export const success = process.env.CI ? '[ success ]' : '[ green(success) ]' -export const error = process.env.CI ? '[ error ]' : '[ red(error) ]' -export const warning = process.env.CI ? '[ warn ]' : '[ yellow(warn) ]' -export const dimYellow = (value: string) => (process.env.CI ? value : `dim(yellow(${value}))`) diff --git a/test/fixtures/template1.mustache b/test/fixtures/template1.mustache deleted file mode 100644 index 58a5d94..0000000 --- a/test/fixtures/template1.mustache +++ /dev/null @@ -1 +0,0 @@ -Hello {{value1}}, {{value2}} diff --git a/test/fixtures/template1.txt b/test/fixtures/template1.txt deleted file mode 100644 index 6ab1460..0000000 --- a/test/fixtures/template1.txt +++ /dev/null @@ -1 +0,0 @@ -Hello ${value1}, ${value2} \ No newline at end of file diff --git a/test/generator-file.spec.ts b/test/generator-file.spec.ts deleted file mode 100644 index 5ae6c0a..0000000 --- a/test/generator-file.spec.ts +++ /dev/null @@ -1,265 +0,0 @@ -/* - * @adonisjs/ace - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { test } from '@japa/runner' -import { join } from 'path' -import { GeneratorFile } from '../src/Generator/File' - -test.group('Generator File', () => { - test('use the base name for computing the filename', ({ assert }) => { - const file = new GeneratorFile('foo/bar', { pattern: 'pascalcase' }) - file.destinationDir(__dirname) - - assert.deepEqual(file.toJSON(), { - filename: 'Bar', - contents: '', - state: 'pending', - filepath: join(__dirname, 'foo', 'Bar.ts'), - relativepath: join(__dirname, 'foo', 'Bar.ts'), - extension: '.ts', - }) - }) - - test('add suffix when defined', ({ assert }) => { - const file = new GeneratorFile('foo/user', { - suffix: 'controller', - pattern: 'pascalcase', - }) - file.destinationDir(__dirname) - - assert.deepEqual(file.toJSON(), { - filename: 'UserController', - contents: '', - state: 'pending', - filepath: join(__dirname, 'foo', 'UserController.ts'), - relativepath: join(__dirname, 'foo', 'UserController.ts'), - extension: '.ts', - }) - }) - - test('do not add suffix when already defined in the name', ({ assert }) => { - const file = new GeneratorFile('foo/userController', { - suffix: 'controller', - pattern: 'pascalcase', - }) - file.destinationDir(__dirname) - - assert.deepEqual(file.toJSON(), { - filename: 'UserController', - contents: '', - state: 'pending', - filepath: join(__dirname, 'foo', 'UserController.ts'), - relativepath: join(__dirname, 'foo', 'UserController.ts'), - extension: '.ts', - }) - }) - - test('pluralize name when form is plural', ({ assert }) => { - const file = new GeneratorFile('foo/user', { - suffix: 'controller', - form: 'plural', - pattern: 'pascalcase', - }) - file.destinationDir(__dirname) - - assert.deepEqual(file.toJSON(), { - filename: 'UsersController', - contents: '', - state: 'pending', - filepath: join(__dirname, 'foo', 'UsersController.ts'), - relativepath: join(__dirname, 'foo', 'UsersController.ts'), - extension: '.ts', - }) - }) - - test('pluralize name properly when name has suffix', ({ assert }) => { - const file = new GeneratorFile('usercontroller', { - suffix: 'controller', - form: 'plural', - pattern: 'pascalcase', - }) - file.destinationDir(__dirname) - - assert.deepEqual(file.toJSON(), { - filename: 'UsersController', - contents: '', - state: 'pending', - filepath: join(__dirname, 'UsersController.ts'), - relativepath: join(__dirname, 'UsersController.ts'), - extension: '.ts', - }) - }) - - test('handle case where suffix is name is added after a dash', ({ assert }) => { - const file = new GeneratorFile('user-controller', { - suffix: 'controller', - form: 'plural', - pattern: 'pascalcase', - }) - file.destinationDir(__dirname) - - assert.deepEqual(file.toJSON(), { - filename: 'UsersController', - contents: '', - state: 'pending', - filepath: join(__dirname, 'UsersController.ts'), - relativepath: join(__dirname, 'UsersController.ts'), - extension: '.ts', - }) - }) - - test('use app root when destination path is not absolute', ({ assert }) => { - const file = new GeneratorFile('foo/user-controller', { - suffix: 'controller', - form: 'plural', - pattern: 'pascalcase', - }) - - file.appRoot(__dirname) - file.destinationDir('foo') - - assert.deepEqual(file.toJSON(), { - filename: 'UsersController', - contents: '', - state: 'pending', - filepath: join(__dirname, 'foo', 'foo', 'UsersController.ts'), - relativepath: join('foo', 'foo', 'UsersController.ts'), - extension: '.ts', - }) - }) - - test('do not use app root when destination path is absolute', ({ assert }) => { - const file = new GeneratorFile('user-controller', { - suffix: 'controller', - form: 'plural', - pattern: 'pascalcase', - }) - - file.appRoot(__dirname) - file.destinationDir(__dirname) - - assert.deepEqual(file.toJSON(), { - filename: 'UsersController', - contents: '', - state: 'pending', - filepath: join(__dirname, 'UsersController.ts'), - relativepath: 'UsersController.ts', - extension: '.ts', - }) - }) - - test('use process.cwd() when app root is not defined', ({ assert }) => { - const file = new GeneratorFile('user-controller', { - suffix: 'controller', - form: 'plural', - pattern: 'pascalcase', - }) - file.destinationDir('foo') - - assert.deepEqual(file.toJSON(), { - filename: 'UsersController', - contents: '', - state: 'pending', - filepath: join(process.cwd(), 'foo', 'UsersController.ts'), - relativepath: join(process.cwd(), 'foo', 'UsersController.ts'), - extension: '.ts', - }) - }) - - test('substitute stub variables from raw string', ({ assert }) => { - const file = new GeneratorFile('foo/user-controller', { - suffix: 'controller', - form: 'plural', - pattern: 'pascalcase', - }) - file.destinationDir('foo') - file.stub('Hello ${name}', { raw: true }).apply({ name: 'virk' }) - - assert.deepEqual(file.toJSON(), { - filename: 'UsersController', - contents: 'Hello virk', - state: 'pending', - filepath: join(process.cwd(), 'foo', 'foo', 'UsersController.ts'), - relativepath: join(process.cwd(), 'foo', 'foo', 'UsersController.ts'), - extension: '.ts', - }) - }) - - test('add prefix when defined', ({ assert }) => { - const file = new GeneratorFile('foo/user', { - prefix: 'controller', - pattern: 'pascalcase', - }) - file.destinationDir(__dirname) - - assert.deepEqual(file.toJSON(), { - filename: 'ControllerUser', - contents: '', - state: 'pending', - filepath: join(__dirname, 'foo', 'ControllerUser.ts'), - relativepath: join(__dirname, 'foo', 'ControllerUser.ts'), - extension: '.ts', - }) - }) - - test('do not add prefix when already defined in the name', ({ assert }) => { - const file = new GeneratorFile('foo/controlleruser', { - prefix: 'controller', - pattern: 'pascalcase', - }) - file.destinationDir(__dirname) - - assert.deepEqual(file.toJSON(), { - filename: 'ControllerUser', - contents: '', - state: 'pending', - filepath: join(__dirname, 'foo', 'ControllerUser.ts'), - relativepath: join(__dirname, 'foo', 'ControllerUser.ts'), - extension: '.ts', - }) - }) - - test('do not pluralize when word is in ignore list', ({ assert }) => { - const file = new GeneratorFile('foo/home', { - suffix: 'controller', - form: 'plural', - pattern: 'pascalcase', - formIgnoreList: ['Home'], - }) - file.destinationDir(__dirname) - - assert.deepEqual(file.toJSON(), { - filename: 'HomeController', - contents: '', - state: 'pending', - filepath: join(__dirname, 'foo', 'HomeController.ts'), - relativepath: join(__dirname, 'foo', 'HomeController.ts'), - extension: '.ts', - }) - }) - - test('do not pluralize when word is in ignore list and has the suffix', ({ assert }) => { - const file = new GeneratorFile('homecontroller', { - suffix: 'controller', - form: 'plural', - pattern: 'pascalcase', - formIgnoreList: ['Home'], - }) - file.destinationDir(__dirname) - - assert.deepEqual(file.toJSON(), { - filename: 'HomeController', - contents: '', - state: 'pending', - filepath: join(__dirname, 'HomeController.ts'), - relativepath: join(__dirname, 'HomeController.ts'), - extension: '.ts', - }) - }) -}) diff --git a/test/generator.spec.ts b/test/generator.spec.ts deleted file mode 100644 index 5fc0196..0000000 --- a/test/generator.spec.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * @adonisjs/ace - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { test } from '@japa/runner' -import { join } from 'path' - -import { Generator } from '../src/Generator' -import { BaseCommand } from '../src/BaseCommand' -import { setupApp, fs, getKernel } from '../test-helpers' - -class GeneratorCommand extends BaseCommand { - public async handle() {} -} - -test.group('Generator', (group) => { - group.teardown(async () => { - await fs.cleanup() - }) - - test('generate one or more entity files', async ({ assert }) => { - const app = setupApp() - const kernel = getKernel(app) - - const generator = new Generator(new GeneratorCommand(app, kernel), fs.basePath) - generator.addFile('user', { suffix: 'controller', pattern: 'pascalcase' }) - generator.addFile('account', { suffix: 'controller', pattern: 'pascalcase' }) - - const files = await generator.run() - assert.equal(files[0].state, 'persisted') - assert.equal(files[1].state, 'persisted') - - const userExists = await fs.fsExtra.pathExists(join(fs.basePath, 'UserController.ts')) - const accountExists = await fs.fsExtra.pathExists(join(fs.basePath, 'UserController.ts')) - - assert.isTrue(userExists) - assert.isTrue(accountExists) - }) - - test('do not overwrite existing files', async ({ assert }) => { - const app = setupApp() - const kernel = getKernel(app) - - const generator = new Generator(new GeneratorCommand(app, kernel), fs.basePath) - await fs.add('UserController.ts', "export const greeting = 'hello world'") - - generator.addFile('user', { suffix: 'controller', pattern: 'pascalcase' }) - - const files = await generator.run() - assert.equal(files[0].state, 'pending') - assert.equal(files[0].toJSON().filename, 'UserController') - - const user = await fs.get('UserController.ts') - assert.equal(user, "export const greeting = 'hello world'") - }) -}) diff --git a/test/kernel-flow.spec.ts b/test/kernel-flow.spec.ts deleted file mode 100644 index e497cb4..0000000 --- a/test/kernel-flow.spec.ts +++ /dev/null @@ -1,1347 +0,0 @@ -/* - * @adonisjs/ace - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { test } from '@japa/runner' -import { Kernel } from '../src/Kernel' -import { BaseCommand } from '../src/BaseCommand' - -import { setupApp } from '../test-helpers' -import { args } from '../src/Decorators/args' -import { flags } from '../src/Decorators/flags' - -test.group('Kernel | no argv', () => { - test('execute the default command when no argv are defined', async ({ assert }) => { - assert.plan(3) - - class MyDefaultCommand extends BaseCommand { - public static commandName = 'help' - public async run() { - assert.isTrue(true) - } - } - - const kernel = new Kernel(setupApp()) - kernel.defaultCommand = MyDefaultCommand - - kernel.onExit(() => { - assert.equal(kernel.exitCode, 0) - assert.isUndefined(kernel.error) - }) - - await kernel.handle([]) - }) - - test('handle exceptions raised by the default command', async ({ assert }) => { - assert.plan(2) - - class MyDefaultCommand extends BaseCommand { - public static commandName = 'help' - public async run() { - throw new Error('boom') - } - } - - const kernel = new Kernel(setupApp()) - kernel.defaultCommand = MyDefaultCommand - - kernel.onExit(() => { - assert.equal(kernel.exitCode, 1) - assert.equal(kernel.error!.message, 'boom') - }) - - await kernel.handle([]) - }) - - test('execute find hooks when running the default command', async ({ assert }) => { - assert.plan(3) - - const stack: string[] = [] - - class MyDefaultCommand extends BaseCommand { - public static commandName = 'help' - public async run() {} - } - - const kernel = new Kernel(setupApp()) - kernel.defaultCommand = MyDefaultCommand - - kernel.before('find', () => stack.push('before-find')) - kernel.after('find', () => stack.push('after-find')) - - kernel.onExit(() => { - assert.equal(kernel.exitCode, 0) - assert.isUndefined(kernel.error) - assert.deepEqual(stack, ['before-find', 'after-find']) - }) - - await kernel.handle([]) - }) - - test('execute run hooks when running the default command', async ({ assert }) => { - assert.plan(3) - - const stack: string[] = [] - - class MyDefaultCommand extends BaseCommand { - public static commandName = 'help' - public async run() {} - } - - const kernel = new Kernel(setupApp()) - kernel.defaultCommand = MyDefaultCommand - - kernel.before('find', () => stack.push('before-find')) - kernel.after('find', () => stack.push('after-find')) - - kernel.before('run', () => stack.push('before-run')) - kernel.after('run', () => stack.push('after-run')) - - kernel.onExit(() => { - assert.equal(kernel.exitCode, 0) - assert.isUndefined(kernel.error) - assert.deepEqual(stack, ['before-find', 'after-find', 'before-run', 'after-run']) - }) - - await kernel.handle([]) - }) - - test('run all hooks even when default command raises an exception', async ({ assert }) => { - assert.plan(3) - - const stack: string[] = [] - - class MyDefaultCommand extends BaseCommand { - public static commandName = 'help' - public async run() { - throw new Error('boom') - } - } - - const kernel = new Kernel(setupApp()) - kernel.defaultCommand = MyDefaultCommand - - kernel.before('find', () => stack.push('before-find')) - kernel.after('find', () => stack.push('after-find')) - - kernel.before('run', () => stack.push('before-run')) - kernel.after('run', () => stack.push('after-run')) - - kernel.onExit(() => { - assert.equal(kernel.exitCode, 1) - assert.equal(kernel.error!.message, 'boom') - assert.deepEqual(stack, ['before-find', 'after-find', 'before-run', 'after-run']) - }) - - await kernel.handle([]) - }) - - test('handle case where find hooks raise an exception', async ({ assert }) => { - assert.plan(3) - - const stack: string[] = [] - - class MyDefaultCommand extends BaseCommand { - public static commandName = 'help' - public async run() { - throw new Error('boom') - } - } - - const kernel = new Kernel(setupApp()) - kernel.defaultCommand = MyDefaultCommand - - kernel.before('find', () => { - throw new Error('before find failed') - }) - kernel.after('find', () => stack.push('after-find')) - - kernel.before('run', () => stack.push('before-run')) - kernel.after('run', () => stack.push('after-run')) - - kernel.onExit(() => { - assert.equal(kernel.exitCode, 1) - assert.equal(kernel.error!.message, 'before find failed') - assert.deepEqual(stack, []) - }) - - await kernel.handle([]) - }) - - test('handle case where run hooks raise an exception', async ({ assert }) => { - assert.plan(3) - - const stack: string[] = [] - - class MyDefaultCommand extends BaseCommand { - public static commandName = 'help' - public async run() { - throw new Error('boom') - } - } - - const kernel = new Kernel(setupApp()) - kernel.defaultCommand = MyDefaultCommand - - kernel.before('find', () => stack.push('before-find')) - kernel.after('find', () => stack.push('after-find')) - - kernel.before('run', () => { - throw new Error('before run failed') - }) - kernel.after('run', () => stack.push('after-run')) - - kernel.onExit(() => { - assert.equal(kernel.exitCode, 1) - assert.equal(kernel.error!.message, 'before run failed') - assert.deepEqual(stack, ['before-find', 'after-find']) - }) - - await kernel.handle([]) - }) - - test('handle case where "after run" hooks raise an exception', async ({ assert }) => { - assert.plan(3) - - const stack: string[] = [] - - class MyDefaultCommand extends BaseCommand { - public static commandName = 'help' - public async run() { - throw new Error('boom') - } - } - - const kernel = new Kernel(setupApp()) - kernel.defaultCommand = MyDefaultCommand - - kernel.before('find', () => stack.push('before-find')) - kernel.after('find', () => stack.push('after-find')) - - kernel.before('run', () => stack.push('before-run')) - kernel.after('run', () => { - throw new Error('after run failed') - }) - kernel.after('run', () => stack.push('after-run')) - - kernel.onExit(() => { - assert.equal(kernel.exitCode, 1) - assert.equal(kernel.error!.message, 'after run failed') - assert.deepEqual(stack, ['before-find', 'after-find', 'before-run']) - }) - - await kernel.handle([]) - }) -}) - -test.group('Kernel | only flags', () => { - test('execute global flags when no command name is defined', async ({ assert }) => { - assert.plan(3) - const kernel = new Kernel(setupApp()) - - kernel.flag( - 'help', - () => { - assert.isTrue(true) - }, - {} - ) - - kernel.onExit(() => { - assert.equal(kernel.exitCode, 0) - assert.isUndefined(kernel.error) - }) - - await kernel.handle(['--help']) - }) - - test('execute global flags when no command name is defined', async ({ assert }) => { - assert.plan(3) - const kernel = new Kernel(setupApp()) - - kernel.flag( - 'help', - () => { - assert.isTrue(true) - }, - {} - ) - - kernel.onExit(() => { - assert.equal(kernel.exitCode, 0) - assert.isUndefined(kernel.error) - }) - - await kernel.handle(['--help']) - }) - - test('execute string type flags', async ({ assert }) => { - assert.plan(3) - const kernel = new Kernel(setupApp()) - - kernel.flag( - 'env', - (value) => { - assert.equal(value, 'production') - }, - { - type: 'string', - } - ) - - kernel.onExit(() => { - assert.equal(kernel.exitCode, 0) - assert.isUndefined(kernel.error) - }) - - await kernel.handle(['--env=production']) - }) - - test('execute array type flags', async ({ assert }) => { - assert.plan(3) - const kernel = new Kernel(setupApp()) - - kernel.flag( - 'env', - (value) => { - assert.deepEqual(value, ['production', 'development']) - }, - { - type: 'array', - } - ) - - kernel.onExit(() => { - assert.equal(kernel.exitCode, 0) - assert.isUndefined(kernel.error) - }) - - await kernel.handle(['--env=production,development']) - }) - - test('do not execute flag handlers when not mentioned in argv', async ({ assert }) => { - assert.plan(3) - const kernel = new Kernel(setupApp()) - - kernel.flag( - 'help', - () => { - assert.isTrue(true) - }, - {} - ) - - kernel.flag( - 'env', - () => { - throw new Error('Never expected to be called') - }, - {} - ) - - kernel.onExit(() => { - assert.equal(kernel.exitCode, 0) - assert.isUndefined(kernel.error) - }) - - await kernel.handle(['--help']) - }) - - test('do not execute flag of type string when not mentioned in argv', async ({ assert }) => { - assert.plan(3) - const kernel = new Kernel(setupApp()) - - kernel.flag( - 'help', - () => { - assert.isTrue(true) - }, - {} - ) - - kernel.flag( - 'env', - () => { - throw new Error('Never expected to be called') - }, - { - type: 'string', - } - ) - - kernel.onExit(() => { - assert.equal(kernel.exitCode, 0) - assert.isUndefined(kernel.error) - }) - - await kernel.handle(['--help']) - }) - - test('do not execute flag of type array when not mentioned in argv', async ({ assert }) => { - assert.plan(3) - const kernel = new Kernel(setupApp()) - - kernel.flag( - 'help', - () => { - assert.isTrue(true) - }, - {} - ) - - kernel.flag( - 'env', - () => { - throw new Error('Never expected to be called') - }, - { - type: 'array', - } - ) - - kernel.onExit(() => { - assert.equal(kernel.exitCode, 0) - assert.isUndefined(kernel.error) - }) - - await kernel.handle(['--help']) - }) - - test('handle use case when flag raises an exception', async ({ assert }) => { - assert.plan(2) - const kernel = new Kernel(setupApp()) - - kernel.flag( - 'env', - () => { - throw new Error('boom') - }, - { - type: 'array', - } - ) - - kernel.onExit(() => { - assert.equal(kernel.exitCode, 1) - assert.equal(kernel.error!.message, 'boom') - }) - - await kernel.handle(['--env=production,development']) - }) -}) - -test.group('Kernel | command found', () => { - test('execute registered command', async ({ assert }) => { - assert.plan(3) - const kernel = new Kernel(setupApp()) - - class HelloCommand extends BaseCommand { - public static commandName = 'hello' - - @args.string() - public name: string - - public async run() { - assert.equal(this.name, 'world') - } - } - - kernel.register([HelloCommand]) - - kernel.onExit(() => { - assert.equal(kernel.exitCode, 0) - assert.isUndefined(kernel.error) - }) - - await kernel.handle(['hello', 'world']) - }) - - test('handle use case where command raises an exception', async ({ assert }) => { - assert.plan(2) - const kernel = new Kernel(setupApp()) - - class HelloCommand extends BaseCommand { - public static commandName = 'hello' - - @args.string() - public name: string - - public async run() { - throw new Error('boom') - } - } - - kernel.register([HelloCommand]) - - kernel.onExit(() => { - assert.equal(kernel.exitCode, 1) - assert.equal(kernel.error.message, 'boom') - }) - - await kernel.handle(['hello', 'world']) - }) - - test('handle use case where long lived marks itself as failed', async ({ assert }, done) => { - assert.plan(2) - const kernel = new Kernel(setupApp()) - - class HelloCommand extends BaseCommand { - public static commandName = 'hello' - public static settings = { - stayAlive: true, - } - - @args.string() - public name: string - - public async run() { - setTimeout(() => { - this.error = new Error('boom') - this.exitCode = 1 - this.kernel.exit(this) - }, 200) - } - } - - kernel.register([HelloCommand]) - - kernel.onExit(() => { - assert.equal(kernel.exitCode, 1) - assert.equal(kernel.error.message, 'boom') - done() - }) - - await kernel.handle(['hello', 'world']) - }).waitForDone() - - test('handle case when command invokes kernel.exit right away', async ({ assert }) => { - assert.plan(3) - const kernel = new Kernel(setupApp()) - - class HelloCommand extends BaseCommand { - public static commandName = 'hello' - - @args.string() - public name: string - - public async run() { - assert.equal(this.name, 'world') - this.kernel.exit(this) - } - } - - kernel.register([HelloCommand]) - - kernel.onExit(() => { - assert.equal(kernel.exitCode, 0) - assert.isUndefined(kernel.error) - }) - - await kernel.handle(['hello', 'world']) - }) - - test('invoke find hooks before running the command', async ({ assert }) => { - assert.plan(4) - const kernel = new Kernel(setupApp()) - - const stack: string[] = [] - - class HelloCommand extends BaseCommand { - public static commandName = 'hello' - - @args.string() - public name: string - - public async run() { - assert.equal(this.name, 'world') - stack.push('command') - } - } - - kernel.register([HelloCommand]) - kernel.before('find', () => stack.push('before-find')) - kernel.after('find', () => stack.push('after-find')) - - kernel.onExit(() => { - assert.equal(kernel.exitCode, 0) - assert.isUndefined(kernel.error) - }) - - await kernel.handle(['hello', 'world']) - assert.deepEqual(stack, ['before-find', 'after-find', 'command']) - }) - - test('invoke run hooks when running the command', async ({ assert }) => { - assert.plan(4) - const kernel = new Kernel(setupApp()) - - const stack: string[] = [] - - class HelloCommand extends BaseCommand { - public static commandName = 'hello' - - @args.string() - public name: string - - public async run() { - assert.equal(this.name, 'world') - stack.push('command') - } - } - - kernel.register([HelloCommand]) - kernel.before('find', () => stack.push('before-find')) - kernel.after('find', () => stack.push('after-find')) - kernel.before('run', () => stack.push('before-run')) - kernel.after('run', () => stack.push('after-run')) - - kernel.onExit(() => { - assert.equal(kernel.exitCode, 0) - assert.isUndefined(kernel.error) - }) - - await kernel.handle(['hello', 'world']) - assert.deepEqual(stack, ['before-find', 'after-find', 'before-run', 'command', 'after-run']) - }) - - test('handle case where before find hook fails', async ({ assert }) => { - assert.plan(3) - const kernel = new Kernel(setupApp()) - - const stack: string[] = [] - - class HelloCommand extends BaseCommand { - public static commandName = 'hello' - - @args.string() - public name: string - - public async run() { - assert.equal(this.name, 'world') - stack.push('command') - } - } - - kernel.register([HelloCommand]) - kernel.before('find', () => { - throw new Error('boom') - }) - kernel.after('find', () => stack.push('after-find')) - kernel.before('run', () => stack.push('before-run')) - kernel.after('run', () => stack.push('after-run')) - - kernel.onExit(() => { - assert.equal(kernel.exitCode, 1) - assert.equal(kernel.error.message, 'boom') - }) - - await kernel.handle(['hello', 'world']) - assert.deepEqual(stack, []) - }) - - test('handle case where after find hook fails', async ({ assert }) => { - assert.plan(3) - const kernel = new Kernel(setupApp()) - - const stack: string[] = [] - - class HelloCommand extends BaseCommand { - public static commandName = 'hello' - - @args.string() - public name: string - - public async run() { - assert.equal(this.name, 'world') - stack.push('command') - } - } - - kernel.register([HelloCommand]) - - kernel.before('find', () => stack.push('before-find')) - kernel.after('find', () => { - throw new Error('boom') - }) - kernel.before('run', () => stack.push('before-run')) - kernel.after('run', () => stack.push('after-run')) - - kernel.onExit(() => { - assert.equal(kernel.exitCode, 1) - assert.equal(kernel.error.message, 'boom') - }) - - await kernel.handle(['hello', 'world']) - assert.deepEqual(stack, ['before-find']) - }) - - test('handle case where before run hook fails', async ({ assert }) => { - assert.plan(3) - const kernel = new Kernel(setupApp()) - - const stack: string[] = [] - - class HelloCommand extends BaseCommand { - public static commandName = 'hello' - - @args.string() - public name: string - - public async run() { - assert.equal(this.name, 'world') - stack.push('command') - } - } - - kernel.register([HelloCommand]) - - kernel.before('find', () => stack.push('before-find')) - kernel.after('find', () => stack.push('after-find')) - kernel.before('run', () => { - throw new Error('boom') - }) - kernel.after('run', () => stack.push('after-run')) - - kernel.onExit(() => { - assert.equal(kernel.exitCode, 1) - assert.equal(kernel.error.message, 'boom') - }) - - await kernel.handle(['hello', 'world']) - assert.deepEqual(stack, ['before-find', 'after-find', 'after-run']) - }) - - test('handle case where after run fails', async ({ assert }) => { - assert.plan(4) - const kernel = new Kernel(setupApp()) - - const stack: string[] = [] - - class HelloCommand extends BaseCommand { - public static commandName = 'hello' - - @args.string() - public name: string - - public async run() { - assert.equal(this.name, 'world') - stack.push('command') - } - } - - kernel.register([HelloCommand]) - - kernel.before('find', () => stack.push('before-find')) - kernel.after('find', () => stack.push('after-find')) - kernel.before('run', () => stack.push('before-run')) - - kernel.after('run', () => { - throw new Error('boom') - }) - - kernel.onExit(() => { - assert.equal(kernel.exitCode, 1) - assert.equal(kernel.error.message, 'boom') - }) - - await kernel.handle(['hello', 'world']) - assert.deepEqual(stack, ['before-find', 'after-find', 'before-run', 'command']) - }) - - test('invoke global flags before running the command', async ({ assert }) => { - assert.plan(4) - const kernel = new Kernel(setupApp()) - - const stack: string[] = [] - - class HelloCommand extends BaseCommand { - public static commandName = 'hello' - - @args.string() - public name: string - - public async run() { - assert.equal(this.name, 'world') - stack.push('command') - } - } - - kernel.register([HelloCommand]) - kernel.flag('env', () => stack.push('env-flag'), { - type: 'string', - }) - - kernel.before('find', () => stack.push('before-find')) - kernel.after('find', () => stack.push('after-find')) - kernel.before('run', () => stack.push('before-run')) - kernel.after('run', () => stack.push('after-run')) - - kernel.onExit(() => { - assert.equal(kernel.exitCode, 0) - assert.isUndefined(kernel.error) - }) - - await kernel.handle(['hello', 'world', '--env=prod']) - assert.deepEqual(stack, [ - 'before-find', - 'after-find', - 'env-flag', - 'before-run', - 'command', - 'after-run', - ]) - }) - - test('handle case when global flag raises an exception', async ({ assert }) => { - assert.plan(3) - const kernel = new Kernel(setupApp()) - - const stack: string[] = [] - - class HelloCommand extends BaseCommand { - public static commandName = 'hello' - - @args.string() - public name: string - - public async run() { - assert.equal(this.name, 'world') - stack.push('command') - } - } - - kernel.register([HelloCommand]) - kernel.flag( - 'env', - () => { - throw new Error('boom') - }, - { - type: 'string', - } - ) - kernel.before('find', () => stack.push('before-find')) - kernel.after('find', () => stack.push('after-find')) - kernel.before('run', () => stack.push('before-run')) - kernel.after('run', () => stack.push('after-run')) - - kernel.onExit(() => { - assert.equal(kernel.exitCode, 1) - assert.equal(kernel.error.message, 'boom') - }) - - await kernel.handle(['hello', 'world', '--env=prod']) - assert.deepEqual(stack, ['before-find', 'after-find']) - }) - - test('validate command args', async ({ assert }) => { - assert.plan(3) - const kernel = new Kernel(setupApp()) - - const stack: string[] = [] - - class HelloCommand extends BaseCommand { - public static commandName = 'hello' - - @args.string() - public name: string - - public async run() { - stack.push('command') - } - } - - kernel.register([HelloCommand]) - - kernel.before('find', () => stack.push('before-find')) - kernel.after('find', () => stack.push('after-find')) - kernel.before('run', () => stack.push('before-run')) - kernel.after('run', () => stack.push('after-run')) - - kernel.onExit(() => { - assert.equal(kernel.exitCode, 1) - assert.equal(kernel.error.message, 'E_MISSING_ARGUMENT: Missing required argument "name"') - }) - - await kernel.handle(['hello']) - assert.deepEqual(stack, ['before-find', 'after-find']) - }) - - test('validate command flags', async ({ assert }) => { - assert.plan(3) - const kernel = new Kernel(setupApp()) - - const stack: string[] = [] - - class HelloCommand extends BaseCommand { - public static commandName = 'hello' - - @args.string() - public name: string - - @flags.number() - public logLevel: number - - public async run() { - stack.push('command') - } - } - - kernel.register([HelloCommand]) - - kernel.before('find', () => stack.push('before-find')) - kernel.after('find', () => stack.push('after-find')) - kernel.before('run', () => stack.push('before-run')) - kernel.after('run', () => stack.push('after-run')) - - kernel.onExit(() => { - assert.equal(kernel.exitCode, 1) - assert.equal( - kernel.error.message, - 'E_INVALID_FLAG: "log-level" flag expects a "numeric" value' - ) - }) - - await kernel.handle(['hello', 'world', '--log-level']) - assert.deepEqual(stack, ['before-find', 'after-find']) - }) -}) - -test.group('Kernel | command not found', () => { - test('raise error when command is missing', async ({ assert }) => { - assert.plan(2) - const kernel = new Kernel(setupApp()) - - kernel.onExit(() => { - assert.equal(kernel.exitCode, 1) - assert.equal(kernel.error.message, 'E_INVALID_COMMAND: "hello" is not a registered command') - }) - - await kernel.handle(['hello', 'world']) - }) - - test('run find hooks', async ({ assert }) => { - assert.plan(3) - - const kernel = new Kernel(setupApp()) - const stack: string[] = [] - - kernel.onExit(() => { - assert.equal(kernel.exitCode, 1) - assert.equal(kernel.error.message, 'E_INVALID_COMMAND: "hello" is not a registered command') - }) - - kernel.before('find', () => stack.push('before-find')) - kernel.after('find', () => stack.push('after-find')) - kernel.before('run', () => stack.push('before-run')) - kernel.after('run', () => stack.push('after-run')) - - await kernel.handle(['hello', 'world']) - assert.deepEqual(stack, ['before-find', 'after-find']) - }) - - test('run global flag handlers', async ({ assert }) => { - assert.plan(3) - - const kernel = new Kernel(setupApp()) - const stack: string[] = [] - - kernel.onExit(() => { - assert.equal(kernel.exitCode, 1) - assert.equal(kernel.error.message, 'E_INVALID_COMMAND: "hello" is not a registered command') - }) - - kernel.before('find', () => stack.push('before-find')) - kernel.after('find', () => stack.push('after-find')) - kernel.before('run', () => stack.push('before-run')) - kernel.after('run', () => stack.push('after-run')) - kernel.flag('env', () => stack.push('env-flag'), { - type: 'string', - }) - - await kernel.handle(['hello', 'world', '--env=foo']) - assert.deepEqual(stack, ['before-find', 'after-find', 'env-flag']) - }) -}) - -test.group('Kernel | subcommands', () => { - test('allow executing commands within commands', async ({ assert }) => { - assert.plan(4) - const kernel = new Kernel(setupApp()) - - class HelloCommand extends BaseCommand { - public static commandName = 'hello' - - @args.string() - public name: string - - public async run() { - assert.equal(this.name, 'world') - await this.kernel.exec('hi', ['world']) - } - } - - class HiCommand extends BaseCommand { - public static commandName = 'hi' - - @args.string() - public name: string - - public async run() { - assert.equal(this.name, 'world') - return 'world' - } - } - - kernel.register([HelloCommand, HiCommand]) - - kernel.onExit(() => { - assert.equal(kernel.exitCode, 0) - assert.isUndefined(kernel.error) - }) - - await kernel.handle(['hello', 'world']) - }) - - test('do not trigger exit when subcommand finishes and the main one is pending', async ({ - assert, - }) => { - assert.plan(4) - const kernel = new Kernel(setupApp()) - - class HelloCommand extends BaseCommand { - public static commandName = 'hello' - - @args.string() - public name: string - - public async run() { - assert.equal(this.name, 'world') - await this.kernel.exec('hi', ['world']) - setTimeout(() => { - this.kernel.exit(this) - }, 200) - } - } - - class HiCommand extends BaseCommand { - public static commandName = 'hi' - - @args.string() - public name: string - - public async run() { - assert.equal(this.name, 'world') - } - } - - kernel.register([HelloCommand, HiCommand]) - - kernel.onExit(() => { - assert.equal(kernel.exitCode, 0) - assert.isUndefined(kernel.error) - }) - - await kernel.handle(['hello', 'world']) - }) - - test('execute find hooks before the subcommand', async ({ assert }) => { - assert.plan(5) - const kernel = new Kernel(setupApp()) - - const stack: string[] = [] - - class HelloCommand extends BaseCommand { - public static commandName = 'hello' - - @args.string() - public name: string - - public async run() { - stack.push('hello-command') - assert.equal(this.name, 'world') - await this.kernel.exec('hi', ['world']) - } - } - - class HiCommand extends BaseCommand { - public static commandName = 'hi' - - @args.string() - public name: string - - public async run() { - stack.push('hi-command') - assert.equal(this.name, 'world') - } - } - - kernel.register([HelloCommand, HiCommand]) - - kernel.onExit(() => { - assert.equal(kernel.exitCode, 0) - assert.isUndefined(kernel.error) - }) - - kernel.before('find', () => stack.push('before-find')) - kernel.after('find', () => stack.push('after-find')) - - await kernel.handle(['hello', 'world']) - assert.deepEqual(stack, [ - 'before-find', - 'after-find', - 'hello-command', - 'before-find', - 'after-find', - 'hi-command', - ]) - }) - - test('execute run hooks before the subcommand', async ({ assert }) => { - assert.plan(5) - const kernel = new Kernel(setupApp()) - - const stack: string[] = [] - - class HelloCommand extends BaseCommand { - public static commandName = 'hello' - - @args.string() - public name: string - - public async run() { - stack.push('hello-command') - assert.equal(this.name, 'world') - await this.kernel.exec('hi', ['world']) - } - } - - class HiCommand extends BaseCommand { - public static commandName = 'hi' - - @args.string() - public name: string - - public async run() { - stack.push('hi-command') - assert.equal(this.name, 'world') - } - } - - kernel.register([HelloCommand, HiCommand]) - - kernel.onExit(() => { - assert.equal(kernel.exitCode, 0) - assert.isUndefined(kernel.error) - }) - - kernel.before('find', () => stack.push('before-find')) - kernel.after('find', () => stack.push('after-find')) - kernel.before('run', () => stack.push('before-run')) - kernel.after('run', () => stack.push('after-run')) - - await kernel.handle(['hello', 'world']) - assert.deepEqual(stack, [ - 'before-find', - 'after-find', - 'before-run', - 'hello-command', - 'before-find', - 'after-find', - 'before-run', - 'hi-command', - 'after-run', - 'after-run', - ]) - }) - - test('handle case when subcommand error is unhandled', async ({ assert }) => { - assert.plan(5) - const kernel = new Kernel(setupApp()) - - const stack: string[] = [] - - class HelloCommand extends BaseCommand { - public static commandName = 'hello' - - @args.string() - public name: string - - public async run() { - stack.push('hello-command') - assert.equal(this.name, 'world') - await this.kernel.exec('hi', ['world']) - } - } - - class HiCommand extends BaseCommand { - public static commandName = 'hi' - - @args.string() - public name: string - - public async run() { - stack.push('hi-command') - assert.equal(this.name, 'world') - throw new Error('hi failed') - } - } - - kernel.register([HelloCommand, HiCommand]) - - kernel.onExit(() => { - assert.equal(kernel.exitCode, 1) - assert.equal(kernel.error.message, 'hi failed') - }) - - kernel.before('find', () => stack.push('before-find')) - kernel.after('find', () => stack.push('after-find')) - kernel.before('run', () => stack.push('before-run')) - kernel.after('run', () => stack.push('after-run')) - - await kernel.handle(['hello', 'world']) - assert.deepEqual(stack, [ - 'before-find', - 'after-find', - 'before-run', - 'hello-command', - 'before-find', - 'after-find', - 'before-run', - 'hi-command', - 'after-run', - 'after-run', - ]) - }) - - test('handle case when subcommand error is handled', async ({ assert }) => { - assert.plan(6) - const kernel = new Kernel(setupApp()) - - const stack: string[] = [] - - class HelloCommand extends BaseCommand { - public static commandName = 'hello' - - @args.string() - public name: string - - public async run() { - stack.push('hello-command') - assert.equal(this.name, 'world') - - try { - await this.kernel.exec('hi', ['world']) - } catch (error) { - assert.equal(error.message, 'hi failed') - } - } - } - - class HiCommand extends BaseCommand { - public static commandName = 'hi' - - @args.string() - public name: string - - public async run() { - stack.push('hi-command') - assert.equal(this.name, 'world') - throw new Error('hi failed') - } - } - - kernel.register([HelloCommand, HiCommand]) - - kernel.onExit(() => { - assert.equal(kernel.exitCode, 0) - assert.isUndefined(kernel.error) - }) - - kernel.before('find', () => stack.push('before-find')) - kernel.after('find', () => stack.push('after-find')) - kernel.before('run', () => stack.push('before-run')) - kernel.after('run', () => stack.push('after-run')) - - await kernel.handle(['hello', 'world']) - assert.deepEqual(stack, [ - 'before-find', - 'after-find', - 'before-run', - 'hello-command', - 'before-find', - 'after-find', - 'before-run', - 'hi-command', - 'after-run', - 'after-run', - ]) - }) - - test('do not run global flags when executing subcommand', async ({ assert }) => { - assert.plan(5) - const kernel = new Kernel(setupApp()) - - const stack: string[] = [] - - class HelloCommand extends BaseCommand { - public static commandName = 'hello' - - @args.string() - public name: string - - public async run() { - stack.push('hello-command') - assert.equal(this.name, 'world') - await this.kernel.exec('hi', ['world', '--env=production']) - } - } - - class HiCommand extends BaseCommand { - public static commandName = 'hi' - - @args.string() - public name: string - - public async run() { - stack.push('hi-command') - assert.equal(this.name, 'world') - } - } - - kernel.register([HelloCommand, HiCommand]) - - kernel.onExit(() => { - assert.equal(kernel.exitCode, 0) - assert.isUndefined(kernel.error) - }) - - kernel.before('find', () => stack.push('before-find')) - kernel.after('find', () => stack.push('after-find')) - kernel.before('run', () => stack.push('before-run')) - kernel.after('run', () => stack.push('after-run')) - kernel.flag('env', () => stack.push('env-flag'), { - type: 'string', - }) - - await kernel.handle(['hello', 'world', '--env=production']) - assert.deepEqual(stack, [ - 'before-find', - 'after-find', - 'env-flag', - 'before-run', - 'hello-command', - 'before-find', - 'after-find', - 'before-run', - 'hi-command', - 'after-run', - 'after-run', - ]) - }) -}) diff --git a/test/kernel.spec.ts b/test/kernel.spec.ts deleted file mode 100644 index e274fe0..0000000 --- a/test/kernel.spec.ts +++ /dev/null @@ -1,2135 +0,0 @@ -/* - * @adonisjs/ace - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { test } from '@japa/runner' -import 'reflect-metadata' -import { join } from 'path' - -import { Kernel } from '../src/Kernel' -import { args } from '../src/Decorators/args' -import { flags } from '../src/Decorators/flags' -import { BaseCommand } from '../src/BaseCommand' -import { Application } from '@adonisjs/application' -import { ManifestLoader } from '../src/Manifest/Loader' -import { setupApp, fs, info } from '../test-helpers' - -test.group('Kernel | register', () => { - test('raise error when required argument comes after optional argument', ({ assert }) => { - class Greet extends BaseCommand { - public static commandName = 'greet' - - @args.string({ required: false }) - public name: string - - @args.string() - public age: string - - public async run() {} - } - - const app = setupApp() - const kernel = new Kernel(app) - const fn = () => kernel.register([Greet]) - assert.throws(fn, 'Optional argument "name" must be after the required argument "age"') - }) - - test('raise error when command name is missing', ({ assert }) => { - class Greet extends BaseCommand { - public async run() {} - } - - const app = setupApp() - const kernel = new Kernel(app) - const fn = () => kernel.register([Greet]) - assert.throws( - fn, - 'Invalid command "Greet". Make sure to define the static property "commandName"' - ) - }) - - test("raise error when spread argument isn't the last one", ({ assert }) => { - class Greet extends BaseCommand { - public static commandName = 'greet' - - @args.spread() - public files: string[] - - @args.string() - public name: string - - public async run() {} - } - - const app = setupApp() - const kernel = new Kernel(app) - const fn = () => kernel.register([Greet]) - assert.throws(fn, 'Spread argument "files" must be at last position') - }) - - test('register command', ({ assert }) => { - const app = setupApp() - const kernel = new Kernel(app) - - class Install extends BaseCommand { - public static commandName = 'install' - public async run() {} - } - - class Greet extends BaseCommand { - public static commandName = 'greet' - public async run() {} - } - - kernel.register([Install, Greet]) - assert.deepEqual(kernel.commands, { install: Install, greet: Greet }) - }) - - test('register command with aliases', ({ assert }) => { - const app = setupApp() - const kernel = new Kernel(app) - - class Install extends BaseCommand { - public static commandName = 'install' - public async run() {} - } - - class Greet extends BaseCommand { - public static commandName = 'greet' - public async run() {} - public static aliases = ['g', 'gr'] - } - - kernel.register([Install, Greet]) - assert.deepEqual(kernel.commands, { install: Install, greet: Greet }) - assert.deepEqual(kernel.aliases, { g: 'greet', gr: 'greet' }) - }) - - test('return command name suggestions for a given string', ({ assert }) => { - const app = setupApp() - const kernel = new Kernel(app) - - class Install extends BaseCommand { - public static commandName = 'install' - public async run() {} - } - - class Greet extends BaseCommand { - public static commandName = 'greet' - public async run() {} - } - - kernel.register([Install, Greet]) - assert.deepEqual(kernel.getSuggestions('itall'), ['install']) - }) - - test('return command alias suggestions for a given string', ({ assert }) => { - const app = setupApp() - const kernel = new Kernel(app) - - class Install extends BaseCommand { - public static commandName = 'install' - public async run() {} - } - - class Greet extends BaseCommand { - public static commandName = 'greet' - public static aliases = ['sayhi'] - public async run() {} - } - - kernel.register([Install, Greet]) - assert.deepEqual(kernel.getSuggestions('hi'), ['sayhi']) - }) - - test('return command name suggestions from manifest file', async ({ assert }) => { - const app = setupApp() - const kernel = new Kernel(app) - - const manifestLoader = new ManifestLoader([ - { - basePath: fs.basePath, - manifestAbsPath: join(fs.basePath, 'ace-manifest.json'), - }, - ]) - - await fs.add( - 'ace-manifest.json', - JSON.stringify({ - greet: { - commandName: 'greet', - commandPath: './Commands/Greet.ts', - }, - }) - ) - - await fs.add( - 'Commands/Greet.ts', - `export default class Greet { - public static commandName = 'greet' - public static boot () {} - }` - ) - - kernel.useManifest(manifestLoader) - await kernel.preloadManifest() - - assert.deepEqual(kernel.getSuggestions('eet'), ['greet']) - - await fs.cleanup() - }) - - test('change camelCase alias name to dashcase', ({ assert }) => { - class Greet extends BaseCommand { - public static commandName = 'greet' - - @flags.boolean() - public isAdmin: boolean - - public async run() {} - } - - assert.deepEqual(Greet.flags[0].name, 'is-admin') - }) -}) - -test.group('Kernel | find', () => { - test('find relevant command from the commands list', async ({ assert }) => { - class Greet extends BaseCommand { - public static commandName = 'greet' - public async run() {} - } - - const app = setupApp() - const kernel = new Kernel(app) - kernel.register([Greet]) - - const greet = await kernel.find(['greet']) - assert.deepEqual(greet, Greet) - }) - - test('find relevant command from the commands aliases', async ({ assert }) => { - class Greet extends BaseCommand { - public static commandName = 'greet' - public static aliases = ['sayhi'] - public async run() {} - } - - const app = setupApp() - const kernel = new Kernel(app) - kernel.register([Greet]) - - const greet = await kernel.find(['sayhi']) - assert.deepEqual(greet, Greet) - }) - - test('return null when unable to find command', async ({ assert }) => { - const app = setupApp() - const kernel = new Kernel(app) - const greet = await kernel.find(['greet']) - - assert.isNull(greet) - }) - - test('find command from manifest when manifestCommands exists', async ({ assert }) => { - const app = setupApp() - const kernel = new Kernel(app) - const manifestLoader = new ManifestLoader([ - { - basePath: fs.basePath, - manifestAbsPath: join(fs.basePath, 'ace-manifest.json'), - }, - ]) - - await fs.add( - 'ace-manifest.json', - JSON.stringify({ - greet: { - commandName: 'greet', - commandPath: './Commands/Greet.ts', - }, - }) - ) - - await fs.add( - 'Commands/Greet.ts', - `export default class Greet { - public static commandName = 'greet' - public static args = [] - public static flags = [] - public static boot() {} - }` - ) - - kernel.useManifest(manifestLoader) - await kernel.preloadManifest() - - const greet = await kernel.find(['greet']) - assert.equal(greet!.name, 'Greet') - - await fs.cleanup() - }) - - test('find command from manifest aliases', async ({ assert }) => { - const app = setupApp() - const kernel = new Kernel(app) - const manifestLoader = new ManifestLoader([ - { - basePath: fs.basePath, - manifestAbsPath: join(fs.basePath, 'ace-manifest.json'), - }, - ]) - - await fs.add( - 'ace-manifest.json', - JSON.stringify({ - commands: { - greet: { - commandName: 'greet', - commandPath: './Commands/Greet.ts', - }, - }, - aliases: { - sayhi: 'greet', - }, - }) - ) - - await fs.add( - 'Commands/Greet.ts', - `export default class Greet { - public static commandName = 'greet' - public static args = [] - public static flags = [] - public static boot() {} - }` - ) - - kernel.useManifest(manifestLoader) - await kernel.preloadManifest() - - const greet = await kernel.find(['sayhi']) - assert.equal(greet!.name, 'Greet') - - await fs.cleanup() - }) - - test('define manifest command alias inside adonisjs rc file', async ({ assert }) => { - const app = setupApp() - app.rcFile.commandsAliases = { sayhi: 'greet' } - const kernel = new Kernel(app) - const manifestLoader = new ManifestLoader([ - { - basePath: fs.basePath, - manifestAbsPath: join(fs.basePath, 'ace-manifest.json'), - }, - ]) - - await fs.add( - 'ace-manifest.json', - JSON.stringify({ - commands: { - greet: { - commandName: 'greet', - commandPath: './Commands/Greet.ts', - }, - }, - aliases: {}, - }) - ) - - await fs.add( - 'Commands/Greet.ts', - `export default class Greet { - public static commandName = 'greet' - public static args = [] - public static flags = [] - public static boot() {} - }` - ) - - kernel.useManifest(manifestLoader) - await kernel.preloadManifest() - - const greet = await kernel.find(['sayhi']) - assert.equal(greet!.name, 'Greet') - - await fs.cleanup() - }) - - test('register commands along with manifest', async ({ assert }) => { - const app = setupApp() - const kernel = new Kernel(app) - const manifestLoader = new ManifestLoader([ - { - basePath: fs.basePath, - manifestAbsPath: join(fs.basePath, 'ace-manifest.json'), - }, - ]) - - await fs.add( - 'ace-manifest.json', - JSON.stringify({ - commands: { - greet: { - commandName: 'greet', - commandPath: './Commands/Greet.ts', - }, - }, - aliases: {}, - }) - ) - - await fs.add( - 'Commands/Greet.ts', - `export default class Greet { - public static commandName = 'greet' - public static args = [] - public static flags = [] - public static boot() {} - }` - ) - - kernel.useManifest(manifestLoader) - await kernel.preloadManifest() - - kernel.register([ - class Help extends BaseCommand { - public static commandName = 'help' - public async run() {} - }, - ]) - - const greet = await kernel.find(['greet']) - assert.equal(greet!.commandName, 'greet') - - const help = await kernel.find(['help']) - assert.equal(help!.commandName, 'help') - - await fs.cleanup() - }) - - test('execute before and after hook when finding command from manifest', async ({ assert }) => { - assert.plan(3) - - const app = setupApp() - const kernel = new Kernel(app) - const manifestLoader = new ManifestLoader([ - { - basePath: fs.basePath, - manifestAbsPath: join(fs.basePath, 'ace-manifest.json'), - }, - ]) - - await fs.add( - 'ace-manifest.json', - JSON.stringify({ - greet: { - commandName: 'greet', - commandPath: './Commands/Greet.ts', - }, - }) - ) - - await fs.add( - 'Commands/Greet.ts', - `export default class Greet { - public static commandName = 'greet' - public static args = [] - public static flags = [] - public static boot() {} - }` - ) - - kernel.useManifest(manifestLoader) - - kernel.before('find', (command) => { - assert.equal(command!.commandName, 'greet') - }) - - kernel.after('find', (command) => { - assert.equal(command!.commandName, 'greet') - assert.equal(command!['name'], 'Greet') // It is command constructor - }) - - await kernel.preloadManifest() - await kernel.find(['greet']) - await fs.cleanup() - }) - - test('execute before and after hook when finding command from manifest aliases', async ({ - assert, - }) => { - assert.plan(3) - - const app = setupApp() - const kernel = new Kernel(app) - const manifestLoader = new ManifestLoader([ - { - basePath: fs.basePath, - manifestAbsPath: join(fs.basePath, 'ace-manifest.json'), - }, - ]) - - await fs.add( - 'ace-manifest.json', - JSON.stringify({ - commands: { - greet: { - commandName: 'greet', - commandPath: './Commands/Greet.ts', - }, - }, - aliases: { - sayhi: 'greet', - }, - }) - ) - - await fs.add( - 'Commands/Greet.ts', - `export default class Greet { - public static commandName = 'greet' - public static args = [] - public static flags = [] - public static boot() {} - }` - ) - - kernel.useManifest(manifestLoader) - - kernel.before('find', (command) => { - assert.equal(command!.commandName, 'greet') - }) - - kernel.after('find', (command) => { - assert.equal(command!.commandName, 'greet') - assert.equal(command!['name'], 'Greet') // It is command constructor - }) - - await kernel.preloadManifest() - await kernel.find(['sayhi']) - await fs.cleanup() - }) - - test('pass null to before and after hook when unable to find command', async ({ assert }) => { - assert.plan(3) - - const app = setupApp() - const kernel = new Kernel(app) - kernel.before('find', (command) => assert.isNull(command)) - kernel.after('find', (command) => assert.isNull(command)) - - const greet = await kernel.find(['greet']) - - assert.isNull(greet) - }) - - test('pass command constructor to before and after hook found command from local commands', async ({ - assert, - }) => { - assert.plan(3) - class Greet extends BaseCommand { - public static commandName = 'greet' - public async run() {} - } - - const app = setupApp() - const kernel = new Kernel(app) - kernel.register([Greet]) - - kernel.before('find', (command) => assert.deepEqual(command, Greet)) - kernel.after('find', (command) => assert.deepEqual(command, Greet)) - - const greet = await kernel.find(['greet']) - assert.deepEqual(greet, Greet) - }) - - test('pass command constructor to before and after hook found command from local aliases', async ({ - assert, - }) => { - assert.plan(3) - class Greet extends BaseCommand { - public static commandName = 'greet' - public static aliases = ['sayhi'] - public async run() {} - } - - const app = setupApp() - const kernel = new Kernel(app) - kernel.register([Greet]) - - kernel.before('find', (command) => assert.deepEqual(command, Greet)) - kernel.after('find', (command) => assert.deepEqual(command, Greet)) - - const greet = await kernel.find(['sayhi']) - assert.deepEqual(greet, Greet) - }) -}) - -test.group('Kernel | exec', () => { - test('raise exception when required argument is missing', async ({ assert }, done) => { - assert.plan(3) - - class Greet extends BaseCommand { - public static commandName = 'greet' - - @args.string() - public name: string - - public async run() {} - } - - const app = setupApp() - const kernel = new Kernel(app) - kernel.register([Greet]) - - const argv = ['greet'] - - kernel.onExit(() => { - assert.equal(kernel.error.message, 'E_MISSING_ARGUMENT: Missing required argument "name"') - assert.equal(kernel.error.argumentName, 'name') - assert.deepEqual(kernel.error.command, Greet) - done() - }) - - await kernel.handle(argv) - }).waitForDone() - - test('work fine when argument is missing and is optional', async ({ assert }, done) => { - assert.plan(1) - - class Greet extends BaseCommand { - public static commandName = 'greet' - - @args.string({ required: false }) - public name: string - - public async run() { - assert.deepEqual(this.parsed, { _: [] }) - } - } - - const app = setupApp() - const kernel = new Kernel(app) - kernel.register([Greet]) - - kernel.onExit(() => { - if (kernel.error) { - done(kernel.error) - } else { - done() - } - }) - - const argv = ['greet'] - await kernel.handle(argv) - }).waitForDone() - - test('work fine when required argument is defined', async ({ assert }, done) => { - assert.plan(2) - - class Greet extends BaseCommand { - public static commandName = 'greet' - - @args.string() - public name: string - - public async run() { - assert.deepEqual(this.parsed, { _: ['virk'] }) - assert.equal(this.name, 'virk') - } - } - - const app = setupApp() - const kernel = new Kernel(app) - kernel.register([Greet]) - - kernel.onExit(() => { - if (kernel.error) { - done(kernel.error) - } else { - done() - } - }) - - const argv = ['greet', 'virk'] - await kernel.handle(argv) - }).waitForDone() - - test('define spread arguments', async ({ assert }, done) => { - assert.plan(2) - - class Greet extends BaseCommand { - public static commandName = 'greet' - - @args.spread() - public files: string[] - - public async run() { - assert.deepEqual(this.parsed, { _: ['foo.js', 'bar.js'] }) - assert.deepEqual(this.files, ['foo.js', 'bar.js']) - } - } - - const app = setupApp() - const kernel = new Kernel(app) - kernel.register([Greet]) - - kernel.onExit(() => { - if (kernel.error) { - done(kernel.error) - } else { - done() - } - }) - - const argv = ['greet', 'foo.js', 'bar.js'] - await kernel.handle(argv) - }).waitForDone() - - test('define spread arguments with regular arguments', async ({ assert }, done) => { - assert.plan(4) - - class Greet extends BaseCommand { - public static commandName = 'greet' - - @args.string() - public name: string - - @args.string() - public age: string - - @args.spread() - public files: string[] - - public async run() { - assert.deepEqual(this.parsed, { _: ['virk', '22', 'foo.js', 'bar.js'] }) - assert.equal(this.name, 'virk') - assert.equal(this.age, '22') - assert.deepEqual(this.files, ['foo.js', 'bar.js']) - } - } - - const app = setupApp() - const kernel = new Kernel(app) - kernel.register([Greet]) - - kernel.onExit(() => { - if (kernel.error) { - done(kernel.error) - } else { - done() - } - }) - - const argv = ['greet', 'virk', '22', 'foo.js', 'bar.js'] - await kernel.handle(argv) - }).waitForDone() - - test('allow spread arguments to be optional', async ({ assert }, done) => { - assert.plan(4) - - class Greet extends BaseCommand { - public static commandName = 'greet' - - @args.string() - public name: string - - @args.string() - public age: string - - @args.spread({ required: false }) - public files: string[] - - public async run() { - assert.deepEqual(this.parsed, { _: ['virk', '22'] }) - assert.equal(this.name, 'virk') - assert.equal(this.age, '22') - assert.deepEqual(this.files, []) - } - } - - const app = setupApp() - const kernel = new Kernel(app) - kernel.register([Greet]) - - kernel.onExit(() => { - if (kernel.error) { - done(kernel.error) - } else { - done() - } - }) - - const argv = ['greet', 'virk', '22'] - await kernel.handle(argv) - }).waitForDone() - - test('allow only spread argument to be optional', async ({ assert }, done) => { - assert.plan(2) - - class Greet extends BaseCommand { - public static commandName = 'greet' - - @args.spread({ required: false }) - public files: string[] - - public async run() { - assert.deepEqual(this.parsed, { _: [] }) - assert.deepEqual(this.files, []) - } - } - - const app = setupApp() - const kernel = new Kernel(app) - kernel.register([Greet]) - - kernel.onExit(() => { - if (kernel.error) { - done(kernel.error) - } else { - done() - } - }) - - const argv = ['greet'] - await kernel.handle(argv) - }).waitForDone() - - test('disallow required arg after optional arg', async ({ assert }) => { - class Greet extends BaseCommand { - public static commandName = 'greet' - - @args.string({ required: false }) - public name: string - - @args.string({ required: true }) - public age: string - - public async run() {} - } - - const app = setupApp() - const kernel = new Kernel(app) - assert.throws( - () => kernel.register([Greet]), - 'Optional argument "name" must be after the required argument "age"' - ) - }) - - test('set arguments and flags', async ({ assert }, done) => { - assert.plan(3) - - class Greet extends BaseCommand { - public static commandName = 'greet' - - @args.string() - public name: string - - @flags.boolean() - public admin: boolean - - public async run() { - assert.deepEqual(this.parsed, { _: ['virk'], admin: true }) - assert.equal(this.name, 'virk') - assert.isTrue(this.admin) - } - } - - const app = setupApp() - const kernel = new Kernel(app) - kernel.register([Greet]) - - kernel.onExit(() => { - if (kernel.error) { - done(kernel.error) - } else { - done() - } - }) - - const argv = ['greet', 'virk', '--admin'] - await kernel.handle(argv) - }).waitForDone() - - test('set arguments and flags when flag is defined with = sign', async ({ assert }, done) => { - assert.plan(3) - - class Greet extends BaseCommand { - public static commandName = 'greet' - - @args.string() - public name: string - - @flags.boolean() - public admin: boolean - - public async run() { - assert.deepEqual(this.parsed, { _: ['virk'], admin: true }) - assert.equal(this.name, 'virk') - assert.isTrue(this.admin) - } - } - - const app = setupApp() - const kernel = new Kernel(app) - kernel.register([Greet]) - - kernel.onExit(() => { - if (kernel.error) { - done(kernel.error) - } else { - done() - } - }) - - const argv = ['greet', 'virk', '--admin=true'] - await kernel.handle(argv) - }).waitForDone() - - test('set arguments and flags when flag alias is passed', async ({ assert }, done) => { - assert.plan(3) - - class Greet extends BaseCommand { - public static commandName = 'greet' - - @args.string() - public name: string - - @flags.boolean({ alias: 'a' }) - public admin: boolean - - public async run() { - assert.deepEqual(this.parsed, { _: ['virk'], admin: true, a: true }) - assert.equal(this.name, 'virk') - assert.isTrue(this.admin) - } - } - - const app = setupApp() - const kernel = new Kernel(app) - kernel.register([Greet]) - - kernel.onExit(() => { - if (kernel.error) { - done(kernel.error) - } else { - done() - } - }) - - const argv = ['greet', 'virk', '-a'] - await kernel.handle(argv) - }).waitForDone() - - test("set flag when it's name is different from command property", async ({ assert }, done) => { - assert.plan(3) - - class Greet extends BaseCommand { - public static commandName = 'greet' - - @args.string() - public name: string - - @flags.boolean({ name: 'admin', alias: 'a' }) - public isAdmin: boolean - - public async run() { - assert.deepEqual(this.parsed, { _: ['virk'], admin: true, a: true }) - assert.equal(this.name, 'virk') - assert.isTrue(this.isAdmin) - } - } - - const app = setupApp() - const kernel = new Kernel(app) - kernel.register([Greet]) - - kernel.onExit(() => { - if (kernel.error) { - done(kernel.error) - } else { - done() - } - }) - - const argv = ['greet', 'virk', '-a'] - await kernel.handle(argv) - }).waitForDone() - - test('parse boolean flags as boolean always', async ({ assert }, done) => { - assert.plan(3) - - class Greet extends BaseCommand { - public static commandName = 'greet' - - @args.string() - public name: string - - @flags.boolean() - public admin: boolean - - public async run() { - assert.deepEqual(this.parsed, { _: ['virk'], admin: true }) - assert.equal(this.name, 'virk') - assert.isTrue(this.admin) - } - } - - const app = setupApp() - const kernel = new Kernel(app) - kernel.register([Greet]) - - kernel.onExit(() => { - if (kernel.error) { - done(kernel.error) - } else { - done() - } - }) - - const argv = ['greet', 'virk', '--admin=true'] - await kernel.handle(argv) - }).waitForDone() - - test('parse boolean flags as boolean always also when aliases are defined', async ({ - assert, - }, done) => { - assert.plan(3) - - class Greet extends BaseCommand { - public static commandName = 'greet' - - @args.string() - public name: string - - @flags.boolean({ alias: 'a' }) - public admin: boolean - - public async run() { - assert.deepEqual(this.parsed, { _: ['virk'], admin: true, a: true }) - assert.equal(this.name, 'virk') - assert.isTrue(this.admin) - } - } - - const app = setupApp() - const kernel = new Kernel(app) - kernel.register([Greet]) - - kernel.onExit(() => { - if (kernel.error) { - done(kernel.error) - } else { - done() - } - }) - - const argv = ['greet', 'virk', '-a=true'] - await kernel.handle(argv) - }).waitForDone() - - test('do not override default value when flag is not defined', async ({ assert }, done) => { - assert.plan(3) - - class Greet extends BaseCommand { - public static commandName = 'greet' - - @args.string() - public name: string - - @flags.boolean({ alias: 'a' }) - public admin: boolean = false - - public async run() { - assert.deepEqual(this.parsed, { _: ['virk'] }) - assert.equal(this.name, 'virk') - assert.isFalse(this.admin) - } - } - - const app = setupApp() - const kernel = new Kernel(app) - kernel.register([Greet]) - - kernel.onExit(() => { - if (kernel.error) { - done(kernel.error) - } else { - done() - } - }) - - const argv = ['greet', 'virk'] - await kernel.handle(argv) - }).waitForDone() - - test('do not overwrite default value defined on the instance property', async ({ - assert, - }, done) => { - assert.plan(3) - - class Greet extends BaseCommand { - public static commandName = 'greet' - - @args.string() - public name: string - - @flags.string() - public connection: string = 'foo' - - public async run() { - assert.deepEqual(this.parsed, { _: ['virk'] }) - assert.equal(this.name, 'virk') - assert.equal(this.connection, 'foo') - } - } - - const app = setupApp() - const kernel = new Kernel(app) - kernel.register([Greet]) - - kernel.onExit(() => { - if (kernel.error) { - done(kernel.error) - } else { - done() - } - }) - - const argv = ['greet', 'virk'] - await kernel.handle(argv) - }).waitForDone() - - test('parse flags as array when type is set to array', async ({ assert }, done) => { - assert.plan(3) - - class Greet extends BaseCommand { - public static commandName = 'greet' - - @args.string() - public name: string - - @flags.array() - public files: string[] - - public async run() { - assert.deepEqual(this.parsed, { _: ['virk'], files: ['foo.js'] }) - assert.equal(this.name, 'virk') - assert.deepEqual(this.files, ['foo.js']) - } - } - - const app = setupApp() - const kernel = new Kernel(app) - kernel.register([Greet]) - - kernel.onExit(() => { - if (kernel.error) { - done(kernel.error) - } else { - done() - } - }) - - const argv = ['greet', 'virk', '--files=foo.js'] - await kernel.handle(argv) - }).waitForDone() - - test('register global flags', async ({ assert }, done) => { - assert.plan(2) - - const app = setupApp() - const kernel = new Kernel(app) - kernel.flag( - 'env', - (env, parsed) => { - assert.equal(env, 'production') - assert.deepEqual(parsed, { _: [], env: 'production' }) - }, - { type: 'string' } - ) - - kernel.onExit(() => { - if (kernel.error) { - done(kernel.error) - } else { - done() - } - }) - - const argv = ['--env=production'] - await kernel.handle(argv) - }).waitForDone() - - test('register global boolean flags', async ({ assert }, done) => { - assert.plan(2) - - const app = setupApp() - const kernel = new Kernel(app) - kernel.flag( - 'ansi', - (ansi, parsed) => { - assert.equal(ansi, true) - assert.deepEqual(parsed, { _: [], ansi: true }) - }, - {} - ) - - kernel.onExit(() => { - if (kernel.error) { - done(kernel.error) - } else { - done() - } - }) - - const argv = ['--ansi'] - await kernel.handle(argv) - }).waitForDone() - - test('do not execute string global flag when flag is not defined', async (_, done) => { - const app = setupApp() - const kernel = new Kernel(app) - kernel.flag( - 'env', - () => { - throw new Error('Not expected to be called') - }, - { type: 'string' } - ) - - const argv = [] - - kernel.onExit(() => { - if (kernel.error) { - done(kernel.error) - } else { - done() - } - }) - - await kernel.handle(argv) - }).waitForDone() - - test('do not execute num array type global flag when flag is not defined', async (_, done) => { - const app = setupApp() - const kernel = new Kernel(app) - kernel.flag( - 'env', - () => { - throw new Error('Not expected to be called') - }, - { type: 'numArray' } - ) - - kernel.onExit(() => { - if (kernel.error) { - done(kernel.error) - } else { - done() - } - }) - - const argv = [] - await kernel.handle(argv) - }).waitForDone() - - test('pass command instance to the global flag, when flag is defined on a command', async ({ - assert, - }, done) => { - assert.plan(3) - const app = setupApp() - const kernel = new Kernel(app) - - class Greet extends BaseCommand { - public static commandName = 'greet' - - @args.string() - public name: string - - public async run() {} - } - - kernel.register([Greet]) - - kernel.flag( - 'env', - (env, parsed, command) => { - assert.equal(env, 'production') - assert.deepEqual(parsed, { _: ['virk'], env: 'production' }) - assert.deepEqual(command, Greet) - }, - { type: 'string' } - ) - - kernel.onExit(() => { - if (kernel.error) { - done(kernel.error) - } else { - done() - } - }) - - const argv = ['greet', 'virk', '--env=production'] - await kernel.handle(argv) - }).waitForDone() - - test('define arg name different from property name', async ({ assert }, done) => { - assert.plan(2) - - class Greet extends BaseCommand { - public static commandName = 'greet' - - @args.string({ name: 'theName' }) - public name: string - - public async run() { - assert.deepEqual(this.parsed, { _: ['virk'] }) - assert.equal(this.name, 'virk') - } - } - - const app = setupApp() - const kernel = new Kernel(app) - kernel.register([Greet]) - - kernel.onExit(() => { - if (kernel.error) { - done(kernel.error) - } else { - done() - } - }) - - const argv = ['greet', 'virk'] - await kernel.handle(argv) - }).waitForDone() - - test('define flag name different from property name', async ({ assert }, done) => { - assert.plan(2) - - class Greet extends BaseCommand { - public static commandName = 'greet' - - @flags.boolean({ name: 'isAdmin' }) - public admin: boolean - - public async run() { - assert.deepEqual(this.parsed, { _: [], isAdmin: true }) - assert.isTrue(this.admin) - } - } - - const app = setupApp() - const kernel = new Kernel(app) - kernel.register([Greet]) - - kernel.onExit(() => { - if (kernel.error) { - done(kernel.error) - } else { - done() - } - }) - - const argv = ['greet', '--isAdmin'] - await kernel.handle(argv) - }).waitForDone() - - test('execute before and after run hooks', async ({ assert }, done) => { - assert.plan(2) - - class Greet extends BaseCommand { - public static commandName = 'greet' - - @flags.boolean({ name: 'isAdmin' }) - public admin: boolean - - public async run() {} - } - - const app = setupApp() - const kernel = new Kernel(app) - kernel.before('run', (command) => { - assert.instanceOf(command, Greet) - }) - - kernel.after('run', (command) => { - assert.instanceOf(command, Greet) - }) - - kernel.register([Greet]) - - kernel.onExit(() => { - if (kernel.error) { - done(kernel.error) - } else { - done() - } - }) - - const argv = ['greet'] - await kernel.handle(argv) - }).waitForDone() - - test('execute before and after run hooks even when command raises an exception', async ({ - assert, - }, done) => { - assert.plan(5) - - class Greet extends BaseCommand { - public static commandName = 'greet' - - @flags.boolean({ name: 'isAdmin' }) - public admin: boolean - - public async run() { - throw new Error('Boom') - } - } - - const app = setupApp() - const kernel = new Kernel(app) - kernel.before('run', (command) => { - assert.instanceOf(command, Greet) - }) - - kernel.after('run', (command) => { - assert.instanceOf(command, Greet) - assert.equal(command.error!.message, 'Boom') - }) - - kernel.register([Greet]) - - kernel.onExit(() => { - assert.equal(kernel.exitCode, 1) - assert.equal(kernel.error.message, 'Boom') - done() - }) - - const argv = ['greet'] - await kernel.handle(argv) - }).waitForDone() - - test('isMain should be false when command is called using exec', async ({ assert }) => { - assert.plan(2) - - class Greet extends BaseCommand { - public static commandName = 'greet' - - @flags.boolean({ name: 'isAdmin' }) - public admin: boolean - - public async run() { - assert.isFalse(this.isMain) - } - } - - const app = setupApp() - const kernel = new Kernel(app) - kernel.register([Greet]) - const greet = await kernel.exec('greet', []) - assert.instanceOf(greet, Greet) - }) - - test('do not overwrite default value when argument value is not defined', async ({ - assert, - }, done) => { - assert.plan(2) - - class Greet extends BaseCommand { - public static commandName = 'greet' - - @args.string({ required: false }) - public name: string = 'foo' - - public async run() { - assert.deepEqual(this.parsed, { _: [] }) - assert.equal(this.name, 'foo') - } - } - - const app = setupApp() - const kernel = new Kernel(app) - kernel.register([Greet]) - - kernel.onExit(() => { - if (kernel.error) { - done(kernel.error) - } else { - done() - } - }) - - const argv = ['greet'] - await kernel.handle(argv) - }).waitForDone() - - test('do not overwrite default value when spread argument value is not defined', async ({ - assert, - }, done) => { - assert.plan(3) - - class Greet extends BaseCommand { - public static commandName = 'greet' - - @args.string() - public name: string - - @args.spread({ required: false }) - public files: string[] = ['foo'] - - public async run() { - assert.deepEqual(this.parsed, { _: ['virk'] }) - assert.equal(this.name, 'virk') - assert.deepEqual(this.files, ['foo']) - } - } - - const app = setupApp() - const kernel = new Kernel(app) - kernel.register([Greet]) - - kernel.onExit(() => { - if (kernel.error) { - done(kernel.error) - } else { - done() - } - }) - - const argv = ['greet', 'virk'] - await kernel.handle(argv) - }).waitForDone() -}) - -test.group('Kernel | runCommand', () => { - test('test logs', async ({ assert }) => { - assert.plan(1) - - class Greet extends BaseCommand { - public static commandName = 'greet' - - @args.string() - public name: string - - public async run() { - this.logger.info(`Hello ${this.name}`) - } - } - - const app = setupApp() - const kernel = new Kernel(app).mockConsoleOutput() - - const commandInstance = new Greet(app, kernel) - commandInstance.name = 'virk' - await commandInstance.exec() - - assert.deepEqual(commandInstance.ui.testingRenderer.logs, [ - { - message: `${info} Hello virk`, - stream: 'stdout', - }, - ]) - }) - - test('test input prompt', async ({ assert }) => { - assert.plan(1) - - class Greet extends BaseCommand { - public static commandName = 'greet' - - @args.string() - public name: string - - public async run() { - const username = await this.prompt.ask("What's your username?", { - name: 'username', - }) - this.logger.info(username) - } - } - - const app = setupApp() - - const kernel = new Kernel(app).mockConsoleOutput() - const commandInstance = new Greet(app, kernel) - - /** - * Responding to prompt programatically - */ - commandInstance.prompt.on('prompt', (prompt) => { - prompt.answer('virk') - }) - - await commandInstance.exec() - - assert.deepEqual(commandInstance.ui.testingRenderer.logs, [ - { - message: `${info} virk`, - stream: 'stdout', - }, - ]) - }) - - test('test input prompt validation', async ({ assert }) => { - assert.plan(2) - - class Greet extends BaseCommand { - public static commandName = 'greet' - - @args.string() - public name: string - - public async run() { - const username = await this.prompt.ask("What's your username?", { - name: 'username', - validate(value) { - return !!value - }, - }) - - this.logger.info(username) - } - } - - const app = setupApp() - - const kernel = new Kernel(app).mockConsoleOutput() - const commandInstance = new Greet(app, kernel) - - /** - * Responding to prompt programatically - */ - commandInstance.prompt.on('prompt', (prompt) => { - prompt.answer('') - }) - - commandInstance.prompt.on('prompt:error', (message) => { - assert.equal(message, 'Enter the value') - }) - - await commandInstance.exec() - assert.deepEqual(commandInstance.ui.testingRenderer.logs, [ - { - message: `${info} `, - stream: 'stdout', - }, - ]) - }) - - test('test choice prompt', async ({ assert }) => { - assert.plan(1) - - class Greet extends BaseCommand { - public static commandName = 'greet' - - @args.string() - public name: string - - public async run() { - const client = await this.prompt.choice('Select the installation client', ['npm', 'yarn']) - this.logger.info(client) - } - } - - const app = setupApp() - - const kernel = new Kernel(app).mockConsoleOutput() - const commandInstance = new Greet!(app, kernel) - - /** - * Responding to prompt programatically - */ - commandInstance.prompt.on('prompt', (prompt) => { - prompt.select(0) - }) - - await commandInstance.exec() - assert.deepEqual(commandInstance.ui.testingRenderer.logs, [ - { - message: `${info} npm`, - stream: 'stdout', - }, - ]) - }) - - test('test choice prompt validation', async ({ assert }) => { - assert.plan(2) - - class Greet extends BaseCommand { - public static commandName = 'greet' - - @args.string() - public name: string - - public async run() { - const client = await this.prompt.choice('Select the installation client', ['npm', 'yarn'], { - validate(answer) { - return !!answer - }, - }) - this.logger.info(client) - } - } - - const app = setupApp() - - const kernel = new Kernel(app).mockConsoleOutput() - const commandInstance = new Greet(app, kernel) - - /** - * Responding to prompt programatically - */ - commandInstance.prompt.on('prompt', (prompt) => { - prompt.answer('') - }) - - commandInstance.prompt.on('prompt:error', (message) => { - assert.equal(message, 'Enter the value') - }) - - await commandInstance.exec() - assert.deepEqual(commandInstance.ui.testingRenderer.logs, [ - { - message: `${info} `, - stream: 'stdout', - }, - ]) - }) - - test('test multiple prompts', async ({ assert }) => { - assert.plan(1) - process.env.CLI_UI_IS_TESTING = 'true' - - class Greet extends BaseCommand { - public static commandName = 'greet' - - @args.string() - public name: string - - public async run() { - const clients = await this.prompt.multiple('Select the installation client', [ - 'npm', - 'yarn', - ]) - this.logger.info(clients.join(',')) - } - } - - const app = setupApp() - - const kernel = new Kernel(app).mockConsoleOutput() - const commandInstance = new Greet(app, kernel) - - /** - * Responding to prompt programatically - */ - commandInstance.prompt.on('prompt', (prompt) => { - prompt.select(0) - }) - - await commandInstance.exec() - assert.deepEqual(commandInstance.ui.testingRenderer.logs, [ - { - message: `${info} npm`, - stream: 'stdout', - }, - ]) - }) - - test('test multiple prompt validation', async ({ assert }) => { - assert.plan(2) - - class Greet extends BaseCommand { - public static commandName = 'greet' - - @args.string() - public name: string - - public async run() { - const client = await this.prompt.multiple( - 'Select the installation client', - ['npm', 'yarn'], - { - validate(answer) { - return answer.length > 0 - }, - } - ) - - this.logger.info(client.join(',')) - } - } - - const app = setupApp() - - const kernel = new Kernel(app).mockConsoleOutput() - const commandInstance = new Greet(app, kernel) - - /** - * Responding to prompt programatically - */ - commandInstance.prompt.on('prompt', (prompt) => { - prompt.answer([]) - }) - - commandInstance.prompt.on('prompt:error', (message) => { - assert.equal(message, 'Enter the value') - }) - - await commandInstance.exec() - assert.deepEqual(commandInstance.ui.testingRenderer.logs, [ - { - message: `${info} `, - stream: 'stdout', - }, - ]) - }) - - test('test toggle prompt', async ({ assert }) => { - assert.plan(1) - process.env.CLI_UI_IS_TESTING = 'true' - - class Greet extends BaseCommand { - public static commandName = 'greet' - - @args.string() - public name: string - - public async run() { - const deleteFile = await this.prompt.toggle('Delete the file?', ['Yep', 'Nope']) - this.logger.info(deleteFile ? 'Yep' : 'Nope') - } - } - - const app = setupApp() - - const kernel = new Kernel(app).mockConsoleOutput() - const commandInstance = new Greet(app, kernel) - - /** - * Responding to prompt programatically - */ - commandInstance.prompt.on('prompt', (prompt) => { - prompt.accept() - }) - - await commandInstance.exec() - assert.deepEqual(commandInstance.ui.testingRenderer.logs, [ - { - message: `${info} Yep`, - stream: 'stdout', - }, - ]) - }) -}) - -test.group('Kernel | exec', () => { - test('exec command by name', async ({ assert }) => { - assert.plan(1) - - class Foo extends BaseCommand { - public static commandName = 'foo' - public async run() { - assert.isTrue(true) - } - } - - const app = setupApp() - const kernel = new Kernel(app) - kernel.register([Foo]) - - await kernel.exec('foo', []) - }) - - test('exec command by alias', async ({ assert }) => { - assert.plan(1) - - class Foo extends BaseCommand { - public static commandName = 'foo' - public static aliases = ['bar'] - public async run() { - assert.isTrue(true) - } - } - - const app = setupApp() - const kernel = new Kernel(app) - kernel.register([Foo]) - - await kernel.exec('bar', []) - }) - - test('pass arguments and flags to command using exec', async ({ assert }) => { - assert.plan(2) - - class Foo extends BaseCommand { - public static commandName = 'foo' - - @args.string() - public name: string - - @flags.boolean() - public isAdmin: boolean - - public async run() { - assert.isTrue(this.isAdmin) - assert.equal(this.name, 'virk') - } - } - - const app = setupApp() - const kernel = new Kernel(app) - kernel.register([Foo]) - - await kernel.exec('foo', ['virk', '--is-admin=true']) - }) - - test('exec find and run hooks for the command using exec', async ({ assert }) => { - assert.plan(5) - - class Foo extends BaseCommand { - public static commandName = 'foo' - public async run() { - assert.isTrue(true) - } - } - - const app = setupApp() - const kernel = new Kernel(app) - kernel.register([Foo]) - - kernel.before('run', () => { - assert.isTrue(true) - }) - - kernel.after('run', () => { - assert.isTrue(true) - }) - - kernel.before('find', () => { - assert.isTrue(true) - }) - - kernel.after('find', () => { - assert.isTrue(true) - }) - - await kernel.exec('foo', []) - }) - - test('exec command prepare method', async ({ assert }) => { - assert.plan(2) - - class Foo extends BaseCommand { - public static commandName = 'foo' - public async prepare() { - assert.isTrue(true) - } - - public async run() { - assert.isTrue(true) - } - } - - const app = setupApp() - const kernel = new Kernel(app) - kernel.register([Foo]) - await kernel.exec('foo', []) - }) - - test('exec command completed method', async ({ assert }) => { - assert.plan(2) - - class Foo extends BaseCommand { - public static commandName = 'foo' - public async completed() { - assert.isUndefined(this.error) - } - - public async run() { - assert.isTrue(true) - } - } - - const app = setupApp() - const kernel = new Kernel(app) - kernel.register([Foo]) - await kernel.exec('foo', []) - }) - - test('exec command completed method, when command fails', async ({ assert }) => { - assert.plan(2) - - class Foo extends BaseCommand { - public static commandName = 'foo' - public async completed() { - assert.equal(this.error!.message, 'Boom') - return true - } - - public async run() { - assert.isTrue(true) - throw new Error('Boom') - } - } - - const app = setupApp() - const kernel = new Kernel(app) - kernel.register([Foo]) - await kernel.exec('foo', []) - }) - - test("raise exception when completed method doesn't handle it", async ({ assert }) => { - assert.plan(3) - - class Foo extends BaseCommand { - public static commandName = 'foo' - public async completed() { - assert.equal(this.error!.message, 'Boom') - } - - public async run() { - assert.isTrue(true) - throw new Error('Boom') - } - } - - const app = setupApp() - const kernel = new Kernel(app) - kernel.register([Foo]) - try { - await kernel.exec('foo', []) - } catch (error) { - assert.equal(error.message, 'Boom') - } - }) -}) - -test.group('Kernel | IoC container', () => { - test('make command instance by injecting dependencies', async ({ assert }, done) => { - assert.plan(1) - - const app = setupApp() - const kernel = new Kernel(app) - - class Foo {} - app.container.bind('App/Foo', () => { - return new Foo() - }) - - class Install extends BaseCommand { - public static commandName = 'install' - - public static get inject() { - return { - instance: [null, null, Foo], - } - } - - constructor(public application: Application, public _kernel: Kernel, public foo: Foo) { - super(application, _kernel) - } - - public async run() { - assert.instanceOf(this.foo, Foo) - } - } - - kernel.onExit(() => { - if (kernel.error) { - done(kernel.error) - } else { - done() - } - }) - - kernel.register([Install]) - await kernel.handle(['install']) - }).waitForDone() - - test('inject dependencies to command methods', async ({ assert }, done) => { - assert.plan(1) - - const app = setupApp() - const kernel = new Kernel(app) - - class Foo {} - app.container.bind('App/Foo', () => { - return new Foo() - }) - - class Install extends BaseCommand { - public static commandName = 'install' - - public static get inject() { - return { - run: [Foo], - } - } - - public async run(foo: Foo) { - assert.instanceOf(foo, Foo) - } - } - - kernel.register([Install]) - kernel.onExit(() => done()) - - await kernel.handle(['install']) - }).waitForDone() -}) - -test.group('Kernel | defaultCommand', () => { - test('set custom default command', async ({ assert }, done) => { - assert.plan(1) - - class Help extends BaseCommand { - public static commandName = 'help' - public async run() { - assert.isTrue(true) - } - } - - const app = setupApp() - const kernel = new Kernel(app) - kernel.defaultCommand = Help - - kernel.onExit(() => done()) - await kernel.handle([]) - }).waitForDone() - - test('execute before after hooks for the default command', async ({ assert }, done) => { - assert.plan(3) - - class Help extends BaseCommand { - public static commandName = 'help' - public async run() { - assert.isTrue(true) - } - } - - const app = setupApp() - const kernel = new Kernel(app) - - kernel.before('run', () => { - assert.isTrue(true) - }) - - kernel.after('run', () => { - assert.isTrue(true) - }) - - kernel.defaultCommand = Help - kernel.onExit(() => done()) - await kernel.handle([]) - }).waitForDone() -}) diff --git a/test/list-directory-files.spec.ts b/test/list-directory-files.spec.ts deleted file mode 100644 index 9425b80..0000000 --- a/test/list-directory-files.spec.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* - * @adonisjs/ace - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { test } from '@japa/runner' -import { join } from 'path' -import { Filesystem } from '@poppinss/dev-utils' -import { listDirectoryFiles } from '../src/utils/listDirectoryFiles' - -const fs = new Filesystem(join(__dirname, './app')) - -test.group('listDirectoryFiles', (group) => { - group.each.teardown(async () => { - await fs.cleanup() - }) - - test('get a list of javascript files from a given directory', async ({ assert }) => { - await fs.add('foo.js', '') - await fs.add('bar.js', '') - await fs.add('baz.js', '') - await fs.add('README.md', '') - await fs.add('.gitkeep', '') - - const directories = listDirectoryFiles(fs.basePath, fs.basePath) - assert.deepEqual(directories, ['./bar.js', './baz.js', './foo.js']) - }) - - test('allow inline files filter', async ({ assert }) => { - await fs.add('foo.js', '') - await fs.add('bar.js', '') - await fs.add('baz.js', '') - await fs.add('README.md', '') - await fs.add('.gitkeep', '') - - const directories = listDirectoryFiles(fs.basePath, fs.basePath, (name) => { - return name !== './baz.js' - }) - assert.deepEqual(directories, ['./bar.js', './foo.js']) - }) - - test('define nested directories', async ({ assert }) => { - await fs.add('commands/foo.js', '') - await fs.add('commands/bar.js', '') - await fs.add('commands/baz.js', '') - await fs.add('commands/README.md', '') - await fs.add('commands/.gitkeep', '') - - const directories = listDirectoryFiles(join(fs.basePath, 'commands'), fs.basePath, (name) => { - return name !== './commands/baz.js' - }) - - assert.deepEqual(directories, ['./commands/bar.js', './commands/foo.js']) - }) - - test('ignore files by defining list of ignored files', async ({ assert }) => { - await fs.add('commands/foo.js', '') - await fs.add('commands/bar.js', '') - await fs.add('commands/baz.js', '') - await fs.add('commands/README.md', '') - await fs.add('commands/.gitkeep', '') - - const directories = listDirectoryFiles(join(fs.basePath, 'commands'), fs.basePath, [ - './commands/baz.js', - ]) - - assert.deepEqual(directories, ['./commands/bar.js', './commands/foo.js']) - }) - - test('ignore files by defining list of ignored extension agnostic files', async ({ assert }) => { - await fs.add('commands/foo.js', '') - await fs.add('commands/bar.js', '') - await fs.add('commands/baz.js', '') - await fs.add('commands/README.md', '') - await fs.add('commands/.gitkeep', '') - - const directories = listDirectoryFiles(join(fs.basePath, 'commands'), fs.basePath, [ - './commands/baz', - ]) - - assert.deepEqual(directories, ['./commands/bar.js', './commands/foo.js']) - }) -}) diff --git a/test/manifest-generator.spec.ts b/test/manifest-generator.spec.ts deleted file mode 100644 index 5cacbc0..0000000 --- a/test/manifest-generator.spec.ts +++ /dev/null @@ -1,299 +0,0 @@ -/* - * @adonisjs/ace - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import 'reflect-metadata' -import { test } from '@japa/runner' -import { join } from 'path' - -import { fs } from '../test-helpers' -import { ManifestGenerator } from '../src/Manifest/Generator' - -test.group('Manifest Generator', (group) => { - group.setup(async () => { - await fs.ensureRoot() - }) - - group.each.teardown(async () => { - await fs.cleanup() - }) - - test('generate manifest from command paths', async ({ assert }) => { - await fs.add( - 'Commands/Make.ts', - ` - import { args, flags } from '../../../index' - import { BaseCommand } from '../../../src/BaseCommand' - - export default class Greet extends BaseCommand { - public static commandName = 'greet' - public static description = 'Greet a user' - - @args.string() - public name: string - - @flags.boolean() - public adult: boolean - - public async run () {} - }` - ) - - const manifest = new ManifestGenerator(fs.basePath, ['./Commands/Make.ts']) - await manifest.generate() - - const manifestJSON = await fs.fsExtra.readJSON(join(fs.basePath, 'ace-manifest.json')) - - assert.deepEqual(manifestJSON, { - commands: { - greet: { - settings: {}, - aliases: [], - commandPath: './Commands/Make', - commandName: 'greet', - description: 'Greet a user', - args: [ - { - name: 'name', - type: 'string', - propertyName: 'name', - required: true, - }, - ], - flags: [ - { - name: 'adult', - propertyName: 'adult', - type: 'boolean', - }, - ], - }, - }, - aliases: {}, - }) - }) - - test("raise exception when commandPath doesn't exports a command", async ({ assert }) => { - assert.plan(1) - - await fs.add( - 'Commands/Make.ts', - ` - import { args, flags } from '../../../index' - import { BaseCommand } from '../../../src/BaseCommand' - - export class Greet extends BaseCommand { - public static commandName = 'greet' - public static description = 'Greet a user' - - @args.string() - public name: string - - @flags.boolean() - public adult: boolean - - public async run () {} - }` - ) - - const manifest = new ManifestGenerator(fs.basePath, ['./Commands/Make.ts']) - - try { - await manifest.generate() - } catch ({ message }) { - assert.equal( - message, - 'Invalid command" ./Commands/Make.ts". Make sure the command is exported using the "export default"' - ) - } - }) - - test('generate manifest from command subpaths', async ({ assert }) => { - await fs.add( - 'Commands/Make.ts', - ` - import { args, flags } from '../../../index' - import { BaseCommand } from '../../../src/BaseCommand' - - export default class Greet extends BaseCommand { - public static commandName = 'greet' - public static description = 'Greet a user' - - @args.string() - public name: string - - @flags.boolean() - public adult: boolean - - public async run () {} - }` - ) - - await fs.add( - 'Commands/index.ts', - ` - export default [ - './Commands/Make', - ] - ` - ) - - const manifest = new ManifestGenerator(fs.basePath, ['./Commands/index.ts']) - await manifest.generate() - - const manifestJSON = await fs.fsExtra.readJSON(join(fs.basePath, 'ace-manifest.json')) - assert.deepEqual(manifestJSON, { - commands: { - greet: { - settings: {}, - aliases: [], - commandPath: './Commands/Make', - commandName: 'greet', - description: 'Greet a user', - args: [ - { - name: 'name', - type: 'string', - propertyName: 'name', - required: true, - }, - ], - flags: [ - { - name: 'adult', - propertyName: 'adult', - type: 'boolean', - }, - ], - }, - }, - aliases: {}, - }) - }) - - test('generate manifest from command subpaths using listDirectoryFiles', async ({ assert }) => { - await fs.add( - 'Commands/Make.ts', - ` - import { args, flags } from '../../../index' - import { BaseCommand } from '../../../src/BaseCommand' - - export default class Greet extends BaseCommand { - public static commandName = 'greet' - public static description = 'Greet a user' - - @args.string() - public name: string - - @flags.boolean() - public adult: boolean - - public async run () {} - }` - ) - - await fs.add( - 'Commands/index.ts', - ` - import { listDirectoryFiles } from '../../../index' - export default listDirectoryFiles(__dirname, '${fs.basePath.replace(/\\/g, '\\\\')}') - ` - ) - - const manifest = new ManifestGenerator(fs.basePath, ['./Commands/index.ts']) - await manifest.generate() - - const manifestJSON = await fs.fsExtra.readJSON(join(fs.basePath, 'ace-manifest.json')) - assert.deepEqual(manifestJSON, { - commands: { - greet: { - settings: {}, - aliases: [], - commandPath: './Commands/Make', - commandName: 'greet', - description: 'Greet a user', - args: [ - { - name: 'name', - type: 'string', - propertyName: 'name', - required: true, - }, - ], - flags: [ - { - name: 'adult', - propertyName: 'adult', - type: 'boolean', - }, - ], - }, - }, - aliases: {}, - }) - }) - - test('register command aliases inside manifest file', async ({ assert }) => { - await fs.add( - 'Commands/Make.ts', - ` - import { args, flags } from '../../../index' - import { BaseCommand } from '../../../src/BaseCommand' - - export default class Greet extends BaseCommand { - public static commandName = 'greet' - public static description = 'Greet a user' - public static aliases = ['g', 'gr'] - - @args.string() - public name: string - - @flags.boolean() - public adult: boolean - - public async run () {} - }` - ) - - const manifest = new ManifestGenerator(fs.basePath, ['./Commands/Make.ts']) - await manifest.generate() - - const manifestJSON = await fs.fsExtra.readJSON(join(fs.basePath, 'ace-manifest.json')) - - assert.deepEqual(manifestJSON, { - commands: { - greet: { - settings: {}, - aliases: ['g', 'gr'], - commandPath: './Commands/Make', - commandName: 'greet', - description: 'Greet a user', - args: [ - { - name: 'name', - type: 'string', - propertyName: 'name', - required: true, - }, - ], - flags: [ - { - name: 'adult', - propertyName: 'adult', - type: 'boolean', - }, - ], - }, - }, - aliases: { - g: 'greet', - gr: 'greet', - }, - }) - }) -}) diff --git a/test/manifest-loader.spec.ts b/test/manifest-loader.spec.ts deleted file mode 100644 index 3ce1e51..0000000 --- a/test/manifest-loader.spec.ts +++ /dev/null @@ -1,507 +0,0 @@ -/* - * @adonisjs/ace - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import 'reflect-metadata' -import { test } from '@japa/runner' -import { join } from 'path' - -import { fs } from '../test-helpers' -import { ManifestLoader } from '../src/Manifest/Loader' -import { ManifestGenerator } from '../src/Manifest/Generator' - -test.group('Manifest Generator', (group) => { - group.setup(async () => { - await fs.ensureRoot() - }) - - group.each.teardown(async () => { - await fs.cleanup() - }) - - test('read manifest file', async ({ assert }) => { - await fs.add( - './Commands/Greet.ts', - ` - import { args, flags } from '../../../index' - import { BaseCommand } from '../../../src/BaseCommand' - - export default class Greet extends BaseCommand { - public static commandName = 'greet' - public static description = 'Greet a user' - - @args.string() - public name: string - - @flags.boolean() - public adult: boolean - - public async handle () {} - }` - ) - - await new ManifestGenerator(fs.basePath, ['./Commands/Greet']).generate() - const manifestLoader = new ManifestLoader([ - { - basePath: fs.basePath, - manifestAbsPath: join(fs.basePath, 'ace-manifest.json'), - }, - ]) - - await manifestLoader.boot() - - assert.deepEqual(manifestLoader.getCommands(), { - commands: [ - { - settings: {}, - commandPath: './Commands/Greet', - commandName: 'greet', - description: 'Greet a user', - aliases: [], - args: [ - { - name: 'name', - propertyName: 'name', - type: 'string', - required: true, - }, - ], - flags: [ - { - name: 'adult', - type: 'boolean', - propertyName: 'adult', - }, - ], - }, - ], - aliases: {}, - }) - }) - - test('read more than one manifest files', async ({ assert }) => { - await fs.add( - './Commands/Greet.ts', - ` - import { args, flags } from '../../../index' - import { BaseCommand } from '../../../src/BaseCommand' - - export default class Greet extends BaseCommand { - public static commandName = 'greet' - public static description = 'Greet a user' - - @args.string() - public name: string - - @flags.boolean() - public adult: boolean - - public async run () {} - }` - ) - - await fs.add( - './sub-app/MyCommands/Run.ts', - ` - import { args, flags } from '../../../../index' - import { BaseCommand } from '../../../../src/BaseCommand' - - export default class Run extends BaseCommand { - public static commandName = 'run' - public static description = 'Run another command' - - @args.string() - public name: string - - public async run () {} - }` - ) - - await new ManifestGenerator(fs.basePath, ['./Commands/Greet']).generate() - await new ManifestGenerator(join(fs.basePath, 'sub-app'), ['./MyCommands/Run']).generate() - - const manifestLoader = new ManifestLoader([ - { - basePath: fs.basePath, - manifestAbsPath: join(fs.basePath, 'ace-manifest.json'), - }, - { - basePath: join(fs.basePath, 'sub-app'), - manifestAbsPath: join(fs.basePath, 'sub-app', 'ace-manifest.json'), - }, - ]) - - await manifestLoader.boot() - - assert.deepEqual(manifestLoader.getCommands(), { - commands: [ - { - settings: {}, - commandPath: './Commands/Greet', - commandName: 'greet', - description: 'Greet a user', - aliases: [], - args: [ - { - name: 'name', - propertyName: 'name', - type: 'string', - required: true, - }, - ], - flags: [ - { - name: 'adult', - type: 'boolean', - propertyName: 'adult', - }, - ], - }, - { - settings: {}, - commandPath: './MyCommands/Run', - commandName: 'run', - description: 'Run another command', - aliases: [], - args: [ - { - name: 'name', - propertyName: 'name', - type: 'string', - required: true, - }, - ], - flags: [], - }, - ], - aliases: {}, - }) - }) - - test('merge aliases of more than one command', async ({ assert }) => { - await fs.add( - './Commands/Greet.ts', - ` - import { args, flags } from '../../../index' - import { BaseCommand } from '../../../src/BaseCommand' - - export default class Greet extends BaseCommand { - public static commandName = 'greet' - public static description = 'Greet a user' - public static aliases = ['sayhi'] - - @args.string() - public name: string - - @flags.boolean() - public adult: boolean - - public async run () {} - }` - ) - - await fs.add( - './sub-app/MyCommands/Run.ts', - ` - import { args, flags } from '../../../../index' - import { BaseCommand } from '../../../../src/BaseCommand' - - export default class Run extends BaseCommand { - public static commandName = 'run' - public static description = 'Run another command' - public static aliases = ['fire'] - - @args.string() - public name: string - - public async run () {} - }` - ) - - await new ManifestGenerator(fs.basePath, ['./Commands/Greet']).generate() - await new ManifestGenerator(join(fs.basePath, 'sub-app'), ['./MyCommands/Run']).generate() - - const manifestLoader = new ManifestLoader([ - { - basePath: fs.basePath, - manifestAbsPath: join(fs.basePath, 'ace-manifest.json'), - }, - { - basePath: join(fs.basePath, 'sub-app'), - manifestAbsPath: join(fs.basePath, 'sub-app', 'ace-manifest.json'), - }, - ]) - - await manifestLoader.boot() - - assert.deepEqual(manifestLoader.getCommands(), { - commands: [ - { - settings: {}, - commandPath: './Commands/Greet', - commandName: 'greet', - description: 'Greet a user', - aliases: ['sayhi'], - args: [ - { - name: 'name', - propertyName: 'name', - type: 'string', - required: true, - }, - ], - flags: [ - { - name: 'adult', - type: 'boolean', - propertyName: 'adult', - }, - ], - }, - { - settings: {}, - commandPath: './MyCommands/Run', - commandName: 'run', - description: 'Run another command', - aliases: ['fire'], - args: [ - { - name: 'name', - propertyName: 'name', - type: 'string', - required: true, - }, - ], - flags: [], - }, - ], - aliases: { - sayhi: 'greet', - fire: 'run', - }, - }) - }) - - test('find if a command exists', async ({ assert }) => { - await fs.add( - './Commands/Greet.ts', - ` - import { args, flags } from '../../../index' - import { BaseCommand } from '../../../src/BaseCommand' - - export default class Greet extends BaseCommand { - public static commandName = 'greet' - public static description = 'Greet a user' - - @args.string() - public name: string - - @flags.boolean() - public adult: boolean - - public async run () {} - }` - ) - - await fs.add( - './sub-app/MyCommands/Run.ts', - ` - import { args, flags } from '../../../../index' - import { BaseCommand } from '../../../../src/BaseCommand' - - export default class Run extends BaseCommand { - public static commandName = 'run' - public static description = 'Run another command' - - @args.string() - public name: string - - public async run () {} - }` - ) - - await new ManifestGenerator(fs.basePath, ['./Commands/Greet']).generate() - await new ManifestGenerator(join(fs.basePath, 'sub-app'), ['./MyCommands/Run']).generate() - - const manifestLoader = new ManifestLoader([ - { - basePath: fs.basePath, - manifestAbsPath: join(fs.basePath, 'ace-manifest.json'), - }, - { - basePath: join(fs.basePath, 'sub-app'), - manifestAbsPath: join(fs.basePath, 'sub-app', 'ace-manifest.json'), - }, - ]) - - await manifestLoader.boot() - assert.isTrue(manifestLoader.hasCommand('greet')) - assert.isTrue(manifestLoader.hasCommand('run')) - assert.isFalse(manifestLoader.hasCommand('make')) - }) - - test('get command manifest node', async ({ assert }) => { - await fs.add( - './Commands/Greet.ts', - ` - import { args, flags } from '../../../index' - import { BaseCommand } from '../../../src/BaseCommand' - - export default class Greet extends BaseCommand { - public static commandName = 'greet' - public static description = 'Greet a user' - - @args.string() - public name: string - - @flags.boolean() - public adult: boolean - - public async run () {} - }` - ) - - await fs.add( - './sub-app/MyCommands/Run.ts', - ` - import { args, flags } from '../../../../index' - import { BaseCommand } from '../../../../src/BaseCommand' - - export default class Run extends BaseCommand { - public static commandName = 'run' - public static description = 'Run another command' - - @args.string() - public name: string - - public async run () {} - }` - ) - - await new ManifestGenerator(fs.basePath, ['./Commands/Greet']).generate() - await new ManifestGenerator(join(fs.basePath, 'sub-app'), ['./MyCommands/Run']).generate() - - const manifestLoader = new ManifestLoader([ - { - basePath: fs.basePath, - manifestAbsPath: join(fs.basePath, 'ace-manifest.json'), - }, - { - basePath: join(fs.basePath, 'sub-app'), - manifestAbsPath: join(fs.basePath, 'sub-app', 'ace-manifest.json'), - }, - ]) - - await manifestLoader.boot() - assert.deepEqual(manifestLoader.getCommand('greet'), { - basePath: fs.basePath, - command: { - settings: {}, - commandPath: './Commands/Greet', - commandName: 'greet', - aliases: [], - description: 'Greet a user', - args: [ - { - name: 'name', - propertyName: 'name', - type: 'string', - required: true, - }, - ], - flags: [ - { - name: 'adult', - type: 'boolean', - propertyName: 'adult', - }, - ], - }, - }) - - assert.deepEqual(manifestLoader.getCommand('run'), { - basePath: join(fs.basePath, 'sub-app'), - command: { - settings: {}, - aliases: [], - commandPath: './MyCommands/Run', - commandName: 'run', - description: 'Run another command', - args: [ - { - name: 'name', - propertyName: 'name', - type: 'string', - required: true, - }, - ], - flags: [], - }, - }) - - assert.isUndefined(manifestLoader.getCommand('make')) - }) - - test('load command', async ({ assert }) => { - await fs.add( - './Commands/Greet.ts', - ` - import { args, flags } from '../../../index' - import { BaseCommand } from '../../../src/BaseCommand' - - export default class Greet extends BaseCommand { - public static commandName = 'greet' - public static description = 'Greet a user' - - @args.string() - public name: string - - @flags.boolean() - public adult: boolean - - public async run () {} - }` - ) - - await fs.add( - './sub-app/MyCommands/Run.ts', - ` - import { args, flags } from '../../../../index' - import { BaseCommand } from '../../../../src/BaseCommand' - - export default class Run extends BaseCommand { - public static commandName = 'run' - public static description = 'Run another command' - - @args.string() - public name: string - - public async run () {} - }` - ) - - await new ManifestGenerator(fs.basePath, ['./Commands/Greet']).generate() - await new ManifestGenerator(join(fs.basePath, 'sub-app'), ['./MyCommands/Run']).generate() - - const manifestLoader = new ManifestLoader([ - { - basePath: fs.basePath, - manifestAbsPath: join(fs.basePath, 'ace-manifest.json'), - }, - { - basePath: join(fs.basePath, 'sub-app'), - manifestAbsPath: join(fs.basePath, 'sub-app', 'ace-manifest.json'), - }, - ]) - - await manifestLoader.boot() - const command = await manifestLoader.loadCommand('greet') - assert.deepEqual(command.commandName, 'greet') - assert.isTrue(command.booted) - }) -}) diff --git a/test/parser.spec.ts b/test/parser.spec.ts deleted file mode 100644 index 2a56682..0000000 --- a/test/parser.spec.ts +++ /dev/null @@ -1,346 +0,0 @@ -/* - * @adonisjs/ace - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { test } from '@japa/runner' - -import { Parser } from '../src/Parser' -import { args } from '../src/Decorators/args' -import { BaseCommand } from '../src/BaseCommand' - -test.group('Parser | flags', () => { - test('parse flags as boolean', ({ assert }) => { - const parser = new Parser({ - admin: { - type: 'boolean' as 'boolean', - name: 'admin', - propertyName: 'admin', - handler: () => {}, - }, - }) - - const output = parser.parse(['--admin=true']) - assert.deepEqual(output, { _: [], admin: true }) - }) - - test('do not parse string values as true', ({ assert }) => { - const parser = new Parser({ - admin: { - type: 'boolean' as 'boolean', - name: 'admin', - propertyName: 'admin', - handler: () => {}, - }, - }) - - assert.throws( - () => parser.parse(['--admin=yes']), - 'E_INVALID_FLAG: "admin" flag expects a "boolean" value' - ) - }) - - test('parse negative flags as boolean', ({ assert }) => { - const parser = new Parser({ - admin: { - type: 'boolean' as 'boolean', - name: 'admin', - propertyName: 'admin', - handler: () => {}, - }, - }) - - const output = parser.parse(['--no-admin']) - assert.deepEqual(output, { _: [], admin: false }) - }) - - test('set flag to true when its undefined', ({ assert }) => { - const parser = new Parser({ - admin: { - type: 'boolean' as 'boolean', - name: 'admin', - propertyName: 'admin', - handler: () => {}, - }, - }) - - const output = parser.parse(['--admin']) - assert.deepEqual(output, { _: [], admin: true }) - }) - - test('do not set boolean flag when it is not mentioned', ({ assert }) => { - const parser = new Parser({ - admin: { - type: 'boolean' as 'boolean', - name: 'admin', - propertyName: 'admin', - handler: () => {}, - }, - }) - - const output = parser.parse([]) - assert.deepEqual(output, { _: [] }) - }) - - test('parse flags as string', ({ assert }) => { - const parser = new Parser({ - admin: { - type: 'string' as 'string', - name: 'admin', - propertyName: 'admin', - handler: () => {}, - }, - }) - - const output = parser.parse(['--admin=true']) - assert.deepEqual(output, { _: [], admin: 'true' }) - }) - - test('do not define string flag when it is not mentioned', ({ assert }) => { - const parser = new Parser({ - admin: { - type: 'string' as 'string', - name: 'admin', - propertyName: 'admin', - handler: () => {}, - }, - }) - - const output = parser.parse([]) - assert.deepEqual(output, { _: [] }) - }) - - test('raise error when string flag is used without value', ({ assert }) => { - const parser = new Parser({ - admin: { - type: 'string' as 'string', - name: 'admin', - propertyName: 'admin', - handler: () => {}, - }, - }) - - assert.throws( - () => parser.parse(['--admin']), - 'E_INVALID_FLAG: "admin" flag expects a "string" value' - ) - }) - - test('set flag to number', ({ assert }) => { - const parser = new Parser({ - age: { - type: 'number' as 'number', - name: 'age', - propertyName: 'age', - handler: () => {}, - }, - }) - - const output = parser.parse(['--age=22']) - assert.deepEqual(output, { _: [], age: 22 }) - }) - - test('set number like values as string when defined as string', ({ assert }) => { - const parser = new Parser({ - age: { - type: 'string' as 'string', - name: 'age', - propertyName: 'age', - handler: () => {}, - }, - }) - - const output = parser.parse(['--age=22']) - assert.deepEqual(output, { _: [], age: '22' }) - }) - - test('raise error when number flag receives a string', ({ assert }) => { - const parser = new Parser({ - age: { - type: 'number' as 'number', - name: 'age', - propertyName: 'age', - handler: () => {}, - }, - }) - - const output = () => parser.parse(['--age=foo']) - assert.throws(output, 'E_INVALID_FLAG: "age" flag expects a "numeric" value') - }) - - test('raise error when number flag receives a boolean', ({ assert }) => { - const parser = new Parser({ - age: { - type: 'number' as 'number', - name: 'age', - propertyName: 'age', - handler: () => {}, - }, - }) - - const output = () => parser.parse(['--age']) - assert.throws(output, 'E_INVALID_FLAG: "age" flag expects a "numeric" value') - }) - - test('parse value as an array of strings', ({ assert }) => { - const parser = new Parser({ - names: { - type: 'array' as 'array', - name: 'names', - propertyName: 'names', - handler: () => {}, - }, - }) - - const output = parser.parse(['--names=virk']) - assert.deepEqual(output, { _: [], names: ['virk'] }) - }) - - test('parse value as an array of strings when passed for multiple times', ({ assert }) => { - const parser = new Parser({ - names: { - type: 'array' as 'array', - name: 'names', - propertyName: 'names', - handler: () => {}, - }, - }) - - const output = parser.parse(['--names=virk', '--names=nikk']) - assert.deepEqual(output, { _: [], names: ['virk', 'nikk'] }) - }) - - test('parse value as an array of strings when defined a numeric value', ({ assert }) => { - const parser = new Parser({ - names: { - type: 'array' as 'array', - name: 'names', - propertyName: 'names', - handler: () => {}, - }, - }) - - const output = parser.parse(['--names=22']) - assert.deepEqual(output, { _: [], names: ['22'] }) - }) - - test('parse value as an array of strings when defined a numeric value multiple times', ({ - assert, - }) => { - const parser = new Parser({ - names: { - type: 'array' as 'array', - name: 'names', - propertyName: 'names', - handler: () => {}, - }, - }) - - const output = parser.parse(['--names=22', '--names=foo']) - assert.deepEqual(output, { _: [], names: ['22', 'foo'] }) - }) - - test('raise error when array receives a boolean', ({ assert }) => { - const parser = new Parser({ - names: { - type: 'array' as 'array', - name: 'names', - propertyName: 'names', - handler: () => {}, - }, - }) - - assert.throws( - () => parser.parse(['--names']), - 'E_INVALID_FLAG: "names" flag expects an "array of strings" value' - ) - }) - - test('parse value as an array of number', ({ assert }) => { - const parser = new Parser({ - scores: { - type: 'numArray' as 'numArray', - name: 'scores', - propertyName: 'scores', - handler: () => {}, - }, - }) - - const output = parser.parse(['--scores=10']) - assert.deepEqual(output, { _: [], scores: [10] }) - }) - - test('parse value as an array of numbers when passed for multiple times', ({ assert }) => { - const parser = new Parser({ - scores: { - type: 'numArray' as 'numArray', - name: 'scores', - propertyName: 'scores', - handler: () => {}, - }, - }) - - const output = parser.parse(['--scores=10', '--scores=20']) - assert.deepEqual(output, { _: [], scores: [10, 20] }) - }) - - test('raise error when one of the array value is not a number', ({ assert }) => { - const parser = new Parser({ - scores: { - type: 'numArray' as 'numArray', - name: 'scores', - propertyName: 'scores', - handler: () => {}, - }, - }) - - assert.throws( - () => parser.parse(['--scores=10', '--scores=foo']), - 'E_INVALID_FLAG: "scores" flag expects an "array of numbers" value' - ) - - assert.throws( - () => parser.parse(['--scores=foo']), - 'E_INVALID_FLAG: "scores" flag expects an "array of numbers" value' - ) - - assert.throws( - () => parser.parse(['--scores=foo,22']), - 'E_INVALID_FLAG: "scores" flag expects an "array of numbers" value' - ) - }) -}) - -test.group('Parser | args', () => { - test('parse string arguments', ({ assert }) => { - class Greet extends BaseCommand { - @args.string() - public name: string - - public async handle() {} - } - - const parser = new Parser({}) - - const output = parser.parse(['virk'], Greet) - assert.deepEqual(output, { _: ['virk'] }) - }) - - test('mark argument as optional', ({ assert }) => { - class Greet extends BaseCommand { - @args.string({ required: false }) - public name: string - - public async handle() {} - } - - const parser = new Parser({}) - - const output = parser.parse([], Greet) - assert.deepEqual(output, { _: [] }) - }) -}) diff --git a/test/sort-commands.spec.ts b/test/sort-commands.spec.ts deleted file mode 100644 index 9dbc79d..0000000 --- a/test/sort-commands.spec.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* - * @adonisjs/ace - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { test } from '@japa/runner' - -import { BaseCommand } from '../src/BaseCommand' -import { sortAndGroupCommands } from '../src/utils/sortAndGroupCommands' - -test.group('utils | sortAndGroupCommands', () => { - test('sort commands in alphabetical order', ({ assert }) => { - class Greet extends BaseCommand { - public static commandName = 'greet' - public async handle() {} - } - - class Run extends BaseCommand { - public static commandName = 'run' - public async handle() {} - } - - const output = sortAndGroupCommands([Run, Greet]) - assert.deepEqual(output, [{ group: 'root', commands: [Greet, Run] }]) - }) - - test('sort and group commands in alphabetical order', ({ assert }) => { - class MakeController extends BaseCommand { - public static commandName = 'make:controller' - public async handle() {} - } - - class MakeModel extends BaseCommand { - public static commandName = 'make:model' - public async handle() {} - } - - class Run extends BaseCommand { - public static commandName = 'run' - public async handle() {} - } - - const output = sortAndGroupCommands([MakeController, MakeModel, Run]) - assert.deepEqual(output, [ - { group: 'root', commands: [Run] }, - { group: 'make', commands: [MakeController, MakeModel] }, - ]) - }) - - test('sort groups in alphabetical order too', ({ assert }) => { - class MakeController extends BaseCommand { - public static commandName = 'make:controller' - public async handle() {} - } - - class MakeModel extends BaseCommand { - public static commandName = 'make:model' - public async handle() {} - } - - class AuthScaffold extends BaseCommand { - public static commandName = 'auth:scaffold' - public async handle() {} - } - - class Run extends BaseCommand { - public static commandName = 'run' - public async handle() {} - } - - const output = sortAndGroupCommands([MakeController, MakeModel, Run, AuthScaffold]) - assert.deepEqual(output, [ - { group: 'root', commands: [Run] }, - { group: 'auth', commands: [AuthScaffold] }, - { group: 'make', commands: [MakeController, MakeModel] }, - ]) - }) -}) diff --git a/test/template.spec.ts b/test/template.spec.ts deleted file mode 100644 index b0351e4..0000000 --- a/test/template.spec.ts +++ /dev/null @@ -1,91 +0,0 @@ -/* - * @adonisjs/ace - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { test } from '@japa/runner' -import { join } from 'path' -import { template, templateFromFile } from '../src/utils/template' - -test.group('template', () => { - test('interpolate valid template', ({ assert }) => { - const result = template('${test} ${other}', { - test: 123, - other: 'hello', - }) - assert.strictEqual(result, '123 hello') - }) - - test('raise error when value is missing', ({ assert }) => { - const fn = () => template('${test} ${other}', {}) - assert.throws(fn, 'Error in template eval:1:1\ntest is not defined') - }) - - test('interpolate template using mustache', ({ assert }) => { - const result = template( - '{{test}} {{other}}', - { - test: 123, - other: 'hello', - }, - undefined, - true - ) - assert.strictEqual(result, '123 hello') - }) - - test('interpolate function calls', ({ assert }) => { - const result = template('${test()}', { - test: () => 123, - }) - assert.strictEqual(result, '123') - }) - - test('interpolate property accessor', ({ assert }) => { - const result = template('${user.name}', { - user: { name: 'virk' }, - }) - assert.strictEqual(result, 'virk') - }) -}) - -test.group('Template From File', () => { - test('interpolate valid template', ({ assert }) => { - const result = templateFromFile( - join(__dirname, 'fixtures/template1.txt'), - { - value1: 'World', - value2: 42, - }, - false - ) - - assert.strictEqual(result, 'Hello World, 42') - }) - - test('raise error when values is missing', ({ assert }) => { - const file = join(__dirname, 'fixtures/template1.txt') - const fn = () => templateFromFile(file, {}, false) - assert.throws(fn, `Error in template ${file}:1:10\nvalue1 is not defined`) - }) - - test('error if file is missing', ({ assert }) => { - assert.throws(() => templateFromFile(join(__dirname, 'fixtures/i-do-not-exist'), {}, false)) - }) - - test('interpolate mustache from template file', ({ assert }) => { - const result = templateFromFile( - join(__dirname, 'fixtures/template1.mustache'), - { - value1: 'World', - value2: 42, - }, - true - ) - assert.strictEqual(result.trim(), 'Hello World, 42') - }) -}) diff --git a/tests/base_command/define_args.spec.ts b/tests/base_command/define_args.spec.ts new file mode 100644 index 0000000..e2c9062 --- /dev/null +++ b/tests/base_command/define_args.spec.ts @@ -0,0 +1,161 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { BaseCommand } from '../../src/commands/base.js' + +test.group('Base command | arguments', () => { + test('define argument for the command', ({ assert }) => { + class MakeModel extends BaseCommand {} + MakeModel.defineArgument('name', { type: 'string' }) + + assert.deepEqual(MakeModel.args, [ + { name: 'name', required: true, type: 'string', argumentName: 'name' }, + ]) + }) + + test('define argument with inheritance', ({ assert }) => { + class MakeEntity extends BaseCommand {} + MakeEntity.defineArgument('name', { type: 'string' }) + + class MakeModel extends MakeEntity {} + MakeModel.defineArgument('dbConnection', { type: 'string' }) + + class MakeController extends MakeEntity {} + MakeController.defineArgument('resourceName', { type: 'string' }) + + assert.deepEqual(MakeModel.args, [ + { name: 'name', required: true, type: 'string', argumentName: 'name' }, + { name: 'dbConnection', required: true, type: 'string', argumentName: 'db-connection' }, + ]) + + assert.deepEqual(MakeController.args, [ + { name: 'name', required: true, type: 'string', argumentName: 'name' }, + { name: 'resourceName', required: true, type: 'string', argumentName: 'resource-name' }, + ]) + }) + + test('define arguments with description', ({ assert }) => { + class MakeEntity extends BaseCommand {} + MakeEntity.defineArgument('name', { description: 'The name of the entity', type: 'string' }) + + class MakeModel extends MakeEntity {} + MakeModel.defineArgument('dbConnection', { + description: 'Db connection to use', + type: 'string', + }) + + class MakeController extends MakeEntity {} + MakeController.defineArgument('resourceName', { + description: 'The resource name', + type: 'string', + }) + + assert.deepEqual(MakeModel.args, [ + { + name: 'name', + required: true, + type: 'string', + description: 'The name of the entity', + argumentName: 'name', + }, + { + name: 'dbConnection', + required: true, + type: 'string', + description: 'Db connection to use', + argumentName: 'db-connection', + }, + ]) + assert.deepEqual(MakeController.args, [ + { + name: 'name', + required: true, + type: 'string', + description: 'The name of the entity', + argumentName: 'name', + }, + { + name: 'resourceName', + required: true, + type: 'string', + description: 'The resource name', + argumentName: 'resource-name', + }, + ]) + }) + + test('fail when adding required argument after optional argument', ({ assert }) => { + class MakeModel extends BaseCommand {} + MakeModel.defineArgument('name', { type: 'string' }) + MakeModel.defineArgument('connection', { required: false, type: 'string' }) + + assert.throws( + () => MakeModel.defineArgument('resourceName', { type: 'string' }), + 'Cannot define required argument "MakeModel.resourceName" after optional argument "MakeModel.connection"' + ) + }) + + test('define spread argument', ({ assert }) => { + class MakeModel extends BaseCommand {} + MakeModel.defineArgument('name', { type: 'string' }) + MakeModel.defineArgument('connections', { type: 'spread' }) + + assert.deepEqual(MakeModel.args, [ + { name: 'name', required: true, type: 'string', argumentName: 'name' }, + { + name: 'connections', + required: true, + type: 'spread', + argumentName: 'connections', + }, + ]) + }) + + test('fail when adding standard argument after the spread argument', ({ assert }) => { + class MakeModel extends BaseCommand {} + MakeModel.defineArgument('connections', { type: 'spread' }) + + assert.throws( + () => MakeModel.defineArgument('name', { type: 'string' }), + 'Cannot define argument "MakeModel.name" after spread argument "MakeModel.connections". Spread argument should be the last one' + ) + }) + + test('fail when argument type is missing', ({ assert }) => { + class MakeModel extends BaseCommand {} + assert.throws( + // @ts-expect-error + () => MakeModel.defineArgument('name', {}), + 'Cannot define argument "MakeModel.name". Specify the argument type' + ) + }) +}) + +test.group('Base command | arguments | parserOutput', () => { + test('define parse function for the arguments', ({ assert }) => { + class MakeModel extends BaseCommand {} + MakeModel.defineArgument('name', { + type: 'string', + parse(input) { + return input + }, + }) + MakeModel.defineArgument('connections', { + type: 'spread', + parse(input) { + return input + }, + }) + + const options = MakeModel.getParserOptions().argumentsParserOptions + assert.isFunction(options[0].parse) + assert.isFunction(options[1].parse) + }) +}) diff --git a/tests/base_command/define_flags.spec.ts b/tests/base_command/define_flags.spec.ts new file mode 100644 index 0000000..f8fad0d --- /dev/null +++ b/tests/base_command/define_flags.spec.ts @@ -0,0 +1,213 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { BaseCommand } from '../../src/commands/base.js' + +test.group('Base command | flags', () => { + test('define flag for the command', ({ assert }) => { + class MakeModel extends BaseCommand {} + MakeModel.defineFlag('connection', { type: 'string' }) + + assert.deepEqual(MakeModel.flags, [ + { name: 'connection', required: false, type: 'string', flagName: 'connection' }, + ]) + }) + + test('define flag with inheritance', ({ assert }) => { + class MakeEntity extends BaseCommand {} + MakeEntity.defineFlag('env', { type: 'string' }) + + class MakeModel extends MakeEntity {} + MakeModel.defineFlag('connection', { type: 'string' }) + + class MakeController extends MakeEntity {} + MakeController.defineFlag('model', { type: 'string' }) + + assert.deepEqual(MakeModel.flags, [ + { name: 'env', required: false, type: 'string', flagName: 'env' }, + { name: 'connection', required: false, type: 'string', flagName: 'connection' }, + ]) + + assert.deepEqual(MakeController.flags, [ + { name: 'env', required: false, type: 'string', flagName: 'env' }, + { name: 'model', required: false, type: 'string', flagName: 'model' }, + ]) + }) + + test('define flag with description', ({ assert }) => { + class MakeModel extends BaseCommand {} + MakeModel.defineFlag('connection', { + description: 'Db connection to use', + type: 'string', + }) + + assert.deepEqual(MakeModel.flags, [ + { + name: 'connection', + required: false, + type: 'string', + description: 'Db connection to use', + flagName: 'connection', + }, + ]) + }) + + test('make flag required', ({ assert }) => { + class MakeModel extends BaseCommand {} + MakeModel.defineFlag('connection', { + description: 'Db connection to use', + type: 'string', + required: true, + }) + + assert.deepEqual(MakeModel.flags, [ + { + name: 'connection', + required: true, + type: 'string', + description: 'Db connection to use', + flagName: 'connection', + }, + ]) + }) + + test('fail when flag type is missing', ({ assert }) => { + class MakeModel extends BaseCommand {} + assert.throws( + // @ts-expect-error + () => MakeModel.defineFlag('name', {}), + 'Cannot define flag "MakeModel.name". Specify the flag type' + ) + }) +}) + +test.group('Base command | flags | parserOutput', () => { + test('define flags for all data types', ({ assert }) => { + class MakeModel extends BaseCommand {} + MakeModel.defineFlag('connection', { type: 'string' }) + MakeModel.defineFlag('dropAll', { type: 'boolean' }) + MakeModel.defineFlag('batchSize', { type: 'number' }) + MakeModel.defineFlag('files', { type: 'array' }) + + assert.deepEqual(MakeModel.getParserOptions().flagsParserOptions, { + all: ['connection', 'drop-all', 'batch-size', 'files'], + string: ['connection'], + boolean: ['drop-all'], + array: ['files'], + number: ['batch-size'], + alias: {}, + count: [], + coerce: {}, + default: {}, + }) + }) + + test('define flags aliases', ({ assert }) => { + class MakeModel extends BaseCommand {} + MakeModel.defineFlag('connection', { type: 'string', alias: 'c' }) + MakeModel.defineFlag('dropAll', { type: 'boolean', alias: 'da' }) + MakeModel.defineFlag('batchSize', { type: 'number', alias: ['b', 'bs'] }) + MakeModel.defineFlag('files', { type: 'array', alias: ['ff'] }) + + assert.deepEqual(MakeModel.getParserOptions().flagsParserOptions, { + all: ['connection', 'drop-all', 'batch-size', 'files'], + string: ['connection'], + boolean: ['drop-all'], + array: ['files'], + number: ['batch-size'], + alias: { + 'connection': 'c', + 'drop-all': 'da', + 'batch-size': ['b', 'bs'], + 'files': ['ff'], + }, + count: [], + coerce: {}, + default: {}, + }) + }) + + test('define flag default values', ({ assert }) => { + class MakeModel extends BaseCommand {} + MakeModel.defineFlag('connection', { type: 'string', alias: 'c', default: 'sqlite' }) + MakeModel.defineFlag('dropAll', { type: 'boolean', alias: 'da' }) + MakeModel.defineFlag('batchSize', { type: 'number', alias: ['b', 'bs'], default: 2 }) + MakeModel.defineFlag('files', { type: 'array', alias: ['ff'] }) + + assert.deepEqual(MakeModel.getParserOptions().flagsParserOptions, { + all: ['connection', 'drop-all', 'batch-size', 'files'], + string: ['connection'], + boolean: ['drop-all'], + array: ['files'], + number: ['batch-size'], + alias: { + 'connection': 'c', + 'drop-all': 'da', + 'batch-size': ['b', 'bs'], + 'files': ['ff'], + }, + count: [], + coerce: {}, + default: { + 'connection': 'sqlite', + 'batch-size': 2, + }, + }) + }) + + test('define parse function for the flags', ({ assert }) => { + class MakeModel extends BaseCommand {} + MakeModel.defineFlag('connection', { + type: 'string', + alias: 'c', + default: 'sqlite', + parse(input) { + return input + }, + }) + MakeModel.defineFlag('dropAll', { + type: 'boolean', + alias: 'da', + parse(input) { + return input + }, + }) + MakeModel.defineFlag('batchSize', { + type: 'number', + alias: ['b', 'bs'], + default: 2, + parse(input) { + return input + }, + }) + MakeModel.defineFlag('files', { + type: 'array', + alias: ['ff'], + parse(input) { + return input + }, + }) + + const coerce = MakeModel.getParserOptions().flagsParserOptions.coerce + assert.isFunction(coerce['batch-size']) + assert.isFunction(coerce.connection) + assert.isFunction(coerce['drop-all']) + assert.isFunction(coerce['files']) + }) + + test('fail when flag type is missing', ({ assert }) => { + class MakeModel extends BaseCommand {} + assert.throws( + // @ts-expect-error + () => MakeModel.defineFlag('name', {}), + 'Cannot define flag "MakeModel.name". Specify the flag type' + ) + }) +}) diff --git a/tests/base_command/exec.spec.ts b/tests/base_command/exec.spec.ts new file mode 100644 index 0000000..97090d1 --- /dev/null +++ b/tests/base_command/exec.spec.ts @@ -0,0 +1,320 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import sinon from 'sinon' +import { test } from '@japa/runner' +import { cliui } from '@poppinss/cliui' + +import { Kernel } from '../../src/kernel.js' +import { BaseCommand } from '../../src/commands/base.js' + +test.group('Base command | execute', () => { + test('execute command and its template methods', async ({ assert }) => { + class MakeModel extends BaseCommand { + name!: string + connection!: string + stack: string[] = [] + + async prepare() { + this.stack.push('prepare') + super.prepare() + } + + async interact() { + this.stack.push('interact') + super.interact() + } + + async completed() { + this.stack.push('completed') + super.completed() + } + + async run() { + this.stack.push('run') + super.run() + } + } + + MakeModel.defineArgument('name', { type: 'string' }) + MakeModel.defineFlag('connection', { type: 'string' }) + + const kernel = new Kernel() + const model = await kernel.create(MakeModel, ['user', '--connection=sqlite']) + + await model.exec() + assert.deepEqual(model.stack, ['prepare', 'interact', 'run', 'completed']) + }) + + test('store run method return value in the result property', async ({ assert }) => { + class MakeModel extends BaseCommand { + name!: string + connection!: string + stack: string[] = [] + + async prepare() { + this.stack.push('prepare') + } + + async interact() { + this.stack.push('interact') + } + + async completed() { + this.stack.push('completed') + } + + async run() { + this.stack.push('run') + return 'completed' + } + } + + MakeModel.defineArgument('name', { type: 'string' }) + MakeModel.defineFlag('connection', { type: 'string' }) + + const kernel = new Kernel() + kernel.ui = cliui({ mode: 'raw' }) + const model = await kernel.create(MakeModel, ['user', '--connection=sqlite']) + + await model.exec() + assert.deepEqual(model.stack, ['prepare', 'interact', 'run', 'completed']) + assert.equal(model.result, 'completed') + }) +}) + +test.group('Base command | execute | prepare fails', () => { + test('fail command when prepare method fails', async ({ assert }) => { + class MakeModel extends BaseCommand { + name!: string + connection!: string + stack: string[] = [] + + async prepare() { + throw new Error('Something went wrong') + } + + async run() { + return 'completed' + } + } + + MakeModel.defineArgument('name', { type: 'string' }) + MakeModel.defineFlag('connection', { type: 'string' }) + + const kernel = new Kernel() + kernel.ui = cliui({ mode: 'raw' }) + const model = await kernel.create(MakeModel, ['user', '--connection=sqlite']) + + await model.exec() + assert.isUndefined(model.result) + assert.equal(model.error?.message, 'Something went wrong') + assert.lengthOf(model.ui.logger.getRenderer().getLogs(), 1) + assert.equal(model.exitCode, 1) + }) + + test('run completed template method when prepare method fails', async ({ assert }) => { + class MakeModel extends BaseCommand { + name!: string + connection!: string + stack: string[] = [] + + async prepare() { + this.stack.push('prepare') + throw new Error('Something went wrong') + } + + async interact() { + this.stack.push('interact') + } + + async completed() { + this.stack.push('completed') + } + + async run() { + this.stack.push('run') + return 'completed' + } + } + + MakeModel.defineArgument('name', { type: 'string' }) + MakeModel.defineFlag('connection', { type: 'string' }) + + const kernel = new Kernel() + kernel.ui = cliui({ mode: 'raw' }) + const model = await kernel.create(MakeModel, ['user', '--connection=sqlite']) + + await model.exec() + assert.deepEqual(model.stack, ['prepare', 'completed']) + }) +}) + +test.group('Base command | execute | intertact fails', () => { + test('fail command when intertact method fails', async ({ assert }) => { + class MakeModel extends BaseCommand { + name!: string + connection!: string + stack: string[] = [] + + async interact() { + throw new Error('Something went wrong') + } + + async run() { + return 'completed' + } + } + + MakeModel.defineArgument('name', { type: 'string' }) + MakeModel.defineFlag('connection', { type: 'string' }) + + const kernel = new Kernel() + kernel.ui = cliui({ mode: 'raw' }) + const model = await kernel.create(MakeModel, ['user', '--connection=sqlite']) + + await model.exec() + assert.isUndefined(model.result) + assert.equal(model.error?.message, 'Something went wrong') + assert.lengthOf(model.ui.logger.getRenderer().getLogs(), 1) + assert.equal(model.exitCode, 1) + }) + + test('run completed template method when intertact method fails', async ({ assert }) => { + class MakeModel extends BaseCommand { + name!: string + connection!: string + stack: string[] = [] + + async interact() { + this.stack.push('interact') + throw new Error('Something went wrong') + } + + async completed() { + this.stack.push('completed') + } + + async run() { + this.stack.push('run') + return 'completed' + } + } + + MakeModel.defineArgument('name', { type: 'string' }) + MakeModel.defineFlag('connection', { type: 'string' }) + + const kernel = new Kernel() + kernel.ui = cliui({ mode: 'raw' }) + const model = await kernel.create(MakeModel, ['user', '--connection=sqlite']) + + await model.exec() + assert.deepEqual(model.stack, ['interact', 'completed']) + }) +}) + +test.group('Base command | execute | run fails', () => { + test('fail command when run method fails', async ({ assert }) => { + class MakeModel extends BaseCommand { + name!: string + connection!: string + stack: string[] = [] + + async run() { + throw new Error('Something went wrong') + } + } + + MakeModel.defineArgument('name', { type: 'string' }) + MakeModel.defineFlag('connection', { type: 'string' }) + + const kernel = new Kernel() + kernel.ui = cliui({ mode: 'raw' }) + const model = await kernel.create(MakeModel, ['user', '--connection=sqlite']) + + await model.exec() + assert.isUndefined(model.result) + assert.equal(model.error?.message, 'Something went wrong') + assert.lengthOf(model.ui.logger.getRenderer().getLogs(), 1) + assert.equal(model.exitCode, 1) + }) + + test('run completed template method when run method fails', async ({ assert }) => { + class MakeModel extends BaseCommand { + name!: string + connection!: string + stack: string[] = [] + + async completed() { + this.stack.push('completed') + } + + async run() { + this.stack.push('run') + throw new Error('Something went wrong') + } + } + + MakeModel.defineArgument('name', { type: 'string' }) + MakeModel.defineFlag('connection', { type: 'string' }) + + const kernel = new Kernel() + kernel.ui = cliui({ mode: 'raw' }) + const model = await kernel.create(MakeModel, ['user', '--connection=sqlite']) + + await model.exec() + assert.deepEqual(model.stack, ['run', 'completed']) + }) +}) + +test.group('Base command | execute | complete method', () => { + test('do not report command error if complete method handles it', async ({ assert }) => { + class MakeModel extends BaseCommand { + name!: string + connection!: string + + async completed() { + return true + } + + async run() { + throw new Error('Something went wrong') + } + } + + MakeModel.defineArgument('name', { type: 'string' }) + MakeModel.defineFlag('connection', { type: 'string' }) + + const kernel = new Kernel() + kernel.ui = cliui({ mode: 'raw' }) + const model = await kernel.create(MakeModel, ['user', '--connection=sqlite']) + + await model.exec() + assert.lengthOf(model.ui.logger.getRenderer().getLogs(), 0) + }) +}) + +test.group('Base command | terminate', () => { + test('call terminate method on kernel', async ({ cleanup }) => { + class MakeModel extends BaseCommand {} + MakeModel.boot() + + const kernel = new Kernel() + kernel.ui = cliui({ mode: 'raw' }) + const model = await kernel.create(MakeModel, []) + + const terminate = sinon.stub(kernel, 'terminate') + cleanup(() => { + terminate.restore() + }) + + await model.terminate() + terminate.calledWith(model) + }) +}) diff --git a/tests/base_command/main.spec.ts b/tests/base_command/main.spec.ts new file mode 100644 index 0000000..9d8fb46 --- /dev/null +++ b/tests/base_command/main.spec.ts @@ -0,0 +1,150 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { cliui } from '@poppinss/cliui' + +import { Parser } from '../../src/parser.js' +import { Kernel } from '../../src/kernel.js' +import { BaseCommand } from '../../src/commands/base.js' + +test.group('Base command', () => { + test('access the ui logger from the logger property', ({ assert }) => { + class MakeModel extends BaseCommand { + name!: string + connection!: string + } + + MakeModel.boot() + + const kernel = new Kernel() + const model = new MakeModel(kernel, { _: [], args: [], unknownFlags: [], flags: {} }, cliui()) + assert.strictEqual(model.logger, model.ui.logger) + }) + + test('access the ui colors from the colors property', ({ assert }) => { + class MakeModel extends BaseCommand { + name!: string + connection!: string + } + + MakeModel.boot() + + const kernel = new Kernel() + const model = new MakeModel(kernel, { _: [], args: [], unknownFlags: [], flags: {} }, cliui()) + assert.strictEqual(model.colors, model.ui.colors) + }) +}) + +test.group('Base command | consume args', () => { + test('consume parsed output to set command properties', ({ assert }) => { + class MakeModel extends BaseCommand { + name!: string + connection!: string + } + + MakeModel.defineArgument('name', { type: 'string' }) + MakeModel.defineFlag('connection', { type: 'string' }) + + const parsed = new Parser(MakeModel.getParserOptions()).parse('user --connection=sqlite') + + const kernel = new Kernel() + const model = MakeModel.create(kernel, parsed, cliui()) + + assert.equal(model.name, 'user') + assert.equal(model.connection, 'sqlite') + }) + + test('consume spread arg', ({ assert }) => { + class MakeModel extends BaseCommand { + names!: string[] + connection!: string + } + + MakeModel.defineArgument('names', { type: 'spread' }) + MakeModel.defineFlag('connection', { type: 'string' }) + + const parsed = new Parser(MakeModel.getParserOptions()).parse('user post --connection=sqlite') + + const kernel = new Kernel() + const model = MakeModel.create(kernel, parsed, cliui()) + + assert.deepEqual(model.names, ['user', 'post']) + assert.equal(model.connection, 'sqlite') + }) +}) + +test.group('Base command | consume flags', () => { + test('consume boolean flag', ({ assert }) => { + class MakeModel extends BaseCommand { + name!: string + connection!: string + dropAll!: boolean + } + + MakeModel.defineArgument('name', { type: 'string' }) + MakeModel.defineFlag('connection', { type: 'string' }) + MakeModel.defineFlag('dropAll', { type: 'boolean', default: true }) + + const parsed = new Parser(MakeModel.getParserOptions()).parse( + 'user --connection=sqlite --drop-all' + ) + + const kernel = new Kernel() + const model = MakeModel.create(kernel, parsed, cliui()) + + assert.equal(model.name, 'user') + assert.equal(model.connection, 'sqlite') + assert.isTrue(model.dropAll) + }) + + test('consume array flag', ({ assert }) => { + class MakeModel extends BaseCommand { + name!: string + connections!: string[] + dropAll!: boolean + } + + MakeModel.defineArgument('name', { type: 'string' }) + MakeModel.defineFlag('connections', { type: 'array' }) + MakeModel.defineFlag('dropAll', { type: 'boolean' }) + + const parsed = new Parser(MakeModel.getParserOptions()).parse( + 'user --connections=sqlite --connections=mysql' + ) + + const kernel = new Kernel() + const model = MakeModel.create(kernel, parsed, cliui()) + + assert.equal(model.name, 'user') + assert.deepEqual(model.connections, ['sqlite', 'mysql']) + assert.isUndefined(model.dropAll) + }) + + test('use default value when array flag is missing', ({ assert }) => { + class MakeModel extends BaseCommand { + name!: string + connections!: string[] + dropAll!: boolean + } + + MakeModel.defineArgument('name', { type: 'string' }) + MakeModel.defineFlag('connections', { type: 'array', default: ['sqlite'] }) + MakeModel.defineFlag('dropAll', { type: 'boolean' }) + + const parsed = new Parser(MakeModel.getParserOptions()).parse('user') + + const kernel = new Kernel() + const model = MakeModel.create(kernel, parsed, cliui()) + + assert.equal(model.name, 'user') + assert.deepEqual(model.connections, ['sqlite']) + assert.isUndefined(model.dropAll) + }) +}) diff --git a/tests/base_command/serialize.spec.ts b/tests/base_command/serialize.spec.ts new file mode 100644 index 0000000..117442b --- /dev/null +++ b/tests/base_command/serialize.spec.ts @@ -0,0 +1,138 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { BaseCommand } from '../../src/commands/base.js' + +test.group('Base command | serialize', () => { + test('serialize command', ({ assert }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'migrate' + static description: string = 'Migrate db' + } + + assert.deepEqual(MakeModel.serialize(), { + commandName: 'migrate', + namespace: null, + description: 'Migrate db', + help: '', + args: [], + flags: [], + aliases: [], + options: { + allowUnknownFlags: false, + handlesSignals: false, + staysAlive: false, + }, + }) + }) + + test('serialize command with namespaced name', ({ assert }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'make:model' + static description: string = 'Make a new model' + } + + assert.deepEqual(MakeModel.serialize(), { + commandName: 'make:model', + namespace: 'make', + description: 'Make a new model', + help: '', + args: [], + flags: [], + aliases: [], + options: { + allowUnknownFlags: false, + handlesSignals: false, + staysAlive: false, + }, + }) + }) + + test('serialize command with args', ({ assert }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'make:model' + static description: string = 'Make a new model' + } + + MakeModel.defineArgument('name', { type: 'string', description: 'Name of the argument' }) + + assert.deepEqual(MakeModel.serialize(), { + commandName: 'make:model', + namespace: 'make', + description: 'Make a new model', + help: '', + args: [ + { + name: 'name', + argumentName: 'name', + required: true, + type: 'string', + description: 'Name of the argument', + }, + ], + flags: [], + aliases: [], + options: { + allowUnknownFlags: false, + handlesSignals: false, + staysAlive: false, + }, + }) + }) + + test('serialize command with flags', ({ assert }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'make:model' + static description: string = 'Make a new model' + } + + MakeModel.defineArgument('name', { type: 'string', description: 'Name of the argument' }) + MakeModel.defineFlag('verbose', { type: 'boolean' }) + + assert.deepEqual(MakeModel.serialize(), { + commandName: 'make:model', + namespace: 'make', + help: '', + description: 'Make a new model', + args: [ + { + name: 'name', + argumentName: 'name', + required: true, + type: 'string', + description: 'Name of the argument', + }, + ], + flags: [ + { + name: 'verbose', + flagName: 'verbose', + required: false, + type: 'boolean', + }, + ], + options: { + allowUnknownFlags: false, + handlesSignals: false, + staysAlive: false, + }, + aliases: [], + }) + }) + + test('error when command does not have a name', ({ assert }) => { + class MakeModel extends BaseCommand {} + + assert.throws( + () => MakeModel.serialize(), + 'Cannot serialize command "MakeModel". Missing static property "commandName"' + ) + }) +}) diff --git a/tests/base_command/validate.spec.ts b/tests/base_command/validate.spec.ts new file mode 100644 index 0000000..71c3e36 --- /dev/null +++ b/tests/base_command/validate.spec.ts @@ -0,0 +1,458 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { Parser } from '../../src/parser.js' +import { CommandOptions } from '../../src/types.js' +import { BaseCommand } from '../../src/commands/base.js' + +test.group('Base command | validate args', () => { + test('fail when required argument value is missing', ({ assert }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'migrate' + static description: string = 'Migrate db' + } + + MakeModel.defineArgument('name', { type: 'string' }) + const output = new Parser(MakeModel.getParserOptions()).parse('') + + assert.throws(() => MakeModel.validate(output), 'Missing required argument "name"') + }) + + test('allow missing value when argument is optional', ({ assert }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'migrate' + static description: string = 'Migrate db' + } + + MakeModel.defineArgument('name', { type: 'string' }) + MakeModel.defineArgument('connection', { type: 'string', required: false }) + const output = new Parser(MakeModel.getParserOptions()).parse('user') + + assert.doesNotThrows(() => MakeModel.validate(output)) + }) + + test('fail when spread argument value is missing', ({ assert }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'migrate' + static description: string = 'Migrate db' + } + + MakeModel.defineArgument('name', { type: 'string' }) + MakeModel.defineArgument('connections', { type: 'spread' }) + const output = new Parser(MakeModel.getParserOptions()).parse('user') + + assert.throws(() => MakeModel.validate(output), 'Missing required argument "connections"') + }) + + test('allow missing value for optional spread argument', ({ assert }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'migrate' + static description: string = 'Migrate db' + } + + MakeModel.defineArgument('name', { type: 'string' }) + MakeModel.defineArgument('connections', { type: 'spread', required: false }) + const output = new Parser(MakeModel.getParserOptions()).parse('user') + + assert.doesNotThrows(() => MakeModel.validate(output)) + }) + + test('fail when optional argument receives empty value', ({ assert }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'migrate' + static description: string = 'Migrate db' + } + + MakeModel.defineArgument('name', { type: 'string', required: false }) + const output = new Parser(MakeModel.getParserOptions()).parse(['']) + + assert.throws(() => MakeModel.validate(output), 'Missing value for argument "name"') + }) + + test('fail when required argument receives empty value', ({ assert }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'migrate' + static description: string = 'Migrate db' + } + + MakeModel.defineArgument('name', { type: 'string', required: true }) + const output = new Parser(MakeModel.getParserOptions()).parse(['']) + + assert.throws(() => MakeModel.validate(output), 'Missing value for argument "name"') + }) + + test('work fine when required argument allows empty values', ({ assert }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'migrate' + static description: string = 'Migrate db' + } + + MakeModel.defineArgument('name', { type: 'string', required: true, allowEmptyValue: true }) + const output = new Parser(MakeModel.getParserOptions()).parse(['']) + + assert.doesNotThrows(() => MakeModel.validate(output)) + }) + + test('work fine when optional argument allows empty values', ({ assert }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'migrate' + static description: string = 'Migrate db' + } + + MakeModel.defineArgument('name', { type: 'string', required: false, allowEmptyValue: true }) + const output = new Parser(MakeModel.getParserOptions()).parse(['']) + + assert.doesNotThrows(() => MakeModel.validate(output)) + }) +}) + +test.group('Base command | validate unknown flags', () => { + test('fail when unknown flags are specified', ({ assert }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'migrate' + static description: string = 'Migrate db' + } + + MakeModel.defineFlag('connection', { type: 'string' }) + MakeModel.defineArgument('name', { type: 'string' }) + + const output = new Parser(MakeModel.getParserOptions()).parse( + 'foo --connection=sqlite --drop-all' + ) + + assert.throws(() => MakeModel.validate(output), 'Unknown flag "--drop-all"') + }) + + test('fail when unknown shorthand flags are specified', ({ assert }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'migrate' + static description: string = 'Migrate db' + } + + MakeModel.defineFlag('connection', { type: 'string' }) + MakeModel.defineArgument('name', { type: 'string' }) + + const output = new Parser(MakeModel.getParserOptions()).parse('foo --connection=sqlite -d') + assert.throws(() => MakeModel.validate(output), 'Unknown flag "-d"') + }) + + test('do not fail when argument value starts with --', ({ assert }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'migrate' + static description: string = 'Migrate db' + } + + MakeModel.defineFlag('connection', { type: 'string' }) + MakeModel.defineArgument('name', { type: 'string' }) + + const output = new Parser(MakeModel.getParserOptions()).parse('"--da" --connection=sqlite') + assert.doesNotThrows(() => MakeModel.validate(output), 'Unknown flag "-da"') + }) + + test('allow unknown flags', ({ assert }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'migrate' + static description: string = 'Migrate db' + static options: CommandOptions = { + allowUnknownFlags: true, + } + } + + MakeModel.defineFlag('connection', { type: 'string' }) + MakeModel.defineArgument('name', { type: 'string' }) + + const output = new Parser(MakeModel.getParserOptions()).parse('user --connection=sqlite -da') + assert.doesNotThrows(() => MakeModel.validate(output)) + }) +}) + +test.group('Base command | validate string flag', () => { + test('fail when a required string flag is missing', ({ assert }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'migrate' + static description: string = 'Migrate db' + } + + MakeModel.defineFlag('connection', { type: 'string', required: true }) + const output = new Parser(MakeModel.getParserOptions()).parse('') + + assert.throws(() => MakeModel.validate(output), 'Missing required option "connection"') + }) + + test('fail when a required string flag is defined without any value', ({ assert }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'migrate' + static description: string = 'Migrate db' + } + + MakeModel.defineFlag('connection', { type: 'string', required: true }) + const output = new Parser(MakeModel.getParserOptions()).parse('--connection') + + assert.throws(() => MakeModel.validate(output), 'Missing value for option "connection"') + }) + + test('work fine when an optional string is missing', ({ assert }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'migrate' + static description: string = 'Migrate db' + } + + MakeModel.defineFlag('connection', { type: 'string' }) + const output = new Parser(MakeModel.getParserOptions()).parse('') + + assert.doesNotThrows(() => MakeModel.validate(output)) + }) + + test('fail when a optional string flag is defined without any value', ({ assert }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'migrate' + static description: string = 'Migrate db' + } + + MakeModel.defineFlag('connection', { type: 'string' }) + const output = new Parser(MakeModel.getParserOptions()).parse('--connection') + + assert.throws(() => MakeModel.validate(output), 'Missing value for option "connection"') + }) + + test('use default value when flag is not mentioned', ({ assert }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'migrate' + static description: string = 'Migrate db' + } + + MakeModel.defineFlag('connection', { type: 'string', required: true, default: 'sqlite' }) + const output = new Parser(MakeModel.getParserOptions()).parse('') + + assert.equal(output.flags.connection, 'sqlite') + assert.doesNotThrows(() => MakeModel.validate(output)) + }) + + test('work fine when an optional string flag is mentioned without value and empty values are allowed', ({ + assert, + }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'migrate' + static description: string = 'Migrate db' + } + + MakeModel.defineFlag('connection', { type: 'string', allowEmptyValue: true }) + const output = new Parser(MakeModel.getParserOptions()).parse('--connection') + + assert.doesNotThrows(() => MakeModel.validate(output)) + }) + + test('fail when required string flag is not mentioned and empty values are allowed', ({ + assert, + }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'migrate' + static description: string = 'Migrate db' + } + + MakeModel.defineFlag('connection', { type: 'string', allowEmptyValue: true, required: true }) + const output = new Parser(MakeModel.getParserOptions()).parse('') + + assert.throws(() => MakeModel.validate(output), 'Missing required option "connection"') + }) +}) + +test.group('Base command | validate numeric flag', () => { + test('fail when a required numeric flag is missing', ({ assert }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'migrate' + static description: string = 'Migrate db' + } + + MakeModel.defineFlag('batchSize', { type: 'number', required: true }) + const output = new Parser(MakeModel.getParserOptions()).parse('') + + assert.throws(() => MakeModel.validate(output), 'Missing required option "batch-size"') + }) + + test('fail when a required numeric flag is defined without any value', ({ assert }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'migrate' + static description: string = 'Migrate db' + } + + MakeModel.defineFlag('batchSize', { type: 'number', required: true }) + const output = new Parser(MakeModel.getParserOptions()).parse('--batch-size') + + assert.throws(() => MakeModel.validate(output), 'Missing value for option "batch-size"') + }) + + test('work fine when an optional numeric flag is missing', ({ assert }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'migrate' + static description: string = 'Migrate db' + } + + MakeModel.defineFlag('batchSize', { type: 'number' }) + const output = new Parser(MakeModel.getParserOptions()).parse('') + + assert.doesNotThrows(() => MakeModel.validate(output)) + }) + + test('fail when an optional numeric flag is defined without any value', ({ assert }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'migrate' + static description: string = 'Migrate db' + } + + MakeModel.defineFlag('batchSize', { type: 'number' }) + const output = new Parser(MakeModel.getParserOptions()).parse('--batch-size') + + assert.throws(() => MakeModel.validate(output), 'Missing value for option "batch-size"') + }) + + test('fail when numeric flag value is not a valid number', ({ assert }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'migrate' + static description: string = 'Migrate db' + } + + MakeModel.defineFlag('batchSize', { type: 'number' }) + const output = new Parser(MakeModel.getParserOptions()).parse('--batch-size=foo') + + assert.throws( + () => MakeModel.validate(output), + 'Invalid value. The "batch-size" flag accepts a "numeric" value' + ) + }) +}) + +test.group('Base command | validate boolean flag', () => { + test('ignore values next to a boolean flag', ({ assert }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'migrate' + static description: string = 'Migrate db' + } + + MakeModel.defineFlag('dropAll', { type: 'boolean', required: true }) + const output = new Parser(MakeModel.getParserOptions()).parse('--drop-all=no') + + assert.doesNotThrows(() => MakeModel.validate(output)) + }) + + test('set value to true when boolean flag is mentioned', ({ assert }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'migrate' + static description: string = 'Migrate db' + } + + MakeModel.defineFlag('dropAll', { type: 'boolean', required: true }) + const output = new Parser(MakeModel.getParserOptions()).parse('--drop-all') + assert.isTrue(output.flags['drop-all']) + assert.doesNotThrows(() => MakeModel.validate(output)) + }) + + test('fail when a required boolean flag is missing', ({ assert }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'migrate' + static description: string = 'Migrate db' + } + + MakeModel.defineFlag('dropAll', { type: 'boolean', required: true }) + const output = new Parser(MakeModel.getParserOptions()).parse('') + + assert.throws(() => MakeModel.validate(output), 'Missing required option "drop-all"') + }) + + test('work fine when an optional boolean flag is missing', ({ assert }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'migrate' + static description: string = 'Migrate db' + } + + MakeModel.defineFlag('dropAll', { type: 'boolean' }) + const output = new Parser(MakeModel.getParserOptions()).parse('') + + assert.doesNotThrows(() => MakeModel.validate(output)) + }) +}) + +test.group('Base command | validate array flag', () => { + test('fail when a required array flag is missing', ({ assert }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'migrate' + static description: string = 'Migrate db' + } + + MakeModel.defineFlag('connection', { type: 'array', required: true }) + const output = new Parser(MakeModel.getParserOptions()).parse('') + + assert.throws(() => MakeModel.validate(output), 'Missing required option "connection"') + }) + + test('fail when a required array flag is mentioned with no value', ({ assert }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'migrate' + static description: string = 'Migrate db' + } + + MakeModel.defineFlag('connections', { type: 'array', required: true }) + const output = new Parser(MakeModel.getParserOptions()).parse('--connections') + + assert.throws(() => MakeModel.validate(output), 'Missing value for option "connections"') + }) + + test('fail when a required array flag is mentioned multiple times with no value', ({ + assert, + }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'migrate' + static description: string = 'Migrate db' + } + + MakeModel.defineFlag('connections', { type: 'array', required: true }) + const output = new Parser(MakeModel.getParserOptions()).parse('--connections --connections') + + assert.throws(() => MakeModel.validate(output), 'Missing value for option "connections"') + }) + + test('when fine when an optional array flag is missing', ({ assert }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'migrate' + static description: string = 'Migrate db' + } + + MakeModel.defineFlag('connection', { type: 'array' }) + const output = new Parser(MakeModel.getParserOptions()).parse('') + + assert.doesNotThrows(() => MakeModel.validate(output)) + }) + + test('fail when an optional array flag is mentioned with no value', ({ assert }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'migrate' + static description: string = 'Migrate db' + } + + MakeModel.defineFlag('connections', { type: 'array', required: false }) + const output = new Parser(MakeModel.getParserOptions()).parse('--connections --connections') + + assert.throws(() => MakeModel.validate(output), 'Missing value for option "connections"') + }) + + test('work fine when an optional array flag is mentioned without value and empty values are allowed', ({ + assert, + }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'migrate' + static description: string = 'Migrate db' + } + + MakeModel.defineFlag('connections', { type: 'array', required: false, allowEmptyValue: true }) + const output = new Parser(MakeModel.getParserOptions()).parse('--connections --connections') + + assert.doesNotThrows(() => MakeModel.validate(output)) + }) +}) diff --git a/tests/commands/list.spec.ts b/tests/commands/list.spec.ts new file mode 100644 index 0000000..96c72ec --- /dev/null +++ b/tests/commands/list.spec.ts @@ -0,0 +1,216 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { Kernel } from '../../src/kernel.js' +import { args } from '../../src/decorators/args.js' +import { flags } from '../../src/decorators/flags.js' +import { CommandsList } from '../../src/loaders/list.js' +import { BaseCommand } from '../../src/commands/base.js' + +test.group('List command', () => { + test('show list of all the registered commands', async ({ assert }) => { + const kernel = new Kernel() + kernel.ui.switchMode('raw') + + class Serve extends BaseCommand { + static commandName: string = 'serve' + static description: string = 'Start the AdonisJS HTTP server' + } + + class MakeController extends BaseCommand { + @args.string({ description: 'Name of the controller' }) + name!: string + + @flags.boolean({ description: 'Add resourceful methods', default: false }) + resource!: boolean + + static aliases: string[] = ['mc'] + static commandName: string = 'make:controller' + static description: string = 'Make a new HTTP controller' + } + + kernel.addLoader(new CommandsList([Serve, MakeController])) + const command = await kernel.exec('list', []) + + assert.equal(command.exitCode, 0) + assert.deepEqual(kernel.ui.logger.getLogs(), [ + { + message: '', + stream: 'stdout', + }, + { + message: 'yellow(Available commands:)', + stream: 'stdout', + }, + { + message: [ + ' green(list) dim(View list of available commands)', + ' green(serve) dim(Start the AdonisJS HTTP server)', + ].join('\n'), + stream: 'stdout', + }, + { + message: '', + stream: 'stdout', + }, + { + message: 'yellow(make)', + stream: 'stdout', + }, + { + message: ' green(make:controller) dim((mc)) dim(Make a new HTTP controller)', + stream: 'stdout', + }, + ]) + }) + + test('show list of all the registered commands for a namespace', async ({ assert }) => { + const kernel = new Kernel() + kernel.ui.switchMode('raw') + + class Serve extends BaseCommand { + static commandName: string = 'serve' + static description: string = 'Start the AdonisJS HTTP server' + } + + class MakeController extends BaseCommand { + @args.string({ description: 'Name of the controller' }) + name!: string + + @flags.boolean({ description: 'Add resourceful methods', default: false }) + resource!: boolean + + static aliases: string[] = ['mc'] + static commandName: string = 'make:controller' + static description: string = 'Make a new HTTP controller' + } + + kernel.addLoader(new CommandsList([Serve, MakeController])) + const command = await kernel.exec('list', ['make']) + + assert.equal(command.exitCode, 0) + assert.deepEqual(kernel.ui.logger.getLogs(), [ + { + message: '', + stream: 'stdout', + }, + { + message: 'yellow(make)', + stream: 'stdout', + }, + { + message: ' green(make:controller) dim((mc)) dim(Make a new HTTP controller)', + stream: 'stdout', + }, + ]) + }) + + test('display error when namespace is invalid', async ({ assert }) => { + const kernel = new Kernel() + kernel.ui.switchMode('raw') + + class Serve extends BaseCommand { + static commandName: string = 'serve' + static description: string = 'Start the AdonisJS HTTP server' + } + + class MakeController extends BaseCommand { + @args.string({ description: 'Name of the controller' }) + name!: string + + @flags.boolean({ description: 'Add resourceful methods', default: false }) + resource!: boolean + + static aliases: string[] = ['mc'] + static commandName: string = 'make:controller' + static description: string = 'Make a new HTTP controller' + } + + kernel.addLoader(new CommandsList([Serve, MakeController])) + const command = await kernel.exec('list', ['foo']) + + assert.equal(command.exitCode, 1) + assert.deepEqual(kernel.ui.logger.getLogs(), [ + { + message: 'red(Namespace "foo" is not defined)', + stream: 'stderr', + }, + ]) + }) + + test('show list of all kernel global flags', async ({ assert }) => { + const kernel = new Kernel() + kernel.ui.switchMode('raw') + + class Serve extends BaseCommand { + static commandName: string = 'serve' + static description: string = 'Start the AdonisJS HTTP server' + } + + class MakeController extends BaseCommand { + @args.string({ description: 'Name of the controller' }) + name!: string + + @flags.boolean({ description: 'Add resourceful methods', default: false }) + resource!: boolean + + static aliases: string[] = ['mc'] + static commandName: string = 'make:controller' + static description: string = 'Make a new HTTP controller' + } + + kernel.addLoader(new CommandsList([Serve, MakeController])) + kernel.defineFlag('help', { type: 'boolean', description: 'View help of a given command' }) + const command = await kernel.exec('list', []) + + assert.equal(command.exitCode, 0) + assert.deepEqual(kernel.ui.logger.getLogs(), [ + { + message: '', + stream: 'stdout', + }, + { + message: 'yellow(Options:)', + stream: 'stdout', + }, + { + message: ' green(--help) dim(View help of a given command)', + stream: 'stdout', + }, + { + message: '', + stream: 'stdout', + }, + { + message: 'yellow(Available commands:)', + stream: 'stdout', + }, + { + message: [ + ' green(list) dim(View list of available commands)', + ' green(serve) dim(Start the AdonisJS HTTP server)', + ].join('\n'), + stream: 'stdout', + }, + { + message: '', + stream: 'stdout', + }, + { + message: 'yellow(make)', + stream: 'stdout', + }, + { + message: ' green(make:controller) dim((mc)) dim(Make a new HTTP controller)', + stream: 'stdout', + }, + ]) + }) +}) diff --git a/tests/decorators/args.spec.ts b/tests/decorators/args.spec.ts new file mode 100644 index 0000000..0a05a14 --- /dev/null +++ b/tests/decorators/args.spec.ts @@ -0,0 +1,71 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { args } from '../../src/decorators/args.js' +import { BaseCommand } from '../../src/commands/base.js' + +test.group('Decorators | args', () => { + test('define string argument', ({ assert }) => { + class MakeModel extends BaseCommand { + @args.string() + name!: string + } + + assert.deepEqual(MakeModel.args, [ + { name: 'name', required: true, type: 'string', argumentName: 'name' }, + ]) + }) + + test('define argument with inheritance', ({ assert }) => { + class MakeEntity extends BaseCommand { + @args.string() + name!: string + } + + class MakeModel extends MakeEntity { + @args.string() + dbConnection!: string + } + + class MakeController extends MakeEntity { + @args.string() + resourceName!: string + } + + assert.deepEqual(MakeModel.args, [ + { name: 'name', required: true, type: 'string', argumentName: 'name' }, + { name: 'dbConnection', required: true, type: 'string', argumentName: 'db-connection' }, + ]) + + assert.deepEqual(MakeController.args, [ + { name: 'name', required: true, type: 'string', argumentName: 'name' }, + { name: 'resourceName', required: true, type: 'string', argumentName: 'resource-name' }, + ]) + }) + test('define spread argument', ({ assert }) => { + class MakeModel extends BaseCommand { + @args.string() + name!: string + + @args.spread() + connections!: string[] + } + + assert.deepEqual(MakeModel.args, [ + { name: 'name', required: true, type: 'string', argumentName: 'name' }, + { + name: 'connections', + required: true, + type: 'spread', + argumentName: 'connections', + }, + ]) + }) +}) diff --git a/tests/decorators/flags.spec.ts b/tests/decorators/flags.spec.ts new file mode 100644 index 0000000..5745d76 --- /dev/null +++ b/tests/decorators/flags.spec.ts @@ -0,0 +1,42 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { BaseCommand } from '../../src/commands/base.js' +import { flags } from '../../src/decorators/flags.js' + +test.group('Base command | flags', () => { + test('define flags using decorators', ({ assert }) => { + class MakeModel extends BaseCommand { + @flags.string() + connection?: string + + @flags.boolean() + dropAll?: boolean + + @flags.number() + batchSize?: number + + @flags.array() + files?: string[] + } + + assert.deepEqual(MakeModel.getParserOptions().flagsParserOptions, { + all: ['connection', 'drop-all', 'batch-size', 'files'], + string: ['connection'], + boolean: ['drop-all'], + array: ['files'], + number: ['batch-size'], + alias: {}, + count: [], + coerce: {}, + default: {}, + }) + }) +}) diff --git a/tests/formatters/arg.spec.ts b/tests/formatters/arg.spec.ts new file mode 100644 index 0000000..7d792fd --- /dev/null +++ b/tests/formatters/arg.spec.ts @@ -0,0 +1,90 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { colors } from '@poppinss/cliui' +import { args } from '../../src/decorators/args.js' +import { BaseCommand } from '../../src/commands/base.js' +import { ArgumentFormatter } from '../../src/formatters/argument.js' + +test.group('Formatters | arg', () => { + test('format string arg', ({ assert }) => { + class MakeController extends BaseCommand { + @args.string() + name!: string + } + + const formatter = new ArgumentFormatter(MakeController.args[0], colors.raw()) + assert.equal(formatter.formatOption(), 'dim()') + assert.equal(formatter.formatListOption(), ' green(name) ') + }) + + test('format optional string arg', ({ assert }) => { + class MakeController extends BaseCommand { + @args.string({ required: false }) + name!: string + } + + const formatter = new ArgumentFormatter(MakeController.args[0], colors.raw()) + assert.equal(formatter.formatOption(), 'dim([])') + assert.equal(formatter.formatListOption(), ' green([name]) ') + }) + + test('format spread arg', ({ assert }) => { + class MakeController extends BaseCommand { + @args.spread() + name!: string[] + } + + const formatter = new ArgumentFormatter(MakeController.args[0], colors.raw()) + assert.equal(formatter.formatOption(), 'dim()') + assert.equal(formatter.formatListOption(), ' green(name...) ') + }) + + test('format optional spread arg', ({ assert }) => { + class MakeController extends BaseCommand { + @args.spread({ required: false }) + name!: string[] + } + + const formatter = new ArgumentFormatter(MakeController.args[0], colors.raw()) + assert.equal(formatter.formatOption(), 'dim([])') + assert.equal(formatter.formatListOption(), ' green([name...]) ') + }) + + test('format arg description', ({ assert }) => { + class MakeController extends BaseCommand { + @args.string({ description: 'The name of the controller' }) + name!: string + } + + const formatter = new ArgumentFormatter(MakeController.args[0], colors.raw()) + assert.equal(formatter.formatDescription(), 'dim(The name of the controller)') + }) + + test('format description with flag default value', ({ assert }) => { + class MakeController extends BaseCommand { + @args.string({ description: 'The name of the controller', default: 'posts' }) + name!: string + } + + const formatter = new ArgumentFormatter(MakeController.args[0], colors.raw()) + assert.equal(formatter.formatDescription(), 'dim(The name of the controller [default: posts])') + }) + + test('format empty description with flag default value', ({ assert }) => { + class MakeController extends BaseCommand { + @args.string({ default: 'posts' }) + name!: string + } + + const formatter = new ArgumentFormatter(MakeController.args[0], colors.raw()) + assert.equal(formatter.formatDescription(), 'dim([default: posts])') + }) +}) diff --git a/tests/formatters/command.spec.ts b/tests/formatters/command.spec.ts new file mode 100644 index 0000000..cc331fa --- /dev/null +++ b/tests/formatters/command.spec.ts @@ -0,0 +1,237 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { colors } from '@poppinss/cliui' +import { BaseCommand } from '../../src/commands/base.js' +import { args } from '../../src/decorators/args.js' +import { flags } from '../../src/decorators/flags.js' +import { CommandFormatter } from '../../src/formatters/command.js' + +test.group('Formatters | command', () => { + test('format command description', ({ assert }) => { + class MakeController extends BaseCommand { + static commandName: string = 'make:controller' + static description: string = 'Make an HTTP controller' + } + + const formatter = new CommandFormatter(MakeController.serialize(), colors.raw()) + assert.equal(formatter.formatDescription(), 'Make an HTTP controller') + assert.equal(formatter.formatListDescription(), 'dim(Make an HTTP controller)') + }) + + test('return empty string when command has no description', ({ assert }) => { + class MakeController extends BaseCommand { + static commandName: string = 'make:controller' + } + + const formatter = new CommandFormatter(MakeController.serialize(), colors.raw()) + assert.equal(formatter.formatDescription(), '') + assert.equal(formatter.formatListDescription(), '') + }) + + test('format command name for listing', ({ assert }) => { + class MakeController extends BaseCommand { + static commandName: string = 'make:controller' + static description: string = 'Make an HTTP controller' + } + + const formatter = new CommandFormatter(MakeController.serialize(), colors.raw()) + assert.equal(formatter.formatListName([]), ' green(make:controller) ') + }) + + test('format command name with aliases for listing', ({ assert }) => { + class MakeController extends BaseCommand { + static commandName: string = 'make:controller' + static description: string = 'Make an HTTP controller' + } + + const formatter = new CommandFormatter(MakeController.serialize(), colors.raw()) + assert.equal(formatter.formatListName(['mc']), ' green(make:controller) dim((mc)) ') + }) + + test('format command usage', ({ assert }) => { + class MakeController extends BaseCommand { + static commandName: string = 'make:controller' + static description: string = 'Make an HTTP controller' + } + + const formatter = new CommandFormatter(MakeController.serialize(), colors.raw()) + assert.deepEqual(formatter.formatUsage([]), [' make:controller ']) + }) + + test('format command usage that accepts args', ({ assert }) => { + class MakeController extends BaseCommand { + static commandName: string = 'make:controller' + static description: string = 'Make an HTTP controller' + + @args.string() + name!: string + } + + const formatter = new CommandFormatter(MakeController.serialize(), colors.raw()) + assert.deepEqual(formatter.formatUsage([]), [' make:controller dim()']) + }) + + test('format command usage that accepts flags', ({ assert }) => { + class MakeController extends BaseCommand { + static commandName: string = 'make:controller' + static description: string = 'Make an HTTP controller' + + @args.string() + name!: string + + @flags.boolean() + resource!: boolean + } + + const formatter = new CommandFormatter(MakeController.serialize(), colors.raw()) + assert.deepEqual(formatter.formatUsage([]), [ + ' make:controller dim([options]) dim([--]) dim()', + ]) + }) + + test('format command usage that accepts just flags', ({ assert }) => { + class MakeController extends BaseCommand { + static commandName: string = 'make:controller' + static description: string = 'Make an HTTP controller' + + @flags.boolean() + resource!: boolean + } + + const formatter = new CommandFormatter(MakeController.serialize(), colors.raw()) + assert.deepEqual(formatter.formatUsage([]), [' make:controller dim([options])']) + }) + + test('format command usage with aliases', ({ assert }) => { + class MakeController extends BaseCommand { + static commandName: string = 'make:controller' + static description: string = 'Make an HTTP controller' + + @args.string() + name!: string + + @flags.boolean() + resource!: boolean + } + + const formatter = new CommandFormatter(MakeController.serialize(), colors.raw()) + assert.deepEqual(formatter.formatUsage(['mc']), [ + ' make:controller dim([options]) dim([--]) dim()', + ' mc dim([options]) dim([--]) dim()', + ]) + }) + + test('format command usage with binary name', ({ assert }) => { + class MakeController extends BaseCommand { + static commandName: string = 'make:controller' + static description: string = 'Make an HTTP controller' + + @args.string() + name!: string + + @flags.boolean() + resource!: boolean + } + + const formatter = new CommandFormatter(MakeController.serialize(), colors.raw()) + assert.deepEqual(formatter.formatUsage(['mc'], 'node ace'), [ + ' node ace make:controller dim([options]) dim([--]) dim()', + ' node ace mc dim([options]) dim([--]) dim()', + ]) + }) + + test('return empty string when command has no help text', ({ assert }) => { + class MakeController extends BaseCommand { + static commandName: string = 'make:controller' + + @args.string() + name!: string + + @flags.boolean() + resource!: boolean + } + + const formatter = new CommandFormatter(MakeController.serialize(), colors.raw()) + assert.deepEqual(formatter.formatHelp(undefined, 80), '') + }) + + test('format command help text', ({ assert }) => { + class MakeController extends BaseCommand { + static commandName: string = 'make:controller' + static help = 'Make a new HTTP controller make:controller ' + + @args.string() + name!: string + + @flags.boolean() + resource!: boolean + } + + const formatter = new CommandFormatter(MakeController.serialize(), colors.raw()) + assert.deepEqual( + formatter.formatHelp(undefined, 80), + ' Make a new HTTP controller make:controller ' + ) + }) + + test('subsitute binary name in help text', ({ assert }) => { + class MakeController extends BaseCommand { + static commandName: string = 'make:controller' + static help = 'Make a new HTTP controller {{binaryName}}make:controller ' + + @args.string() + name!: string + + @flags.boolean() + resource!: boolean + } + + const formatter = new CommandFormatter(MakeController.serialize(), colors.raw()) + assert.deepEqual( + formatter.formatHelp('node ace', 80), + ' Make a new HTTP controller node ace make:controller ' + ) + }) + + test('wrap command help text', ({ assert }) => { + class MakeController extends BaseCommand { + static commandName: string = 'make:controller' + static help = [ + 'Make a new HTTP controller', + 'make:controller ', + '', + 'To create a resourceful controller. Run the command with resource flag', + 'make:controller --resource', + ] + + @args.string() + name!: string + + @flags.boolean() + resource!: boolean + } + + const formatter = new CommandFormatter(MakeController.serialize(), colors.raw()) + assert.deepEqual( + formatter.formatHelp(undefined, 30), + [ + ' Make a new HTTP controller', + ' make:controller ', + ' ', + ' To create a resourceful', + ' controller. Run the command', + ' with resource flag', + ' make:controller ', + ' --resource', + ].join('\n') + ) + }) +}) diff --git a/tests/formatters/flag.spec.ts b/tests/formatters/flag.spec.ts new file mode 100644 index 0000000..538f90c --- /dev/null +++ b/tests/formatters/flag.spec.ts @@ -0,0 +1,186 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { colors } from '@poppinss/cliui' +import { flags } from '../../src/decorators/flags.js' +import { BaseCommand } from '../../src/commands/base.js' +import { FlagFormatter } from '../../src/formatters/flag.js' + +test.group('Formatters | flag', () => { + test('format string flag name', ({ assert }) => { + class MakeController extends BaseCommand { + @flags.string() + connection!: string + } + + const formatter = new FlagFormatter(MakeController.flags[0], colors.raw()) + assert.equal(formatter.formatOption(), ' green(--connection[=CONNECTION]) ') + }) + + test('format required string flag name', ({ assert }) => { + class MakeController extends BaseCommand { + @flags.string({ required: true }) + connection!: string + } + + const formatter = new FlagFormatter(MakeController.flags[0], colors.raw()) + assert.equal(formatter.formatOption(), ' green(--connection=CONNECTION) ') + }) + + test('format flag with alias', ({ assert }) => { + class MakeController extends BaseCommand { + @flags.string({ required: true, alias: 'c' }) + connection!: string + } + + const formatter = new FlagFormatter(MakeController.flags[0], colors.raw()) + assert.equal(formatter.formatOption(), ' green(-c, --connection=CONNECTION) ') + }) + + test('format flag with mutliple aliases', ({ assert }) => { + class MakeController extends BaseCommand { + @flags.string({ required: true, alias: ['c', 'o'] }) + connection!: string + } + + const formatter = new FlagFormatter(MakeController.flags[0], colors.raw()) + assert.equal(formatter.formatOption(), ' green(-c,-o, --connection=CONNECTION) ') + }) + + test('show negated flag', ({ assert }) => { + class MakeController extends BaseCommand { + @flags.boolean({ required: true, showNegatedVariantInHelp: true }) + resource!: boolean + } + + const formatter = new FlagFormatter(MakeController.flags[0], colors.raw()) + assert.equal(formatter.formatOption(), ' green(--resource|--no-resource) ') + }) + + test('format array flag name', ({ assert }) => { + class MakeController extends BaseCommand { + @flags.array() + connections!: string[] + } + + const formatter = new FlagFormatter(MakeController.flags[0], colors.raw()) + assert.equal(formatter.formatOption(), ' green(--connections[=CONNECTIONS...]) ') + }) + + test('format array flag with aliases', ({ assert }) => { + class MakeController extends BaseCommand { + @flags.array({ alias: ['c'] }) + connections!: string[] + } + + const formatter = new FlagFormatter(MakeController.flags[0], colors.raw()) + assert.equal(formatter.formatOption(), ' green(-c, --connections[=CONNECTIONS...]) ') + }) + + test('format required array flag name', ({ assert }) => { + class MakeController extends BaseCommand { + @flags.array({ required: true }) + connections!: string[] + } + + const formatter = new FlagFormatter(MakeController.flags[0], colors.raw()) + assert.equal(formatter.formatOption(), ' green(--connections=CONNECTIONS...) ') + }) + + test('format numeric flag name', ({ assert }) => { + class MakeController extends BaseCommand { + @flags.number() + actions!: number + } + + const formatter = new FlagFormatter(MakeController.flags[0], colors.raw()) + assert.equal(formatter.formatOption(), ' green(--actions[=ACTIONS]) ') + }) + + test('format numeric flag with alias', ({ assert }) => { + class MakeController extends BaseCommand { + @flags.number({ alias: 'a' }) + actions!: number + } + + const formatter = new FlagFormatter(MakeController.flags[0], colors.raw()) + assert.equal(formatter.formatOption(), ' green(-a, --actions[=ACTIONS]) ') + }) + + test('format required numeric flag name', ({ assert }) => { + class MakeController extends BaseCommand { + @flags.number({ required: true }) + actions!: number + } + + const formatter = new FlagFormatter(MakeController.flags[0], colors.raw()) + assert.equal(formatter.formatOption(), ' green(--actions=ACTIONS) ') + }) + + test('format boolean flag name', ({ assert }) => { + class MakeController extends BaseCommand { + @flags.boolean() + resource!: boolean + } + + const formatter = new FlagFormatter(MakeController.flags[0], colors.raw()) + assert.equal(formatter.formatOption(), ' green(--resource) ') + }) + + test('format boolean flag with alias', ({ assert }) => { + class MakeController extends BaseCommand { + @flags.boolean({ alias: ['r'] }) + resource!: boolean + } + + const formatter = new FlagFormatter(MakeController.flags[0], colors.raw()) + assert.equal(formatter.formatOption(), ' green(-r, --resource) ') + }) + + test('format required boolean flag name', ({ assert }) => { + class MakeController extends BaseCommand { + @flags.boolean({ required: true }) + resource!: boolean + } + + const formatter = new FlagFormatter(MakeController.flags[0], colors.raw()) + assert.equal(formatter.formatOption(), ' green(--resource) ') + }) + + test('format description', ({ assert }) => { + class MakeController extends BaseCommand { + @flags.boolean({ description: 'Generate resource actions' }) + resource!: boolean + } + + const formatter = new FlagFormatter(MakeController.flags[0], colors.raw()) + assert.equal(formatter.formatDescription(), 'dim(Generate resource actions)') + }) + + test('format description with flag default value', ({ assert }) => { + class MakeController extends BaseCommand { + @flags.boolean({ description: 'Generate resource actions', default: true }) + resource!: boolean + } + + const formatter = new FlagFormatter(MakeController.flags[0], colors.raw()) + assert.equal(formatter.formatDescription(), 'dim(Generate resource actions [default: true])') + }) + + test('format empty description with flag default value', ({ assert }) => { + class MakeController extends BaseCommand { + @flags.boolean({ default: true }) + resource!: boolean + } + + const formatter = new FlagFormatter(MakeController.flags[0], colors.raw()) + assert.equal(formatter.formatDescription(), 'dim([default: true])') + }) +}) diff --git a/tests/formatters/list.spec.ts b/tests/formatters/list.spec.ts new file mode 100644 index 0000000..726f9c0 --- /dev/null +++ b/tests/formatters/list.spec.ts @@ -0,0 +1,119 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { ListFormatter } from '../../src/formatters/list.js' + +test.group('Formatters | list', () => { + test('justify option column in all tables', ({ assert }) => { + const formatter = new ListFormatter([ + { + columns: [ + { + option: ' make:controller ', + description: 'Make a new HTTP controller', + }, + { + option: ' make:model ', + description: 'Make a new model', + }, + ], + heading: 'Commands', + }, + { + columns: [ + { + option: ' --env ', + description: 'Set env for command', + }, + { + option: ' --help ', + description: 'View help for a command', + }, + ], + heading: 'Options', + }, + ]) + + assert.deepEqual(formatter.format(80), [ + { + heading: 'Commands', + rows: [ + ' make:controller Make a new HTTP controller', + ' make:model Make a new model', + ], + }, + { + rows: [ + ' --env Set env for command', + ' --help View help for a command', + ], + heading: 'Options', + }, + ]) + }) + + test('wrap descriptions to newline', ({ assert }) => { + const formatter = new ListFormatter([ + { + columns: [ + { + option: ' serve ', + description: + 'Start the AdonisJS HTTP server, along with the file watcher. Also starts the webpack dev server when webpack encore is installed', + }, + { + option: ' make:controller ', + description: 'Make a new HTTP controller', + }, + { + option: ' make:model ', + description: 'Make a new model', + }, + ], + heading: 'Commands', + }, + { + columns: [ + { + option: ' --env ', + description: 'Set env for command', + }, + { + option: ' --help ', + description: 'View help for a command', + }, + ], + heading: 'Options', + }, + ]) + + assert.deepEqual(formatter.format(80), [ + { + heading: 'Commands', + rows: [ + [ + ' serve Start the AdonisJS HTTP server, along with the file watcher.', + ' Also starts the webpack dev server when webpack encore is', + ' installed', + ].join('\n'), + ' make:controller Make a new HTTP controller', + ' make:model Make a new model', + ], + }, + { + rows: [ + ' --env Set env for command', + ' --help View help for a command', + ], + heading: 'Options', + }, + ]) + }) +}) diff --git a/tests/helpers.spec.ts b/tests/helpers.spec.ts new file mode 100644 index 0000000..4570de9 --- /dev/null +++ b/tests/helpers.spec.ts @@ -0,0 +1,45 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { cliui } from '@poppinss/cliui' +import { renderErrorWithSuggestions, sortAlphabetically } from '../src/helpers.js' + +test.group('Helpers | Sort', () => { + test('sort values alphabetically', ({ assert }) => { + const values = ['hello', 'make', 'hi', 'run', 'hello'] + assert.deepEqual(values.sort(sortAlphabetically), ['hello', 'hello', 'hi', 'make', 'run']) + }) +}) + +test.group('Helpers | renderErrorWithSuggestions', () => { + test('render an error message with suggestions', ({ assert }) => { + const ui = cliui({ mode: 'raw' }) + renderErrorWithSuggestions(ui, 'Command "foo" is not defined', ['bar']) + + assert.deepEqual(ui.logger.getLogs(), [ + { + message: 'red(Command "foo" is not defined)\n\ndim(Did you mean?) bar', + stream: 'stderr', + }, + ]) + }) + + test('render an error message without suggestions', ({ assert }) => { + const ui = cliui({ mode: 'raw' }) + renderErrorWithSuggestions(ui, 'Command "foo" is not defined', []) + + assert.deepEqual(ui.logger.getLogs(), [ + { + message: 'red(Command "foo" is not defined)', + stream: 'stderr', + }, + ]) + }) +}) diff --git a/tests/kernel/boot.spec.ts b/tests/kernel/boot.spec.ts new file mode 100644 index 0000000..6b7a2b6 --- /dev/null +++ b/tests/kernel/boot.spec.ts @@ -0,0 +1,106 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' + +import { Kernel } from '../../src/kernel.js' +import { BaseCommand } from '../../src/commands/base.js' +import { CommandsList } from '../../src/loaders/list.js' + +test.group('Kernel | boot', () => { + test('load commands from loader during boot phase', async ({ assert }) => { + const kernel = new Kernel() + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + } + + class MakeModel extends BaseCommand { + static commandName = 'make:model' + } + + kernel.addLoader(new CommandsList([MakeController, MakeModel])) + await kernel.boot() + + assert.deepEqual(kernel.getCommands(), [ + kernel.getDefaultCommand().serialize(), + MakeController.serialize(), + MakeModel.serialize(), + ]) + }) + + test('multiple calls to boot method should be a noop', async ({ assert }) => { + const kernel = new Kernel() + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + } + + class MakeModel extends BaseCommand { + static commandName = 'make:model' + } + + kernel.addLoader(new CommandsList([MakeController, MakeModel])) + await kernel.boot() + await kernel.boot() + await kernel.boot() + + assert.deepEqual(kernel.getState(), 'booted') + assert.deepEqual(kernel.getCommands(), [ + kernel.getDefaultCommand().serialize(), + MakeController.serialize(), + MakeModel.serialize(), + ]) + }) + + test('collect namespaces from the loaded commands', async ({ assert }) => { + const kernel = new Kernel() + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + } + + class MakeModel extends BaseCommand { + static commandName = 'make:model' + } + + class MigrationRun extends BaseCommand { + static commandName = 'migration:run' + } + + kernel.addLoader(new CommandsList([MakeController, MakeModel, MigrationRun])) + await kernel.boot() + + assert.deepEqual(kernel.getNamespaces(), ['make', 'migration']) + }) + + test('collect command aliases', async ({ assert }) => { + const kernel = new Kernel() + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + static aliases: string[] = ['mc'] + } + + class MakeModel extends BaseCommand { + static commandName = 'make:model' + static aliases: string[] = ['mm'] + } + + class MigrationRun extends BaseCommand { + static commandName = 'migration:run' + static aliases: string[] = ['migrate'] + } + + kernel.addLoader(new CommandsList([MakeController, MakeModel, MigrationRun])) + await kernel.boot() + + assert.deepEqual(kernel.getAliases(), ['mc', 'mm', 'migrate']) + }) +}) diff --git a/tests/kernel/default_command.spec.ts b/tests/kernel/default_command.spec.ts new file mode 100644 index 0000000..d38cf9b --- /dev/null +++ b/tests/kernel/default_command.spec.ts @@ -0,0 +1,42 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' + +import { Kernel } from '../../src/kernel.js' +import { BaseCommand } from '../../src/commands/base.js' + +test.group('Kernel | default command', () => { + test('use a custom default command', async ({ assert }) => { + const kernel = new Kernel() + + class VerboseHelp extends BaseCommand { + static commandName = 'help' + } + + kernel.registerDefaultCommand(VerboseHelp) + + await kernel.boot() + assert.strictEqual(kernel.getDefaultCommand(), VerboseHelp) + }) + + test('disallow registering default command after kernel is booted', async ({ assert }) => { + const kernel = new Kernel() + await kernel.boot() + + class VerboseHelp extends BaseCommand { + static commandName = 'help' + } + + assert.throws( + () => kernel.registerDefaultCommand(VerboseHelp), + 'Cannot register default command in "booted" state' + ) + }) +}) diff --git a/tests/kernel/exec.spec.ts b/tests/kernel/exec.spec.ts new file mode 100644 index 0000000..9906ddb --- /dev/null +++ b/tests/kernel/exec.spec.ts @@ -0,0 +1,278 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { Kernel } from '../../src/kernel.js' +import { BaseCommand } from '../../src/commands/base.js' +import { CommandsList } from '../../src/loaders/list.js' + +test.group('Kernel | exec', () => { + test('execute command', async ({ assert }) => { + const kernel = new Kernel() + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + async run() { + return 'executed' + } + } + + kernel.addLoader(new CommandsList([MakeController])) + const command = await kernel.exec('make:controller', []) + + assert.equal(command.result, 'executed') + assert.equal(command.result, 'executed') + assert.equal(command.exitCode, 0) + + assert.isUndefined(kernel.exitCode) + assert.equal(kernel.getState(), 'booted') + }) + + test('run executing and executed hooks', async ({ assert, expectTypeOf }) => { + const kernel = new Kernel() + const stack: string[] = [] + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + async run() { + return 'executed' + } + } + + kernel.addLoader(new CommandsList([MakeController])) + kernel.executing((cmd) => { + expectTypeOf(cmd).toEqualTypeOf() + stack.push('executing') + }) + kernel.executed((cmd) => { + expectTypeOf(cmd).toEqualTypeOf() + stack.push('executed') + }) + + const command = await kernel.exec('make:controller', []) + assert.equal(command.result, 'executed') + assert.equal(command.exitCode, 0) + + assert.isUndefined(kernel.exitCode) + assert.equal(kernel.getState(), 'booted') + assert.deepEqual(stack, ['executing', 'executed']) + }) + + test('report error when command validation fails', async ({ assert }) => { + const kernel = new Kernel() + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + async run() { + return 'executed' + } + } + MakeController.defineArgument('name', { type: 'string' }) + + kernel.addLoader(new CommandsList([MakeController])) + await assert.rejects( + () => kernel.exec('make:controller', []), + 'Missing required argument "name"' + ) + }) + + test('report error when unable to find command', async ({ assert }) => { + const kernel = new Kernel() + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + async run() { + return 'executed' + } + } + MakeController.defineArgument('name', { type: 'string' }) + + kernel.addLoader(new CommandsList([MakeController])) + await assert.rejects(() => kernel.exec('foo', []), 'Command "foo" is not defined') + assert.isUndefined(kernel.exitCode) + assert.equal(kernel.getState(), 'booted') + }) + + test('report error when executing hook fails', async ({ assert }) => { + const kernel = new Kernel() + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + async run() { + return 'executed' + } + } + MakeController.defineArgument('name', { type: 'string' }) + + kernel.addLoader(new CommandsList([MakeController])) + kernel.executing(() => { + throw new Error('Pre hook failed') + }) + + await assert.rejects(() => kernel.exec('make:controller', ['users']), 'Pre hook failed') + }) + + test('report error when executed hook fails', async ({ assert }) => { + const kernel = new Kernel() + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + async run() { + return 'executed' + } + } + MakeController.defineArgument('name', { type: 'string' }) + + kernel.addLoader(new CommandsList([MakeController])) + kernel.executed(() => { + throw new Error('Post hook failed') + }) + + await assert.rejects(() => kernel.exec('make:controller', ['users']), 'Post hook failed') + }) + + test('do not allow termination from non-main commands', async ({ assert }) => { + const kernel = new Kernel() + const stack: string[] = [] + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + async run() { + await this.terminate() + return 'executed' + } + } + + MakeController.defineArgument('name', { type: 'string' }) + + kernel.addLoader(new CommandsList([MakeController])) + kernel.terminating(() => { + stack.push('terminating') + throw new Error('Never expected to run') + }) + + await kernel.exec('make:controller', ['users']) + assert.deepEqual(stack, []) + assert.isUndefined(kernel.exitCode) + assert.equal(kernel.getState(), 'booted') + }) + + test('use custom executor', async ({ assert }) => { + const stack: string[] = [] + + class MakeModel extends BaseCommand { + static commandName: string = 'make:model' + + name!: string + connection!: string + + async prepare() { + stack.push('prepare') + } + + async interact() { + stack.push('interact') + } + + async completed() { + stack.push('completed') + } + + async run() { + stack.push('run') + } + } + + MakeModel.defineArgument('name', { type: 'string' }) + MakeModel.defineFlag('connection', { type: 'string' }) + + const kernel = new Kernel() + kernel.addLoader(new CommandsList([MakeModel])) + + kernel.registerExecutor({ + create(Command, parsed, self) { + stack.push('creating') + return new Command(self, parsed, self.ui) + }, + run(command) { + stack.push('running') + return command.exec() + }, + }) + + await kernel.exec('make:model', ['users']) + assert.deepEqual(stack, ['creating', 'running', 'prepare', 'interact', 'run', 'completed']) + assert.isUndefined(kernel.exitCode) + assert.equal(kernel.getState(), 'booted') + }) + + test('do not register executor after kernel is booted', async ({ assert }) => { + const kernel = new Kernel() + await kernel.boot() + + assert.throws( + () => + kernel.registerExecutor({ + create(Command, parsed, self) { + return new Command(self, parsed, self.ui) + }, + run(command) { + return command.exec() + }, + }), + 'Cannot register commands executor in "booted" state' + ) + }) + + test('do not trigger flag listeners when not executing a main command', async ({ assert }) => { + const kernel = new Kernel() + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + async run() { + return 'executed' + } + } + + MakeController.defineFlag('connections', { type: 'string' }) + + kernel.addLoader(new CommandsList([MakeController])) + kernel.on('connections', () => { + throw new Error('Never expected to reach here') + }) + + const command = await kernel.exec('make:controller', ['--connections=sqlite']) + + assert.equal(command.result, 'executed') + assert.equal(command.result, 'executed') + assert.equal(command.exitCode, 0) + + assert.isUndefined(kernel.exitCode) + assert.equal(kernel.getState(), 'booted') + }) + + test('disallow using global flags when executing commands', async ({ assert }) => { + const kernel = new Kernel() + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + async run() { + return 'executed' + } + } + + kernel.addLoader(new CommandsList([MakeController])) + + kernel.defineFlag('help', { type: 'boolean' }) + await assert.rejects( + () => kernel.exec('make:controller', ['--help']), + 'Unknown flag "--help". The mentioned flag is not accepted by the command' + ) + }) +}) diff --git a/tests/kernel/find.spec.ts b/tests/kernel/find.spec.ts new file mode 100644 index 0000000..47f6fbd --- /dev/null +++ b/tests/kernel/find.spec.ts @@ -0,0 +1,180 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' + +import { Kernel } from '../../src/kernel.js' +import { BaseCommand } from '../../src/commands/base.js' +import { CommandsList } from '../../src/loaders/list.js' +import { CommandMetaData } from '../../src/types.js' + +test.group('Kernel | find', () => { + test('find commands registered using a loader', async ({ assert }) => { + const kernel = new Kernel() + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + } + + class MakeModel extends BaseCommand { + static commandName = 'make:model' + } + + kernel.addLoader(new CommandsList([MakeController, MakeModel])) + await kernel.boot() + + const command = await kernel.find('make:controller') + assert.strictEqual(command, MakeController) + }) + + test('find command using the command alias', async ({ assert }) => { + const kernel = new Kernel() + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + static aliases: string[] = ['mc'] + } + + class MakeModel extends BaseCommand { + static commandName = 'make:model' + } + + kernel.addLoader(new CommandsList([MakeController, MakeModel])) + kernel.addAlias('controller', 'make:controller') + await kernel.boot() + + assert.strictEqual(await kernel.find('mc'), MakeController) + assert.strictEqual(await kernel.find('controller'), MakeController) + }) + + test('raise error when unable to find command', async ({ assert }) => { + const kernel = new Kernel() + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + static aliases: string[] = ['mc'] + } + + class MakeModel extends BaseCommand { + static commandName = 'make:model' + } + + kernel.addLoader(new CommandsList([MakeController, MakeModel])) + await kernel.boot() + + await assert.rejects(() => kernel.find('foo'), 'Command "foo" is not defined') + }) + + test('raise error when loader is not able to lookup command', async ({ assert }) => { + const kernel = new Kernel() + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + static aliases: string[] = ['mc'] + } + + class MakeModel extends BaseCommand { + static commandName = 'make:model' + } + + class CustomLoader extends CommandsList { + async getCommand(_: CommandMetaData): Promise { + return null + } + } + + kernel.addLoader(new CustomLoader([MakeController, MakeModel])) + await kernel.boot() + + await assert.rejects(() => kernel.find('make:model'), 'Command "make:model" is not defined') + }) + + test('find command when using multiple loaders', async ({ assert }) => { + const kernel = new Kernel() + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + static aliases: string[] = ['mc'] + } + + class MakeModel extends BaseCommand { + static commandName = 'make:model' + } + + kernel.addLoader(new CommandsList([MakeController])) + kernel.addLoader(new CommandsList([MakeModel])) + await kernel.boot() + + const command = await kernel.find('make:model') + assert.strictEqual(command, MakeModel) + }) + + test('execute finding and found hooks', async ({ assert }) => { + const kernel = new Kernel() + const stack: string[] = [] + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + static aliases: string[] = ['mc'] + } + + class MakeModel extends BaseCommand { + static commandName = 'make:model' + } + + kernel.addLoader(new CommandsList([MakeController])) + kernel.addLoader(new CommandsList([MakeModel])) + await kernel.boot() + + kernel.finding((commandName) => { + assert.equal(commandName, 'make:model') + stack.push('finding') + }) + + kernel.found((command) => { + assert.equal(command.commandName, 'make:model') + stack.push('found') + }) + const command = await kernel.find('make:model') + + assert.strictEqual(command, MakeModel) + assert.deepEqual(stack, ['finding', 'found']) + }) + + test('do not execute found hook when command not found', async ({ assert }) => { + const kernel = new Kernel() + const stack: string[] = [] + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + static aliases: string[] = ['mc'] + } + + class MakeModel extends BaseCommand { + static commandName = 'make:model' + } + + kernel.addLoader(new CommandsList([MakeController])) + kernel.addLoader(new CommandsList([MakeModel])) + await kernel.boot() + + kernel.finding((commandName) => { + assert.equal(commandName, 'foo') + stack.push('finding') + }) + + kernel.found((command) => { + assert.equal(command.commandName, 'make:model') + stack.push('found') + }) + + await assert.rejects(() => kernel.find('foo'), 'Command "foo" is not defined') + assert.deepEqual(stack, ['finding']) + }) +}) diff --git a/tests/kernel/flag_listeners.spec.ts b/tests/kernel/flag_listeners.spec.ts new file mode 100644 index 0000000..0e29edd --- /dev/null +++ b/tests/kernel/flag_listeners.spec.ts @@ -0,0 +1,152 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { Kernel } from '../../src/kernel.js' +import { BaseCommand } from '../../src/commands/base.js' +import { CommandsList } from '../../src/loaders/list.js' +import { ParsedOutput, UIPrimitives } from '../../src/types.js' + +test.group('Kernel | handle', (group) => { + group.each.teardown(() => { + process.exitCode = undefined + }) + + test('execute flag listener on a global flag', async ({ assert }) => { + const kernel = new Kernel() + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + async run() { + return 'executed' + } + } + MakeController.defineArgument('name', { type: 'string' }) + + kernel.addLoader(new CommandsList([MakeController])) + kernel.defineFlag('verbose', { type: 'boolean' }) + kernel.on('verbose', (Command, _, options) => { + assert.strictEqual(Command, MakeController) + assert.isTrue(options.flags.verbose) + }) + + await kernel.handle(['make:controller', 'users', '--verbose']) + + assert.equal(kernel.getState(), 'terminated') + assert.equal(kernel.exitCode, 0) + }) + + test('execute flag listener on a command flag', async ({ assert }) => { + const kernel = new Kernel() + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + async run() { + return 'executed' + } + } + MakeController.defineArgument('name', { type: 'string' }) + MakeController.defineFlag('connection', { type: 'string' }) + + kernel.addLoader(new CommandsList([MakeController])) + kernel.defineFlag('verbose', { type: 'boolean' }) + kernel.on('connection', (Command, _, options) => { + assert.strictEqual(Command, MakeController) + assert.equal(options.flags.connection, 'sqlite') + }) + + await kernel.handle(['make:controller', 'users', '--connection', 'sqlite']) + + assert.equal(kernel.getState(), 'terminated') + assert.equal(kernel.exitCode, 0) + }) + + test('terminate from the flag listener', async ({ assert }) => { + const kernel = new Kernel() + const stack: string[] = [] + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + constructor($kernel: Kernel, parsed: ParsedOutput, ui: UIPrimitives) { + super($kernel, parsed, ui) + stack.push('constructor') + } + + async run() { + stack.push('run') + return 'executed' + } + } + + MakeController.defineArgument('name', { type: 'string' }) + kernel.addLoader(new CommandsList([MakeController])) + + kernel.defineFlag('help', { type: 'boolean' }) + kernel.on('help', () => { + return true + }) + + await kernel.handle(['make:controller', 'users', '--help']) + assert.equal(kernel.getState(), 'terminated') + assert.equal(kernel.exitCode, 0) + assert.deepEqual(stack, []) + }) + + test('execute flag listener for the default command', async ({ assert }) => { + const kernel = new Kernel() + const stack: string[] = [] + + class Help extends BaseCommand { + static commandName = 'help' + async run() { + stack.push('run help') + } + } + + kernel.registerDefaultCommand(Help) + kernel.defineFlag('help', { type: 'boolean' }) + kernel.on('help', (Command, _, options) => { + assert.strictEqual(Command, Help) + assert.isTrue(options.flags.help) + }) + + await kernel.handle(['--help']) + + assert.equal(kernel.getState(), 'terminated') + assert.equal(kernel.exitCode, 0) + assert.deepEqual(stack, ['run help']) + }) + + test('terminate from the flag listener for the default command', async ({ assert }) => { + const kernel = new Kernel() + const stack: string[] = [] + + class Help extends BaseCommand { + static commandName = 'help' + async run() { + stack.push('run help') + } + } + + kernel.registerDefaultCommand(Help) + kernel.defineFlag('help', { type: 'boolean' }) + + kernel.on('help', (Command, _, options) => { + assert.strictEqual(Command, Help) + assert.isTrue(options.flags.help) + return true + }) + + await kernel.handle(['--help']) + + assert.equal(kernel.getState(), 'terminated') + assert.equal(kernel.exitCode, 0) + assert.deepEqual(stack, []) + }) +}) diff --git a/tests/kernel/gloal_flags.spec.ts b/tests/kernel/gloal_flags.spec.ts new file mode 100644 index 0000000..07f4dcd --- /dev/null +++ b/tests/kernel/gloal_flags.spec.ts @@ -0,0 +1,48 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { Kernel } from '../../src/kernel.js' + +test.group('Kernel | global flags', () => { + test('define global flags', async ({ assert }) => { + const kernel = new Kernel() + const kernel1 = new Kernel() + + kernel.defineFlag('help', { type: 'boolean' }) + kernel1.defineFlag('version', { type: 'boolean' }) + + assert.deepEqual(kernel.flags, [ + { + flagName: 'help', + name: 'help', + type: 'boolean', + required: false, + }, + ]) + assert.deepEqual(kernel1.flags, [ + { + flagName: 'version', + name: 'version', + type: 'boolean', + required: false, + }, + ]) + }) + + test('disallow registering global flags after kernel is booted', async ({ assert }) => { + const kernel = new Kernel() + await kernel.boot() + + assert.throws( + () => kernel.defineFlag('help', { type: 'boolean' }), + 'Cannot register global flag in "booted" state' + ) + }) +}) diff --git a/tests/kernel/handle.spec.ts b/tests/kernel/handle.spec.ts new file mode 100644 index 0000000..da27fd8 --- /dev/null +++ b/tests/kernel/handle.spec.ts @@ -0,0 +1,258 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { cliui } from '@poppinss/cliui' +import { Kernel } from '../../src/kernel.js' +import { CommandOptions } from '../../src/types.js' +import { BaseCommand } from '../../src/commands/base.js' +import { CommandsList } from '../../src/loaders/list.js' + +test.group('Kernel | handle', (group) => { + group.each.teardown(() => { + process.exitCode = undefined + }) + + test('execute command as main command', async ({ assert }) => { + const kernel = new Kernel() + class MakeController extends BaseCommand { + static commandName = 'make:controller' + async run() { + assert.equal(this.kernel.getState(), 'running') + return 'executed' + } + } + + kernel.addLoader(new CommandsList([MakeController])) + await kernel.handle(['make:controller']) + + assert.equal(kernel.exitCode, 0) + assert.equal(process.exitCode, 0) + assert.equal(kernel.getState(), 'terminated') + }) + + test('report error using logger command validation fails', async ({ assert }) => { + const kernel = new Kernel() + kernel.ui = cliui({ mode: 'raw' }) + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + async run() { + return 'executed' + } + } + MakeController.defineArgument('name', { type: 'string' }) + + kernel.addLoader(new CommandsList([MakeController])) + await kernel.handle(['make:controller']) + + assert.equal(kernel.getState(), 'terminated') + assert.equal(kernel.exitCode, 1) + + assert.deepEqual(kernel.ui.logger.getRenderer().getLogs(), [ + { + message: 'bgRed(white( ERROR )) Missing required argument "name"', + stream: 'stderr', + }, + ]) + }) + + test('report error using logger when unable to find command', async ({ assert }) => { + const kernel = new Kernel() + kernel.ui = cliui({ mode: 'raw' }) + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + async run() { + return 'executed' + } + } + MakeController.defineArgument('name', { type: 'string' }) + + kernel.addLoader(new CommandsList([MakeController])) + await kernel.handle(['foo']) + + assert.equal(kernel.getState(), 'terminated') + assert.equal(kernel.exitCode, 1) + assert.deepEqual(kernel.ui.logger.getRenderer().getLogs(), [ + { + message: 'red(Command "foo" is not defined)', + stream: 'stderr', + }, + ]) + }) + + test('report error when command hooks fails', async ({ assert }) => { + const kernel = new Kernel() + const stack: string[] = [] + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + async run() { + stack.push('run') + return 'executed' + } + } + MakeController.defineArgument('name', { type: 'string' }) + + kernel.addLoader(new CommandsList([MakeController])) + kernel.executing(() => { + stack.push('executing') + }) + kernel.executed(() => { + stack.push('executed') + throw new Error('Post hook failed') + }) + kernel.terminating(() => { + stack.push('terminating') + }) + + await kernel.handle(['make:controller', 'users']) + + assert.equal(kernel.getState(), 'terminated') + assert.equal(kernel.exitCode, 1) + assert.deepEqual(stack, ['executing', 'run', 'executed', 'terminating']) + }) + + test('report error when command completed method fails', async ({ assert }) => { + const kernel = new Kernel() + const stack: string[] = [] + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + async run() { + stack.push('run') + return 'executed' + } + + async completed(): Promise { + throw new Error('Something went wrong') + } + } + MakeController.defineArgument('name', { type: 'string' }) + + kernel.addLoader(new CommandsList([MakeController])) + kernel.terminating(() => { + stack.push('terminating') + }) + + await kernel.handle(['make:controller', 'users']) + + assert.equal(kernel.getState(), 'terminated') + assert.equal(kernel.exitCode, 1) + assert.deepEqual(stack, ['run', 'terminating']) + }) + + test('disallow calling handle method twice in parallel', async ({ assert }) => { + const kernel = new Kernel() + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + async run() { + return 'executed' + } + } + MakeController.defineArgument('name', { type: 'string' }) + kernel.addLoader(new CommandsList([MakeController])) + await kernel.boot() + + await assert.rejects( + () => + Promise.all([ + kernel.handle(['make:controller', 'users']), + kernel.handle(['make:controller', 'users']), + ]), + 'Cannot run multiple main commands from a single process' + ) + }) + + test('disallow calling handle method after the process has been terminated', async ({ + assert, + }) => { + const kernel = new Kernel() + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + async run() { + return 'executed' + } + } + MakeController.defineArgument('name', { type: 'string' }) + kernel.addLoader(new CommandsList([MakeController])) + await kernel.handle(['make:controller', 'users']) + + await assert.rejects( + () => kernel.handle(['make:controller', 'users']), + 'The kernel has been terminated. Create a fresh instance to execute commands' + ) + }) + + test('disallow calling exec method after the process has been terminated', async ({ assert }) => { + const kernel = new Kernel() + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + async run() { + return 'executed' + } + } + MakeController.defineArgument('name', { type: 'string' }) + kernel.addLoader(new CommandsList([MakeController])) + await kernel.handle(['make:controller', 'users']) + + await assert.rejects( + () => kernel.exec('make:controller', ['users']), + 'The kernel has been terminated. Create a fresh instance to execute commands' + ) + }) + + test('run default command when args are provided', async ({ assert }) => { + const kernel = new Kernel() + const stack: string[] = [] + + class Help extends BaseCommand { + static commandName = 'help' + async run() { + stack.push('run help') + } + } + + kernel.registerDefaultCommand(Help) + await kernel.handle([]) + + assert.deepEqual(stack, ['run help']) + assert.equal(kernel.exitCode, 0) + assert.equal(process.exitCode, 0) + assert.equal(kernel.getState(), 'terminated') + }) + + test('run default command when only flags are provided', async ({ assert }) => { + const kernel = new Kernel() + const stack: string[] = [] + + class Help extends BaseCommand { + static commandName = 'help' + static options: CommandOptions = { + allowUnknownFlags: true, + } + + async run() { + stack.push('run help') + } + } + + kernel.registerDefaultCommand(Help) + await kernel.handle(['--help']) + + assert.deepEqual(stack, ['run help']) + assert.equal(kernel.exitCode, 0) + assert.equal(process.exitCode, 0) + assert.equal(kernel.getState(), 'terminated') + }) +}) diff --git a/tests/kernel/loaders.spec.ts b/tests/kernel/loaders.spec.ts new file mode 100644 index 0000000..de4e6c4 --- /dev/null +++ b/tests/kernel/loaders.spec.ts @@ -0,0 +1,84 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' + +import { Kernel } from '../../src/kernel.js' +import { BaseCommand } from '../../src/commands/base.js' +import { CommandsList } from '../../src/loaders/list.js' + +test.group('Kernel | loaders', () => { + test('register commands using a loader', async ({ assert }) => { + const kernel = new Kernel() + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + } + + class MakeModel extends BaseCommand { + static commandName = 'make:model' + } + + kernel.addLoader(new CommandsList([MakeController, MakeModel])) + await kernel.boot() + + assert.deepEqual(kernel.getCommands(), [ + kernel.getDefaultCommand().serialize(), + MakeController.serialize(), + MakeModel.serialize(), + ]) + }) + + test('register commands using multiple loaders', async ({ assert }) => { + const kernel = new Kernel() + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + } + + class MakeModel extends BaseCommand { + static commandName = 'make:model' + } + + kernel.addLoader(new CommandsList([MakeController])) + kernel.addLoader(new CommandsList([MakeModel])) + await kernel.boot() + + assert.deepEqual(kernel.getCommands(), [ + kernel.getDefaultCommand().serialize(), + MakeController.serialize(), + MakeModel.serialize(), + ]) + }) + + test('disallow adding a loader after kernel is booted', async ({ assert }) => { + const kernel = new Kernel() + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + } + + class MakeModel extends BaseCommand { + static commandName = 'make:model' + } + + kernel.addLoader(new CommandsList([MakeController])) + await kernel.boot() + + assert.deepEqual(kernel.getCommands(), [ + kernel.getDefaultCommand().serialize(), + MakeController.serialize(), + ]) + + assert.throws( + () => kernel.addLoader(new CommandsList([MakeModel])), + 'Cannot add loader in "booted" state' + ) + }) +}) diff --git a/tests/kernel/main.spec.ts b/tests/kernel/main.spec.ts new file mode 100644 index 0000000..df17291 --- /dev/null +++ b/tests/kernel/main.spec.ts @@ -0,0 +1,305 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' + +import { Kernel } from '../../src/kernel.js' +import { BaseCommand } from '../../src/commands/base.js' +import { CommandsList } from '../../src/loaders/list.js' +import { ListCommand } from '../../src/commands/list.js' + +test.group('Kernel', () => { + test('get alphabetically sorted list of commands', async ({ assert }) => { + const kernel = new Kernel() + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + } + + class MakeModel extends BaseCommand { + static commandName = 'make:model' + } + + kernel.addLoader(new CommandsList([MakeModel, MakeController])) + await kernel.boot() + + const commands = kernel.getCommands() + assert.deepEqual( + commands.map(({ commandName }) => commandName), + ['list', 'make:controller', 'make:model'] + ) + }) + + test('get alphabetically sorted list of namespaces', async ({ assert }) => { + const kernel = new Kernel() + + class ListRoutes extends BaseCommand { + static commandName = 'list:routes' + } + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + } + + class MakeModel extends BaseCommand { + static commandName = 'make:model' + } + + kernel.addLoader(new CommandsList([MakeModel, MakeController, ListRoutes])) + await kernel.boot() + + assert.deepEqual(kernel.getNamespaces(), ['list', 'make']) + }) + + test('get list of commands for a given namespace', async ({ assert }) => { + const kernel = new Kernel() + + class ListRoutes extends BaseCommand { + static commandName = 'list:routes' + } + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + } + + class MakeModel extends BaseCommand { + static commandName = 'make:model' + } + + kernel.addLoader(new CommandsList([MakeModel, MakeController, ListRoutes])) + await kernel.boot() + + assert.deepEqual( + kernel.getNamespaceCommands('make').map(({ commandName }) => commandName), + ['make:controller', 'make:model'] + ) + + assert.deepEqual( + kernel.getNamespaceCommands('list').map(({ commandName }) => commandName), + ['list:routes'] + ) + }) + + test('get list of top level commands without a namespace', async ({ assert }) => { + const kernel = new Kernel() + + class Migrate extends BaseCommand { + static commandName = 'migrate' + } + + class ListRoutes extends BaseCommand { + static commandName = 'list:routes' + } + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + } + + class MakeModel extends BaseCommand { + static commandName = 'make:model' + } + + kernel.addLoader(new CommandsList([MakeModel, MakeController, ListRoutes, Migrate])) + await kernel.boot() + + assert.deepEqual( + kernel.getNamespaceCommands().map(({ commandName }) => commandName), + ['list', 'migrate'] + ) + }) + + test('get an array of registered aliases', async ({ assert }) => { + const kernel = new Kernel() + + class Migrate extends BaseCommand { + static commandName = 'migrate' + static aliases: string[] = ['migration:run'] + } + + class ListRoutes extends BaseCommand { + static commandName = 'list:routes' + } + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + } + + class MakeModel extends BaseCommand { + static commandName = 'make:model' + } + + kernel.addLoader(new CommandsList([MakeModel, MakeController, ListRoutes, Migrate])) + kernel.addAlias('mc', 'make:controller') + kernel.addAlias('mm', 'make:model') + await kernel.boot() + + assert.deepEqual(kernel.getAliases(), ['mc', 'mm', 'migration:run']) + }) + + test('get aliases for a given command', async ({ assert }) => { + const kernel = new Kernel() + + class Migrate extends BaseCommand { + static commandName = 'migrate' + static aliases: string[] = ['migration:run'] + } + + class ListRoutes extends BaseCommand { + static commandName = 'list:routes' + } + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + } + + class MakeModel extends BaseCommand { + static commandName = 'make:model' + } + + kernel.addLoader(new CommandsList([MakeModel, MakeController, ListRoutes, Migrate])) + kernel.addAlias('mc', 'make:controller') + kernel.addAlias('mm', 'make:model') + await kernel.boot() + + assert.deepEqual(kernel.getCommandAliases('migrate'), ['migration:run']) + assert.deepEqual(kernel.getCommandAliases('make:controller'), ['mc']) + }) + + test('get command for an alias', async ({ assert }) => { + const kernel = new Kernel() + + class Migrate extends BaseCommand { + static commandName = 'migrate' + static aliases: string[] = ['migration:run'] + } + + class ListRoutes extends BaseCommand { + static commandName = 'list:routes' + } + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + } + + class MakeModel extends BaseCommand { + static commandName = 'make:model' + } + + kernel.addLoader(new CommandsList([MakeModel, MakeController, ListRoutes, Migrate])) + kernel.addAlias('mc', 'make:controller') + kernel.addAlias('mm', 'unrecognized:command') + await kernel.boot() + + assert.deepEqual(kernel.getAliasCommand('mc')?.commandName, 'make:controller') + assert.isNull(kernel.getAliasCommand('mm')) + assert.isNull(kernel.getAliasCommand('foo')) + }) + + test('get command metadata', async ({ assert }) => { + const kernel = new Kernel() + + class Migrate extends BaseCommand { + static commandName = 'migrate' + } + + kernel.addLoader(new CommandsList([Migrate])) + await kernel.boot() + + assert.deepEqual(kernel.getCommand('migrate'), Migrate.serialize()) + assert.isNull(kernel.getCommand('migration:run')) + }) + + test('get the default command', async ({ assert }) => { + const kernel = new Kernel() + await kernel.boot() + + assert.strictEqual(kernel.getDefaultCommand(), ListCommand) + }) + + test('get commands suggestions for a given keyword', async ({ assert }) => { + const kernel = new Kernel() + + class Migrate extends BaseCommand { + static commandName = 'migration:run' + } + + class ListRoutes extends BaseCommand { + static commandName = 'list:routes' + } + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + } + + class MakeModel extends BaseCommand { + static commandName = 'make:model' + } + + kernel.addLoader(new CommandsList([MakeModel, MakeController, ListRoutes, Migrate])) + kernel.addAlias('mc', 'make:controller') + kernel.addAlias('mm', 'unrecognized:command') + await kernel.boot() + + assert.deepEqual(kernel.getCommandSuggestions('controller'), ['make:controller']) + assert.deepEqual(kernel.getCommandSuggestions('migrate'), ['migration:run']) + }) + + test('get commands suggestions for a namespace', async ({ assert }) => { + const kernel = new Kernel() + + class Migrate extends BaseCommand { + static commandName = 'migration:run' + } + + class ListRoutes extends BaseCommand { + static commandName = 'list:routes' + } + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + } + + class MakeModel extends BaseCommand { + static commandName = 'make:model' + } + + kernel.addLoader(new CommandsList([MakeModel, MakeController, ListRoutes, Migrate])) + kernel.addAlias('mc', 'make:controller') + kernel.addAlias('mm', 'unrecognized:command') + await kernel.boot() + + assert.deepEqual(kernel.getCommandSuggestions('make'), ['make:controller', 'make:model']) + }) + + test('get namespaces suggestions for a given keyword', async ({ assert }) => { + const kernel = new Kernel() + + class Migrate extends BaseCommand { + static commandName = 'migration:run' + } + + class ListRoutes extends BaseCommand { + static commandName = 'list:routes' + } + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + } + + class MakeModel extends BaseCommand { + static commandName = 'make:model' + } + + kernel.addLoader(new CommandsList([MakeModel, MakeController, ListRoutes, Migrate])) + kernel.addAlias('mc', 'make:controller') + await kernel.boot() + + assert.deepEqual(kernel.getNamespaceSuggestions('migrate'), ['migration']) + }) +}) diff --git a/tests/kernel/terminate.spec.ts b/tests/kernel/terminate.spec.ts new file mode 100644 index 0000000..e19fb90 --- /dev/null +++ b/tests/kernel/terminate.spec.ts @@ -0,0 +1,86 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' + +import { Kernel } from '../../src/kernel.js' +import { BaseCommand } from '../../src/commands/base.js' +import { CommandsList } from '../../src/loaders/list.js' + +test.group('Kernel | terminate', (group) => { + group.each.teardown(() => { + process.exitCode = undefined + }) + + test('do not terminate when not in running state', async ({ assert }) => { + const kernel = new Kernel() + await kernel.boot() + await kernel.terminate() + + assert.isUndefined(kernel.exitCode) + assert.isUndefined(process.exitCode) + assert.equal(kernel.getState(), 'booted') + }) + + test('do not terminate from a non-main command', async ({ assert }) => { + const kernel = new Kernel() + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + async run() {} + } + + class MakeModel extends BaseCommand { + static commandName = 'make:model' + async run() {} + } + + kernel.addLoader(new CommandsList([MakeController, MakeModel])) + kernel.executed(async () => { + await kernel.terminate(new MakeModel(kernel, { _: [] }, kernel.ui)) + }) + kernel.executed(async () => { + assert.equal(kernel.getState(), 'running') + }) + + await kernel.handle(['make:controller']) + + assert.equal(kernel.exitCode, 0) + assert.equal(process.exitCode, 0) + assert.equal(kernel.getState(), 'terminated') + }) + + test('terminate from a main command', async ({ assert }) => { + const kernel = new Kernel() + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + async run() {} + } + + class MakeModel extends BaseCommand { + static commandName = 'make:model' + async run() {} + } + + kernel.addLoader(new CommandsList([MakeController, MakeModel])) + kernel.executed(async (command) => { + await kernel.terminate(command) + }) + kernel.executed(async () => { + assert.equal(kernel.getState(), 'terminated') + }) + + await kernel.handle(['make:controller']) + + assert.equal(kernel.exitCode, 0) + assert.equal(process.exitCode, 0) + assert.equal(kernel.getState(), 'terminated') + }) +}) diff --git a/tests/parser.spec.ts b/tests/parser.spec.ts new file mode 100644 index 0000000..36796bb --- /dev/null +++ b/tests/parser.spec.ts @@ -0,0 +1,448 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { Parser } from '../src/parser.js' +import { BaseCommand } from '../src/commands/base.js' + +test.group('Parser | flags', () => { + test('parse flags for all datatypes', ({ assert }) => { + class MakeModel extends BaseCommand {} + MakeModel.defineFlag('connection', { type: 'string' }) + MakeModel.defineFlag('dropAll', { type: 'boolean' }) + MakeModel.defineFlag('batchSize', { type: 'number' }) + MakeModel.defineFlag('files', { type: 'array' }) + + const output = new Parser(MakeModel.getParserOptions()).parse( + '--connection=sqlite --drop-all --batch-size=1 --files=a,b' + ) + + assert.deepEqual(output, { + _: [], + args: [], + unknownFlags: [], + flags: { + 'batch-size': 1, + 'connection': 'sqlite', + 'drop-all': true, + 'files': ['a,b'], + }, + }) + + assert.deepEqual(new Parser(MakeModel.getParserOptions()).parse('--files=a --files=b'), { + _: [], + args: [], + unknownFlags: [], + flags: { + files: ['a', 'b'], + }, + }) + }) + + test('parse flags using aliases', ({ assert }) => { + class MakeModel extends BaseCommand {} + MakeModel.defineFlag('connection', { type: 'string', alias: 'c' }) + MakeModel.defineFlag('dropAll', { type: 'boolean', alias: 'd' }) + MakeModel.defineFlag('batchSize', { type: 'number', alias: 'b' }) + MakeModel.defineFlag('files', { type: 'array', alias: 'f' }) + + const output = new Parser(MakeModel.getParserOptions()).parse('-c=sqlite -d -b=1 -f=a,b') + + assert.deepEqual(output, { + _: [], + args: [], + unknownFlags: [], + flags: { + 'batch-size': 1, + 'connection': 'sqlite', + 'drop-all': true, + 'files': ['a,b'], + }, + }) + + assert.deepEqual(new Parser(MakeModel.getParserOptions()).parse('-f=a -f=b'), { + _: [], + args: [], + unknownFlags: [], + flags: { + files: ['a', 'b'], + }, + }) + }) + + test('do not set flag when not mentioned', ({ assert }) => { + class MakeModel extends BaseCommand {} + MakeModel.defineFlag('dropAll', { type: 'boolean' }) + MakeModel.defineFlag('connection', { type: 'string' }) + MakeModel.defineFlag('batchSize', { type: 'number' }) + MakeModel.defineFlag('files', { type: 'array' }) + + const output = new Parser(MakeModel.getParserOptions()).parse('') + assert.deepEqual(output, { + _: [], + args: [], + unknownFlags: [], + flags: {}, + }) + }) + + test('set flags to default values when not set', ({ assert }) => { + class MakeModel extends BaseCommand {} + MakeModel.defineFlag('dropAll', { type: 'boolean', default: false }) + MakeModel.defineFlag('connection', { type: 'string', default: 'sqlite' }) + MakeModel.defineFlag('batchSize', { type: 'number', default: 1 }) + MakeModel.defineFlag('files', { type: 'array' }) + + const output = new Parser(MakeModel.getParserOptions()).parse('') + assert.deepEqual(output, { + _: [], + args: [], + unknownFlags: [], + flags: { + 'batch-size': 1, + 'drop-all': false, + 'connection': 'sqlite', + }, + }) + }) + + test('set flags to default values when mentioned with no value', ({ assert }) => { + class MakeModel extends BaseCommand {} + MakeModel.defineFlag('dropAll', { type: 'boolean', default: false }) + MakeModel.defineFlag('connection', { type: 'string', default: 'sqlite' }) + MakeModel.defineFlag('batchSize', { type: 'number', default: 1 }) + MakeModel.defineFlag('files', { type: 'array', default: ['a.txt'] }) + + const output = new Parser(MakeModel.getParserOptions()).parse( + '--connection --batch-size --files' + ) + + assert.deepEqual(output, { + _: [], + args: [], + unknownFlags: [], + flags: { + 'batch-size': 1, + 'drop-all': false, + 'connection': 'sqlite', + 'files': ['a.txt'], + }, + }) + }) + + test('return parsed values', ({ assert }) => { + class MakeModel extends BaseCommand {} + MakeModel.defineFlag('dropAll', { + type: 'boolean', + default: false, + parse(value) { + return value ? 1 : 0 + }, + }) + MakeModel.defineFlag('connection', { + type: 'string', + default: 'DEFAULT_CONN', + parse(value) { + return value === 'DEFAULT_CONN' ? 'sqlite' : value + }, + }) + MakeModel.defineFlag('batchSize', { + type: 'number', + default: 1, + parse(value) { + return Number.isNaN(value) ? 1 : value + }, + }) + MakeModel.defineFlag('files', { + type: 'array', + default: [], + parse(value) { + return value + }, + }) + + const output = new Parser(MakeModel.getParserOptions()).parse( + '--batch-size=1 --connection=mysql --drop-all --files' + ) + + assert.deepEqual(output, { + _: [], + args: [], + unknownFlags: [], + flags: { + 'batch-size': 1, + 'drop-all': 1, + 'connection': 'mysql', + 'files': [], + }, + }) + }) + + test('call parse method for default values', ({ assert }) => { + class MakeModel extends BaseCommand {} + MakeModel.defineFlag('dropAll', { + type: 'boolean', + default: false, + parse(value) { + return value ? 1 : 0 + }, + }) + MakeModel.defineFlag('connection', { + type: 'string', + default: 'DEFAULT_CONN', + parse(value) { + return value === 'DEFAULT_CONN' ? 'sqlite' : value + }, + }) + MakeModel.defineFlag('batchSize', { + type: 'number', + default: 1, + parse(value) { + return Number.isNaN(value) ? 1 : value + }, + }) + MakeModel.defineFlag('files', { + type: 'array', + default: [], + parse(value) { + return value + }, + }) + + const output = new Parser(MakeModel.getParserOptions()).parse('--batch-size') + assert.deepEqual(output, { + _: [], + args: [], + unknownFlags: [], + flags: { + 'batch-size': 1, + 'drop-all': 0, + 'connection': 'sqlite', + 'files': [], + }, + }) + }) + + test('do not call parse method when flags are not set', ({ assert }) => { + class MakeModel extends BaseCommand {} + MakeModel.defineFlag('dropAll', { + type: 'boolean', + parse(value) { + return value ? 1 : 0 + }, + }) + MakeModel.defineFlag('files', { + type: 'array', + parse(value) { + return value + }, + }) + + const output = new Parser(MakeModel.getParserOptions()).parse('') + assert.deepEqual(output, { + _: [], + args: [], + unknownFlags: [], + flags: {}, + }) + }) + + test('detect unknown flags', ({ assert }) => { + class MakeModel extends BaseCommand {} + MakeModel.defineFlag('connection', { type: 'string' }) + + const output = new Parser(MakeModel.getParserOptions()).parse( + '--connection=sqlite --foo --bar=baz' + ) + + assert.deepEqual(output, { + _: [], + args: [], + unknownFlags: ['foo', 'bar'], + flags: { + foo: true, + bar: 'baz', + connection: 'sqlite', + }, + }) + }) +}) + +test.group('Parser | arguments', () => { + test('parse arguments for all datatypes', ({ assert }) => { + class MakeModel extends BaseCommand {} + MakeModel.defineArgument('name', { type: 'string' }) + MakeModel.defineArgument('connections', { type: 'spread' }) + + const output = new Parser(MakeModel.getParserOptions()).parse('user sqlite mysql pg') + assert.deepEqual(output, { + _: [], + args: ['user', ['sqlite', 'mysql', 'pg']], + unknownFlags: [], + flags: {}, + }) + }) + + test('use default value when argument is not defined', ({ assert }) => { + class MakeModel extends BaseCommand {} + MakeModel.defineArgument('name', { type: 'string' }) + MakeModel.defineArgument('connections', { type: 'spread', default: ['sqlite'] }) + + const output = new Parser(MakeModel.getParserOptions()).parse('user') + assert.deepEqual(output, { + _: [], + args: ['user', ['sqlite']], + unknownFlags: [], + flags: {}, + }) + }) + + test('do not use default value when argument is defined as empty string', ({ assert }) => { + class MakeModel extends BaseCommand {} + MakeModel.defineArgument('name', { type: 'string' }) + MakeModel.defineArgument('connections', { type: 'spread', default: ['sqlite'] }) + + const output = new Parser(MakeModel.getParserOptions()).parse(['user', '']) + assert.deepEqual(output, { + _: [], + args: ['user', ['']], + unknownFlags: [], + flags: {}, + }) + }) + + test('call parse method', ({ assert }) => { + class MakeModel extends BaseCommand {} + MakeModel.defineArgument('name', { + type: 'string', + parse(value) { + return value.toUpperCase() + }, + }) + MakeModel.defineArgument('connections', { + type: 'spread', + parse(values) { + return values.map((value: string) => value.toUpperCase()) + }, + }) + + const output = new Parser(MakeModel.getParserOptions()).parse(['user', 'sqlite', 'pg']) + assert.deepEqual(output, { + _: [], + args: ['USER', ['SQLITE', 'PG']], + unknownFlags: [], + flags: {}, + }) + }) + + test('call parse method on default value', ({ assert }) => { + class MakeModel extends BaseCommand {} + MakeModel.defineArgument('name', { + type: 'string', + default: 'post', + parse(value) { + return value.toUpperCase() + }, + }) + MakeModel.defineArgument('connections', { + type: 'spread', + default: ['sqlite'], + parse(values) { + return values.map((value: string) => value.toUpperCase()) + }, + }) + + const output = new Parser(MakeModel.getParserOptions()).parse([]) + assert.deepEqual(output, { + _: [], + args: ['POST', ['SQLITE']], + unknownFlags: [], + flags: {}, + }) + }) + + test('do not call parse when value is undefined', ({ assert }) => { + class MakeModel extends BaseCommand {} + MakeModel.defineArgument('name', { + type: 'string', + parse(value) { + return value.toUpperCase() + }, + }) + MakeModel.defineArgument('connections', { + type: 'spread', + parse(values) { + return values.map((value: string) => value.toUpperCase()) + }, + }) + + const output = new Parser(MakeModel.getParserOptions()).parse([]) + assert.deepEqual(output, { + _: [], + args: [undefined, undefined], + unknownFlags: [], + flags: {}, + }) + }) + + test('cast spread default value to array', ({ assert }) => { + class MakeModel extends BaseCommand {} + MakeModel.defineArgument('name', { + type: 'string', + parse(value) { + return value.toUpperCase() + }, + }) + MakeModel.defineArgument('connections', { + type: 'spread', + default: 1, + parse(values) { + return values.map((value: string | number) => { + return typeof value === 'string' ? value.toUpperCase() : value + }) + }, + }) + + const output = new Parser(MakeModel.getParserOptions()).parse([]) + assert.deepEqual(output, { + _: [], + args: [undefined, [1]], + unknownFlags: [], + flags: {}, + }) + }) + + test('define different data type for string default value', ({ assert }) => { + class MakeModel extends BaseCommand {} + MakeModel.defineArgument('name', { + type: 'string', + default: null, + parse(value) { + return value ? value.toUpperCase() : value + }, + }) + MakeModel.defineArgument('connections', { + type: 'spread', + default: 1, + parse(values) { + return values.map((value: string | number) => { + return typeof value === 'string' ? value.toUpperCase() : value + }) + }, + }) + + const output = new Parser(MakeModel.getParserOptions()).parse([]) + assert.deepEqual(output, { + _: [], + args: [null, [1]], + unknownFlags: [], + flags: {}, + }) + }) +}) diff --git a/tsconfig.json b/tsconfig.json index 0ec205d..a09cb7d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,14 +1,33 @@ { - "extends": "./node_modules/@adonisjs/mrm-preset/_tsconfig", "compilerOptions": { + "target": "ESNext", + "module": "NodeNext", + "lib": ["ESNext"], + "useDefineForClassFields": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "isolatedModules": true, + "removeComments": true, + "declaration": true, + "rootDir": "./", "outDir": "./build", + "esModuleInterop": true, + "strictNullChecks": true, + "experimentalDecorators": true, "emitDecoratorMetadata": true, - "experimentalDecorators": true + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "strictPropertyInitialization": true, + "noImplicitAny": true, + "strictBindCallApply": true, + "strictFunctionTypes": true, + "noImplicitThis": true, + "skipLibCheck": true, + "types": ["@types/node"] }, - "include": [ - "**/*" - ], - "files": [ - "./node_modules/@adonisjs/application/build/adonis-typings/index.d.ts" - ] + "include": ["./**/*"], + "exclude": ["./node_modules", "./build", "./backup"], + "ts-node": { + "swc": true + } } From e0a0bdbe1c4ec0ad128e50f18a82503cd0460fab Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 24 Jan 2023 15:50:24 +0530 Subject: [PATCH 002/112] refactor: getting ready for next release --- .github/stale.yml | 4 +- LICENSE.md | 2 +- examples/main.ts | 10 ++- examples/parent.ts | 2 +- flow-chart.png | Bin 167130 -> 0 bytes index.ts | 18 ++++++ package.json | 26 ++++---- src/commands/base.ts | 44 +++++++++---- src/debug.ts | 12 ++++ src/errors.ts | 32 ---------- src/formatters/info.ts | 2 +- src/kernel.ts | 60 ++++++++++++------ src/loaders/list.ts | 4 +- src/types.ts | 18 +++--- tests/base_command/serialize.spec.ts | 4 -- tests/kernel/find.spec.ts | 21 ++++--- tests/kernel/terminate.spec.ts | 91 ++++++++++++++++++++++++--- tests/loaders/list.spec.ts | 57 +++++++++++++++++ tests/parser.spec.ts | 59 +++++++---------- 19 files changed, 315 insertions(+), 151 deletions(-) delete mode 100644 flow-chart.png create mode 100644 index.ts create mode 100644 src/debug.ts create mode 100644 tests/loaders/list.spec.ts diff --git a/.github/stale.yml b/.github/stale.yml index 7a6a571..f767674 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -6,10 +6,10 @@ daysUntilClose: 7 # Issues with these labels will never be considered stale exemptLabels: - - "Type: Security" + - 'Type: Security' # Label to use when marking an issue as stale -staleLabel: "Status: Abandoned" +staleLabel: 'Status: Abandoned' # Comment to post when marking an issue as stale. Set to `false` to disable markComment: > diff --git a/LICENSE.md b/LICENSE.md index 1c19428..59a3cfa 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ # The MIT License -Copyright 2022 Harminder Virk, contributors +Copyright (c) 2023 AdonisJS Framework 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: diff --git a/examples/main.ts b/examples/main.ts index 56cbb14..a274107 100644 --- a/examples/main.ts +++ b/examples/main.ts @@ -109,15 +109,21 @@ kernel.defineFlag('ansi', { }) kernel.on('ansi', (_, $kernel, options) => { - if (options.ansi === false) { + if (options.flags.ansi === false) { $kernel.ui.switchMode('silent') } - if (options.ansi === true) { + if (options.flags.ansi === true) { $kernel.ui.switchMode('normal') } }) +kernel.on('help', async (command, $kernel, options) => { + options.args.unshift(command.commandName) + await new HelpCommand($kernel, options, kernel.ui).exec() + return true +}) + kernel.info.set('binary', 'node ace') kernel.info.set('Framework version', '9.1') kernel.info.set('App version', '1.1.1') diff --git a/examples/parent.ts b/examples/parent.ts index 1522b29..e9263e4 100644 --- a/examples/parent.ts +++ b/examples/parent.ts @@ -1,5 +1,5 @@ import { exec } from 'node:child_process' -exec('node --loader=ts-node/esm examples/main.ts --ansi', {}, (_, stdout, stderr) => { +exec('node --loader=ts-node/esm examples/main.ts', {}, (_, stdout, stderr) => { console.log(stderr) console.log(stdout) }) diff --git a/flow-chart.png b/flow-chart.png deleted file mode 100644 index 31e4e511963ad091bd61f9e51df423ca77d1c309..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 167130 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 - 'Cannot define required argument "%s" after optional argument "%s"', - 'E_CANNOT_DEFINE_REQUIRED_ARG' -) - -/** - * Cannot define another argument after a spread argument - */ -export const E_CANNOT_DEFINE_ARG = createError<[arg: string, spreadArg: string]>( - 'Cannot define argument "%s" after spread argument "%s". Spread argument should be the last one', - 'E_CANNOT_DEFINE_ARG' -) - -/** - * Cannot define a flag because it is missing the flag type - */ -export const E_MISSING_FLAG_TYPE = createError<[flag: string]>( - 'Cannot define flag "%s". Specify the flag type', - 'E_MISSING_FLAG_TYPE' -) - /** * Command is missing the static property command name */ @@ -41,14 +17,6 @@ export const E_MISSING_COMMAND_NAME = createError<[command: string]>( 'E_MISSING_COMMAND_NAME' ) -/** - * Cannot define an argument because it is missing the arg type - */ -export const E_MISSING_ARG_TYPE = createError<[arg: string]>( - 'Cannot define argument "%s". Specify the argument type', - 'E_MISSING_ARG_TYPE' -) - /** * Cannot find a command for the given name */ diff --git a/src/formatters/info.ts b/src/formatters/info.ts index d7a739f..0d984a3 100644 --- a/src/formatters/info.ts +++ b/src/formatters/info.ts @@ -8,7 +8,7 @@ */ import stringWidth from 'string-width' -import { justify, TERMINAL_SIZE } from '../cli_helpers.js' +import { justify, TERMINAL_SIZE } from '@poppinss/cliui/helpers' import type { AllowedInfoValues, UIPrimitives } from '../types.js' /** diff --git a/src/kernel.ts b/src/kernel.ts index b4e28ab..422c3ef 100644 --- a/src/kernel.ts +++ b/src/kernel.ts @@ -23,21 +23,24 @@ import type { Flag, UIPrimitives, FlagListener, - FoundHookArgs, + LoadedHookArgs, CommandMetaData, LoadersContract, + LoadingHookArgs, FindingHookArgs, ExecutedHookArgs, ExecutorContract, - FoundHookHandler, + LoadedHookHandler, AllowedInfoValues, ExecutingHookArgs, + LoadingHookHandler, FindingHookHandler, TerminatingHookArgs, ExecutedHookHandler, ExecutingHookHandler, TerminatingHookHandler, } from './types.js' +import debug from './debug.js' const knowErrorCodes = Object.keys(errors) @@ -49,7 +52,7 @@ const knowErrorCodes = Object.keys(errors) */ export class Kernel { /** - * Listeners for CLI options. Executed for main command + * Listeners for CLI options. Executed for the main command * only */ #optionListeners: Map = new Map() @@ -75,7 +78,8 @@ export class Kernel { */ #hooks: Hooks<{ finding: FindingHookArgs - found: FoundHookArgs + loading: LoadingHookArgs + loaded: LoadedHookArgs executing: ExecutingHookArgs executed: ExecutedHookArgs terminating: TerminatingHookArgs @@ -95,13 +99,13 @@ export class Kernel { } /** - * Keep a track of the main command. There are some action (like termination) + * Keeping track of the main command. There are some action (like termination) * that only the main command can perform */ #mainCommand?: BaseCommand /** - * The current state of kernel. The `running` and `completed` + * The current state of kernel. The `running` and `terminated` * states are only set when kernel takes over the process. */ #state: 'idle' | 'booted' | 'running' | 'terminated' = 'idle' @@ -222,11 +226,6 @@ export class Kernel { */ this.#globalCommand.validate(parsed) - /** - * Validate the parsed output - */ - Command.validate(parsed) - /** * Run options listeners. Option listeners can terminate * the process early @@ -234,6 +233,7 @@ export class Kernel { let shortcircuit = false for (let [option, listener] of this.#optionListeners) { if (parsed.flags[option] !== undefined) { + debug('running listener for "%s" flag', option) shortcircuit = await listener(Command, this, parsed) if (shortcircuit) { break @@ -241,10 +241,16 @@ export class Kernel { } } + /** + * Validate the parsed output + */ + Command.validate(parsed) + /** * Terminate if a flag listener ends the process */ if (shortcircuit) { + debug('short circuiting from flag listener') await this.terminate() return } @@ -255,7 +261,7 @@ export class Kernel { this.#mainCommand = await this.#executor.create(Command, parsed, this) /** - * Execute the command either using the executor + * Execute the command using the executor */ await this.#hooks.runner('executing').run(this.#mainCommand, true) await this.#executor.run(this.#mainCommand, this) @@ -276,12 +282,12 @@ export class Kernel { * Handles the error raised during the main command execution. * * @note: Do not use this error handler for anything other than - * the handle method + * handling errors of the main command */ async #handleError(error: any) { /** * Exit code will always be 1 if a hard exception was raised - * during the command executed. + * during command execution. */ this.exitCode = 1 @@ -314,6 +320,7 @@ export class Kernel { * The callbacks are only executed for the main command */ on(option: string, callback: FlagListener): this { + debug('registering flag listener for "%s" flag', option) this.#optionListeners.set(option, callback) return this } @@ -519,10 +526,18 @@ export class Kernel { } /** - * Listen for the event when a command is found + * Listen for the event when importing the command + */ + loading(callback: LoadingHookHandler) { + this.#hooks.add('loading', callback) + return this + } + + /** + * Listen for the event when the command has been imported */ - found(callback: FoundHookHandler) { - this.#hooks.add('found', callback) + loaded(callback: LoadedHookHandler) { + this.#hooks.add('loaded', callback) return this } @@ -576,7 +591,7 @@ export class Kernel { /** * A set of unique namespaces. Later, we will store them on kernel - * directly as an array sorted alphabetically. + * directly as an alphabetically sorted array. */ const namespaces: Set = new Set() @@ -614,6 +629,8 @@ export class Kernel { throw new errors.E_COMMAND_NOT_FOUND([commandName]) } + await this.#hooks.runner('loading').run(command.metaData) + /** * Find if the loader is able to load the command */ @@ -622,7 +639,7 @@ export class Kernel { throw new errors.E_COMMAND_NOT_FOUND([commandName]) } - await this.#hooks.runner('found').run(commandConstructor) + await this.#hooks.runner('loaded').run(commandConstructor) return commandConstructor as T } @@ -701,6 +718,7 @@ export class Kernel { * or if only flags are mentioned */ if (!argv.length || argv[0].startsWith('-')) { + debug('running default command "%s"', this.#defaultCommand.commandName) return this.#execMain(this.#defaultCommand.commandName, argv) } @@ -708,6 +726,7 @@ export class Kernel { * Run the mentioned command as the main command */ const [commandName, ...args] = argv + debug('running main command "%s"', commandName) return this.#execMain(commandName, args) } @@ -724,6 +743,7 @@ export class Kernel { * is always running when we execute the handle method */ if (this.#state !== 'running') { + debug('denied terminating, since kernel.handle was never called') return } @@ -733,12 +753,14 @@ export class Kernel { * do not terminate */ if (this.#mainCommand && command !== this.#mainCommand) { + debug('denied terminating, since command other than main command attempted to terminate') return } /** * Started the termination process */ + debug('terminating') await this.#hooks.runner('terminating').run(this.#mainCommand) this.#state = 'terminated' diff --git a/src/loaders/list.ts b/src/loaders/list.ts index 809a7e0..ad7ede7 100644 --- a/src/loaders/list.ts +++ b/src/loaders/list.ts @@ -15,9 +15,9 @@ import type { CommandMetaData, LoadersContract } from '../types.js' * The commands are kept within memory */ export class CommandsList implements LoadersContract { - #commands: typeof BaseCommand[] + #commands: (typeof BaseCommand)[] - constructor(commands: typeof BaseCommand[]) { + constructor(commands: (typeof BaseCommand)[]) { this.#commands = commands } diff --git a/src/types.ts b/src/types.ts index ed6fbe0..4f362f5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -309,14 +309,6 @@ export type CommandOptions = { * Defaults to false */ staysAlive?: boolean - - /** - * When set to true, the kernel will not listen for process signals - * like SIGTERM or SIGINT - * - * Defaults to false - */ - handlesSignals?: boolean } /** @@ -328,8 +320,14 @@ export type FindingHookHandler = HookHandler +export type LoadingHookArgs = [[CommandMetaData], [CommandMetaData]] +export type LoadingHookHandler = HookHandler + +/** + * Found hook handler and data + */ +export type LoadedHookArgs = [[typeof BaseCommand], [typeof BaseCommand]] +export type LoadedHookHandler = HookHandler /** * Executing hook handler and data diff --git a/tests/base_command/serialize.spec.ts b/tests/base_command/serialize.spec.ts index 117442b..517ef93 100644 --- a/tests/base_command/serialize.spec.ts +++ b/tests/base_command/serialize.spec.ts @@ -27,7 +27,6 @@ test.group('Base command | serialize', () => { aliases: [], options: { allowUnknownFlags: false, - handlesSignals: false, staysAlive: false, }, }) @@ -49,7 +48,6 @@ test.group('Base command | serialize', () => { aliases: [], options: { allowUnknownFlags: false, - handlesSignals: false, staysAlive: false, }, }) @@ -81,7 +79,6 @@ test.group('Base command | serialize', () => { aliases: [], options: { allowUnknownFlags: false, - handlesSignals: false, staysAlive: false, }, }) @@ -120,7 +117,6 @@ test.group('Base command | serialize', () => { ], options: { allowUnknownFlags: false, - handlesSignals: false, staysAlive: false, }, aliases: [], diff --git a/tests/kernel/find.spec.ts b/tests/kernel/find.spec.ts index 47f6fbd..c4558a7 100644 --- a/tests/kernel/find.spec.ts +++ b/tests/kernel/find.spec.ts @@ -115,7 +115,7 @@ test.group('Kernel | find', () => { assert.strictEqual(command, MakeModel) }) - test('execute finding and found hooks', async ({ assert }) => { + test('execute finding, loading and loaded hooks', async ({ assert }) => { const kernel = new Kernel() const stack: string[] = [] @@ -137,17 +137,23 @@ test.group('Kernel | find', () => { stack.push('finding') }) - kernel.found((command) => { - assert.equal(command.commandName, 'make:model') - stack.push('found') + kernel.loading((command) => { + assert.deepEqual(command, MakeModel.serialize()) + stack.push('loading') + }) + + kernel.loaded((command) => { + assert.strictEqual(command, MakeModel) + stack.push('loaded') }) + const command = await kernel.find('make:model') assert.strictEqual(command, MakeModel) - assert.deepEqual(stack, ['finding', 'found']) + assert.deepEqual(stack, ['finding', 'loading', 'loaded']) }) - test('do not execute found hook when command not found', async ({ assert }) => { + test('do not execute loading hook when command not found', async ({ assert }) => { const kernel = new Kernel() const stack: string[] = [] @@ -169,8 +175,7 @@ test.group('Kernel | find', () => { stack.push('finding') }) - kernel.found((command) => { - assert.equal(command.commandName, 'make:model') + kernel.loading(() => { stack.push('found') }) diff --git a/tests/kernel/terminate.spec.ts b/tests/kernel/terminate.spec.ts index e19fb90..31a541e 100644 --- a/tests/kernel/terminate.spec.ts +++ b/tests/kernel/terminate.spec.ts @@ -43,7 +43,9 @@ test.group('Kernel | terminate', (group) => { kernel.addLoader(new CommandsList([MakeController, MakeModel])) kernel.executed(async () => { - await kernel.terminate(new MakeModel(kernel, { _: [] }, kernel.ui)) + await kernel.terminate( + new MakeModel(kernel, { args: [], _: [], flags: {}, unknownFlags: [] }, kernel.ui) + ) }) kernel.executed(async () => { assert.equal(kernel.getState(), 'running') @@ -56,7 +58,7 @@ test.group('Kernel | terminate', (group) => { assert.equal(kernel.getState(), 'terminated') }) - test('terminate from a main command', async ({ assert }) => { + test('terminate automatically after running the main command', async ({ assert }) => { const kernel = new Kernel() class MakeController extends BaseCommand { @@ -70,14 +72,89 @@ test.group('Kernel | terminate', (group) => { } kernel.addLoader(new CommandsList([MakeController, MakeModel])) - kernel.executed(async (command) => { - await kernel.terminate(command) + await kernel.handle(['make:controller']) + + assert.equal(kernel.exitCode, 0) + assert.equal(process.exitCode, 0) + assert.equal(kernel.getState(), 'terminated') + }) + + test('do not terminate if command options.staysAlive is true', async ({ assert }) => { + const kernel = new Kernel() + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + static options = { + staysAlive: true, + } + async run() {} + } + + class MakeModel extends BaseCommand { + static commandName = 'make:model' + async run() {} + } + + kernel.addLoader(new CommandsList([MakeController, MakeModel])) + await kernel.handle(['make:controller']) + + assert.isUndefined(kernel.exitCode) + assert.isUndefined(process.exitCode) + assert.equal(kernel.getState(), 'running') + }) + + test('terminate when alive command calls terminate method', async ({ assert }) => { + const kernel = new Kernel() + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + static options = { + staysAlive: true, + } + async run() { + await this.terminate() + } + } + + class MakeModel extends BaseCommand { + static commandName = 'make:model' + async run() {} + } + + kernel.addLoader(new CommandsList([MakeController, MakeModel])) + await kernel.handle(['make:controller']) + + assert.equal(kernel.exitCode, 0) + assert.equal(process.exitCode, 0) + assert.equal(kernel.getState(), 'terminated') + }) + + test('terminate from flag listener', async ({ assert }) => { + const kernel = new Kernel() + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + static options = { + staysAlive: true, + } + async run() {} + } + + class MakeModel extends BaseCommand { + static commandName = 'make:model' + async run() {} + } + + kernel.addLoader(new CommandsList([MakeController, MakeModel])) + kernel.defineFlag('help', { + type: 'boolean', }) - kernel.executed(async () => { - assert.equal(kernel.getState(), 'terminated') + + kernel.on('help', () => { + return true }) - await kernel.handle(['make:controller']) + await kernel.handle(['make:controller', '--help']) assert.equal(kernel.exitCode, 0) assert.equal(process.exitCode, 0) diff --git a/tests/loaders/list.spec.ts b/tests/loaders/list.spec.ts new file mode 100644 index 0000000..0ed2e17 --- /dev/null +++ b/tests/loaders/list.spec.ts @@ -0,0 +1,57 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { BaseCommand } from '../../src/commands/base.js' +import { CommandsList } from '../../src/loaders/list.js' + +test.group('Loaders | list', () => { + test('instantiate loader with commands', async ({ assert }) => { + class MakeController extends BaseCommand { + static commandName = 'make:controller' + } + + class MakeModel extends BaseCommand { + static commandName = 'make:model' + } + + const loader = new CommandsList([MakeController, MakeModel]) + assert.strictEqual(await loader.getCommand(MakeController.serialize()), MakeController) + assert.strictEqual(await loader.getCommand(MakeModel.serialize()), MakeModel) + }) + + test('return null when unable to find a command', async ({ assert }) => { + class MakeController extends BaseCommand { + static commandName = 'make:controller' + } + + class MakeModel extends BaseCommand { + static commandName = 'make:model' + } + + const loader = new CommandsList([MakeModel]) + assert.isNull(await loader.getCommand(MakeController.serialize())) + }) + + test('get metadata for registered commands', async ({ assert }) => { + class MakeController extends BaseCommand { + static commandName = 'make:controller' + } + + class MakeModel extends BaseCommand { + static commandName = 'make:model' + } + + const loader = new CommandsList([MakeController, MakeModel]) + assert.deepEqual(await loader.getMetaData(), [ + MakeController.serialize(), + MakeModel.serialize(), + ]) + }) +}) diff --git a/tests/parser.spec.ts b/tests/parser.spec.ts index 36796bb..cbb0776 100644 --- a/tests/parser.spec.ts +++ b/tests/parser.spec.ts @@ -19,22 +19,23 @@ test.group('Parser | flags', () => { MakeModel.defineFlag('batchSize', { type: 'number' }) MakeModel.defineFlag('files', { type: 'array' }) - const output = new Parser(MakeModel.getParserOptions()).parse( - '--connection=sqlite --drop-all --batch-size=1 --files=a,b' + assert.deepEqual( + new Parser(MakeModel.getParserOptions()).parse( + '--connection=sqlite --drop-all --batch-size=1 --files=a,b' + ), + { + _: [], + args: [], + unknownFlags: [], + flags: { + 'batch-size': 1, + 'connection': 'sqlite', + 'drop-all': true, + 'files': ['a,b'], + }, + } ) - assert.deepEqual(output, { - _: [], - args: [], - unknownFlags: [], - flags: { - 'batch-size': 1, - 'connection': 'sqlite', - 'drop-all': true, - 'files': ['a,b'], - }, - }) - assert.deepEqual(new Parser(MakeModel.getParserOptions()).parse('--files=a --files=b'), { _: [], args: [], @@ -52,9 +53,7 @@ test.group('Parser | flags', () => { MakeModel.defineFlag('batchSize', { type: 'number', alias: 'b' }) MakeModel.defineFlag('files', { type: 'array', alias: 'f' }) - const output = new Parser(MakeModel.getParserOptions()).parse('-c=sqlite -d -b=1 -f=a,b') - - assert.deepEqual(output, { + assert.deepEqual(new Parser(MakeModel.getParserOptions()).parse('-c=sqlite -d -b=1 -f=a,b'), { _: [], args: [], unknownFlags: [], @@ -92,7 +91,7 @@ test.group('Parser | flags', () => { }) }) - test('set flags to default values when not set', ({ assert }) => { + test('set flags to default values when not mentioned', ({ assert }) => { class MakeModel extends BaseCommand {} MakeModel.defineFlag('dropAll', { type: 'boolean', default: false }) MakeModel.defineFlag('connection', { type: 'string', default: 'sqlite' }) @@ -136,7 +135,7 @@ test.group('Parser | flags', () => { }) }) - test('return parsed values', ({ assert }) => { + test('parse flags using the parse method', ({ assert }) => { class MakeModel extends BaseCommand {} MakeModel.defineFlag('dropAll', { type: 'boolean', @@ -229,7 +228,7 @@ test.group('Parser | flags', () => { }) }) - test('do not call parse method when flags are not set', ({ assert }) => { + test('do not call parse method when flags are not mentioned', ({ assert }) => { class MakeModel extends BaseCommand {} MakeModel.defineFlag('dropAll', { type: 'boolean', @@ -289,7 +288,7 @@ test.group('Parser | arguments', () => { }) }) - test('use default value when argument is not defined', ({ assert }) => { + test('use default value when argument is not mentioned', ({ assert }) => { class MakeModel extends BaseCommand {} MakeModel.defineArgument('name', { type: 'string' }) MakeModel.defineArgument('connections', { type: 'spread', default: ['sqlite'] }) @@ -303,7 +302,7 @@ test.group('Parser | arguments', () => { }) }) - test('do not use default value when argument is defined as empty string', ({ assert }) => { + test('do not use default value when argument is mentioned as empty string', ({ assert }) => { class MakeModel extends BaseCommand {} MakeModel.defineArgument('name', { type: 'string' }) MakeModel.defineArgument('connections', { type: 'spread', default: ['sqlite'] }) @@ -395,18 +394,10 @@ test.group('Parser | arguments', () => { class MakeModel extends BaseCommand {} MakeModel.defineArgument('name', { type: 'string', - parse(value) { - return value.toUpperCase() - }, }) MakeModel.defineArgument('connections', { type: 'spread', default: 1, - parse(values) { - return values.map((value: string | number) => { - return typeof value === 'string' ? value.toUpperCase() : value - }) - }, }) const output = new Parser(MakeModel.getParserOptions()).parse([]) @@ -423,18 +414,10 @@ test.group('Parser | arguments', () => { MakeModel.defineArgument('name', { type: 'string', default: null, - parse(value) { - return value ? value.toUpperCase() : value - }, }) MakeModel.defineArgument('connections', { type: 'spread', default: 1, - parse(values) { - return values.map((value: string | number) => { - return typeof value === 'string' ? value.toUpperCase() : value - }) - }, }) const output = new Parser(MakeModel.getParserOptions()).parse([]) From 50d3d7676d6bb6d3b5c1713e77d42aa3fdc21d90 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 24 Jan 2023 15:53:44 +0530 Subject: [PATCH 003/112] docs(README): update documentation --- README.md | 54 +++++++++++++++++------------------------------------- 1 file changed, 17 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index e59add5..0a51c8e 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,27 @@ -
- -
+# @adonisjs/ace
-
-

Command line framework of AdonisJS

-

Ace is command line framework embedded into AdonisJS for creating CLI commands. AdonisJS is the only Node.js framework that allows creating CLI commands as part of the application codebase.

-
+[![gh-workflow-image]][gh-workflow-url] [![npm-image]][npm-url] ![][typescript-image] [![license-image]][license-url] [![synk-image]][synk-url] -
+## Introduction +Ace is the command-line framework for Node.js. It is built with **testing in mind**, is **light weight** in comparison to other CLI frameworks, and offers a clean API for creating CLI commands. -
+## Official Documentation +The documentation is available on the official website -[![gh-workflow-image]][gh-workflow-url] [![npm-image]][npm-url] ![][typescript-image] [![license-image]][license-url] [![synk-image]][synk-url] +## Contributing +One of the primary goals of AdonisJS is to have a vibrant community of users and contributors who believes in the principles of the framework. + +We encourage you to read the [contribution guide](https://github.com/adonisjs/.github/blob/main/docs/CONTRIBUTING.md) before contributing to the framework. + +## Code of Conduct +In order to ensure that the AdonisJS community is welcoming to all, please review and abide by the [Code of Conduct](https://github.com/adonisjs/.github/blob/main/docs/CODE_OF_CONDUCT.md). + +## License +AdonisJS Ace is open-sourced software licensed under the [MIT license](LICENSE.md). -
- - - -
- Built with ❤︎ by Harminder Virk -
- -[gh-workflow-image]: https://img.shields.io/github/workflow/status/adonisjs/ace/test?style=for-the-badge +[gh-workflow-image]: https://img.shields.io/github/actions/workflow/status/adonisjs/ace/test.yml?style=for-the-badge [gh-workflow-url]: https://github.com/adonisjs/ace/actions/workflows/test.yml "Github action" [npm-image]: https://img.shields.io/npm/v/@adonisjs/ace/latest.svg?style=for-the-badge&logo=npm From ec115f88e6fa61a4a4db4273adb53ef70a5136bf Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 24 Jan 2023 20:13:19 +0530 Subject: [PATCH 004/112] feat: add modules loader to load commands from ES modules --- index.ts | 1 + package.json | 4 +- src/helpers.ts | 55 ++++- src/loaders/modules_loader.ts | 150 ++++++++++++++ tests/loaders/modules_loader.spec.ts | 289 +++++++++++++++++++++++++++ 5 files changed, 497 insertions(+), 2 deletions(-) create mode 100644 src/loaders/modules_loader.ts create mode 100644 tests/loaders/modules_loader.spec.ts diff --git a/index.ts b/index.ts index 4a54743..0a6f1f1 100644 --- a/index.ts +++ b/index.ts @@ -16,3 +16,4 @@ export { BaseCommand } from './src/commands/base.js' export { HelpCommand } from './src/commands/help.js' export { ListCommand } from './src/commands/list.js' export { CommandsList } from './src/loaders/list.js' +export { ModulesLoader } from './src/loaders/modules_loader.js' diff --git a/package.json b/package.json index ca4892b..4a8df41 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "lint": "eslint . --ext=.ts", "format": "prettier --write .", "sync-labels": "github-label-sync --labels .github/labels.json adonisjs/ace", - "vscode:test": "node --loader=ts-node/esm bin/test.ts" + "vscode:test": "node --loader=ts-node/esm --experimental-import-meta-resolve bin/test.ts" }, "keywords": [ "adonisjs", @@ -53,6 +53,7 @@ "@japa/spec-reporter": "^1.2.0", "@poppinss/dev-utils": "^2.0.3", "@swc/core": "^1.3.27", + "@types/fs-extra": "^11.0.1", "@types/node": "^18.7.15", "@types/sinon": "^10.0.13", "@types/string-similarity": "^4.0.0", @@ -63,6 +64,7 @@ "eslint-config-prettier": "^8.6.0", "eslint-plugin-adonis": "^3.0.3", "eslint-plugin-prettier": "^4.2.1", + "fs-extra": "^11.1.0", "github-label-sync": "^2.2.0", "husky": "^8.0.3", "np": "^7.6.3", diff --git a/src/helpers.ts b/src/helpers.ts index 1f229bf..65561bb 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -7,7 +7,10 @@ * file that was distributed with this source code. */ -import type { UIPrimitives } from './types.js' +import { inspect } from 'node:util' +import { RuntimeException } from '@poppinss/utils' +import type { CommandMetaData, UIPrimitives } from './types.js' +import { BaseCommand } from '../index.js' /** * Helper to sort array of strings alphabetically. @@ -45,3 +48,53 @@ export function renderErrorWithSuggestions( instructions.getRenderer().logError(instructions.prepare()) } + +/** + * Validates the metadata of a command to ensure it has all the neccessary + * properties + */ +export function validCommandMetaData( + command: unknown, + exportPath: string +): asserts command is CommandMetaData { + if (!command || (typeof command !== 'object' && typeof command !== 'function')) { + throw new RuntimeException( + `Invalid command exported from "${exportPath}" method. Expected object, received "${inspect( + command + )}"` + ) + } + + const expectedProperties: (keyof CommandMetaData)[] = [ + 'commandName', + 'aliases', + 'flags', + 'args', + 'options', + ] + + expectedProperties.forEach((prop) => { + if (prop in command === false) { + throw new RuntimeException( + `Invalid command exported from "${exportPath}" method. Missing property "${prop}"` + ) + } + }) +} + +/** + * Validates the command class. We do not check it against the "BaseCommand" + * class, because the ace version mis-match could make the validation + * fail. + */ +export function validateCommand( + command: unknown, + exportPath: string +): asserts command is typeof BaseCommand { + validCommandMetaData(command, exportPath) + if (typeof command !== 'function' && !command.toString().startsWith('class ')) { + throw new RuntimeException( + `Invalid command exported from "${exportPath}". Expected command to be a class` + ) + } +} diff --git a/src/loaders/modules_loader.ts b/src/loaders/modules_loader.ts new file mode 100644 index 0000000..0e2dc95 --- /dev/null +++ b/src/loaders/modules_loader.ts @@ -0,0 +1,150 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { RuntimeException } from '@poppinss/utils' + +import { BaseCommand } from '../commands/base.js' +import type { CommandMetaData, LoadersContract } from '../types.js' +import { validateCommand, validCommandMetaData } from '../helpers.js' + +/** + * Module based command loader must implement the following methods. + */ +type CommandsLoader = { + list: () => Promise + load: (command: CommandMetaData) => Promise +} + +/** + * Modules loader exposes the API to lazy load commands from + * one or more ES modules. + * + * The modules have to implement the `list` and the `load` + * methods + */ +export class ModulesLoader implements LoadersContract { + /** + * The import root is the base path to use when resolving + * modules mentioned in the command source. + */ + #importRoot: string | URL + + /** + * An array of modules to import for loading commands + */ + #commandSources: string[] + + /** + * A collection of commands with their loaders. The key is the + * command name and the value is the imported loader. + */ + #commandsLoaders?: Map + + constructor(importRoot: string | URL, commandSources: string[]) { + this.#importRoot = importRoot + this.#commandSources = commandSources + } + + /** + * Imports the source by first resolving its import path. + */ + async #importSource(sourcePath: string): Promise<{ loader: CommandsLoader; importPath: string }> { + const importPath = await import.meta.resolve!(sourcePath, this.#importRoot) + const loader = await import(importPath) + + /** + * Ensure the loader has list method + */ + if (typeof loader.list !== 'function') { + throw new RuntimeException( + `Invalid command loader "${sourcePath}". Missing "list" method export` + ) + } + + /** + * Ensure the loader has load method + */ + if (typeof loader.load !== 'function') { + throw new RuntimeException( + `Invalid command loader "${sourcePath}". Missing "load" method export` + ) + } + + return { loader, importPath } + } + + /** + * Returns an array of commands returns by the loader list method + */ + async #getCommandsList(loader: CommandsLoader, importPath: string): Promise { + const list = await loader.list() + if (!Array.isArray(list)) { + throw new RuntimeException( + `Invalid commands list. The "${importPath}.list" method must return an array of commands` + ) + } + + return list + } + + /** + * Loads commands from the registered sources + */ + async #loadCommands() { + this.#commandsLoaders = new Map() + const commands: CommandMetaData[] = [] + + for (let sourcePath of this.#commandSources) { + const { loader } = await this.#importSource(sourcePath) + const list = await this.#getCommandsList(loader, sourcePath) + + list.forEach((metaData) => { + validCommandMetaData(metaData, `${sourcePath}.list`) + commands.push(metaData) + this.#commandsLoaders!.set(metaData.commandName, { loader, sourcePath }) + }) + } + + return commands + } + + /** + * Returns an array of command's metadata + */ + async getMetaData(): Promise { + return this.#loadCommands() + } + + /** + * Returns the command class constructor for a given command. Null + * is returned when unable to lookup the command + */ + async getCommand(metaData: CommandMetaData): Promise { + /** + * Running "loadCommands" method instantiates the commands loader + * collection + */ + if (!this.#commandsLoaders) { + await this.#loadCommands() + } + + const commandLoader = this.#commandsLoaders!.get(metaData.commandName) + if (!commandLoader) { + return null + } + + const command = await commandLoader.loader.load(metaData) + if (command === null || command === undefined) { + return null + } + + validateCommand(command, `${commandLoader.sourcePath}.load`) + return command + } +} diff --git a/tests/loaders/modules_loader.spec.ts b/tests/loaders/modules_loader.spec.ts new file mode 100644 index 0000000..59621fa --- /dev/null +++ b/tests/loaders/modules_loader.spec.ts @@ -0,0 +1,289 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import fs from 'fs-extra' +import { join } from 'node:path' +import { test } from '@japa/runner' +import { fileURLToPath } from 'node:url' +import { ModulesLoader } from '../../src/loaders/modules_loader.js' + +const BASE_URL = new URL('./tmp/', import.meta.url) +const BASE_PATH = fileURLToPath(BASE_URL) + +test.group('Loaders | modules', (group) => { + group.each.setup(() => { + return () => fs.remove(BASE_PATH) + }) + + test('raise error when unable to import loaders', async ({ assert }) => { + const loader = new ModulesLoader(BASE_URL, ['./loader_one.js']) + await assert.rejects(() => loader.getMetaData(), /Cannot find module/) + }) + + test('raise error when loader does not implement the list method', async ({ assert }) => { + await fs.outputFile( + join(BASE_PATH, './loader_one.js'), + ` + ` + ) + + const loader = new ModulesLoader(BASE_URL, ['./loader_one.js']) + await assert.rejects( + () => loader.getMetaData(), + 'Invalid command loader "./loader_one.js". Missing "list" method export' + ) + }) + + test('raise error when loader does not implement the load method', async ({ assert }) => { + await fs.outputFile( + join(BASE_PATH, './loader_one.js'), + ` + export async function list() {} + ` + ) + + const loader = new ModulesLoader(BASE_URL, ['./loader_one.js?v=1']) + await assert.rejects( + () => loader.getMetaData(), + 'Invalid command loader "./loader_one.js?v=1". Missing "load" method export' + ) + }) + + test('raise error when list method does not return an array', async ({ assert }) => { + await fs.outputFile( + join(BASE_PATH, './loader_one.js'), + ` + export async function list() {} + export async function load() {} + ` + ) + + const loader = new ModulesLoader(BASE_URL, ['./loader_one.js?v=2']) + await assert.rejects( + () => loader.getMetaData(), + 'Invalid commands list. The "./loader_one.js?v=2.list" method must return an array of commands' + ) + }) + + test('raise error when list array does not have objects', async ({ assert }) => { + await fs.outputFile( + join(BASE_PATH, './loader_one.js'), + ` + export async function list() { + return ['foo'] + } + export async function load() {} + ` + ) + + const loader = new ModulesLoader(BASE_URL, ['./loader_one.js?v=3']) + await assert.rejects( + () => loader.getMetaData(), + `Invalid command exported from "./loader_one.js?v=3.list" method. Expected object, received "'foo'"` + ) + }) + + test('raise error when list array items are not valid metadata objects', async ({ assert }) => { + await fs.outputFile( + join(BASE_PATH, './loader_one.js'), + ` + export async function list() { + return [{}] + } + export async function load() {} + ` + ) + + const loader = new ModulesLoader(BASE_URL, ['./loader_one.js?v=4']) + await assert.rejects( + () => loader.getMetaData(), + `Invalid command exported from "./loader_one.js?v=4.list" method. Missing property "commandName"` + ) + }) + + test('load commands from multiple module loaders', async ({ assert }) => { + await fs.outputFile( + join(BASE_PATH, './loader_two.js'), + ` + export async function list() { + return [ + { + commandName: 'make:controller', + args: [], + flags: [], + aliases: [], + options: {}, + } + ] + } + export async function load() {} + ` + ) + + await fs.outputFile( + join(BASE_PATH, './loader_three.js'), + ` + export async function list() { + return [ + { + commandName: 'make:model', + args: [], + flags: [], + aliases: [], + options: {}, + } + ] + } + export async function load() {} + ` + ) + + const loader = new ModulesLoader(BASE_URL, ['./loader_two.js', './loader_three.js']) + const commands = await loader.getMetaData() + + assert.deepEqual(commands, [ + { + commandName: 'make:controller', + args: [], + flags: [], + aliases: [], + options: {}, + }, + { + commandName: 'make:model', + args: [], + flags: [], + aliases: [], + options: {}, + }, + ]) + }) + + test('return null when load method returns undefined', async ({ assert }) => { + await fs.outputFile( + join(BASE_PATH, './loader_two.js'), + ` + export async function list() { + return [ + { + commandName: 'make:controller', + args: [], + flags: [], + aliases: [], + options: {}, + } + ] + } + export async function load() {} + ` + ) + + const loader = new ModulesLoader(BASE_URL, ['./loader_two.js?v=1']) + const command = await loader.getCommand({ commandName: 'make:controller' } as any) + assert.isNull(command) + }) + + test('return null when command is unknown', async ({ assert }) => { + const loader = new ModulesLoader(BASE_URL, []) + const command = await loader.getCommand({ commandName: 'make:controller' } as any) + assert.isNull(command) + }) + + test('return null when load method returns null', async ({ assert }) => { + await fs.outputFile( + join(BASE_PATH, './loader_two.js'), + ` + export async function list() { + return [ + { + commandName: 'make:controller', + args: [], + flags: [], + aliases: [], + options: {}, + } + ] + } + export async function load() { + return null + } + ` + ) + + const loader = new ModulesLoader(BASE_URL, ['./loader_two.js?v=2']) + const command = await loader.getCommand({ commandName: 'make:controller' } as any) + assert.isNull(command) + }) + + test('raise error when load method does not return command class', async ({ assert }) => { + await fs.outputFile( + join(BASE_PATH, './loader_two.js'), + ` + export async function list() { + return [ + { + commandName: 'make:controller', + args: [], + flags: [], + aliases: [], + options: {}, + } + ] + } + export async function load() { + return { + commandName: 'make:controller', + args: [], + flags: [], + aliases: [], + options: {}, + } + } + ` + ) + + const loader = new ModulesLoader(BASE_URL, ['./loader_two.js?v=3']) + await assert.rejects( + () => loader.getCommand({ commandName: 'make:controller' } as any), + 'Invalid command exported from "./loader_two.js?v=3.load". Expected command to be a class' + ) + }) + + test('get command constructor returned by the load method', async ({ assert }) => { + await fs.outputFile( + join(BASE_PATH, './loader_two.js'), + ` + export async function list() { + return [ + { + commandName: 'make:controller', + args: [], + flags: [], + aliases: [], + options: {}, + } + ] + } + export async function load() { + return class Command { + static commandName = 'make:controller' + static args = [] + static flags = [] + static aliases = [] + static options = {} + } + } + ` + ) + + const loader = new ModulesLoader(BASE_URL, ['./loader_two.js?v=4']) + const command = await loader.getCommand({ commandName: 'make:controller' } as any) + assert.isFunction(command) + }) +}) From 48442d5687a97d45aaa05214aaad4fed42603ef8 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 24 Jan 2023 20:14:50 +0530 Subject: [PATCH 005/112] refactor: rename CommandsList to ListLoader --- examples/main.ts | 4 ++-- index.ts | 2 +- src/kernel.ts | 4 ++-- src/loaders/{list.ts => list_loader.ts} | 2 +- tests/commands/list.spec.ts | 10 +++++----- tests/kernel/boot.spec.ts | 10 +++++----- tests/kernel/exec.spec.ts | 22 +++++++++++----------- tests/kernel/find.spec.ts | 22 +++++++++++----------- tests/kernel/flag_listeners.spec.ts | 8 ++++---- tests/kernel/handle.spec.ts | 18 +++++++++--------- tests/kernel/loaders.spec.ts | 12 ++++++------ tests/kernel/main.spec.ts | 24 ++++++++++++------------ tests/kernel/terminate.spec.ts | 12 ++++++------ tests/loaders/list.spec.ts | 8 ++++---- 14 files changed, 79 insertions(+), 79 deletions(-) rename src/loaders/{list.ts => list_loader.ts} (94%) diff --git a/examples/main.ts b/examples/main.ts index a274107..03f7714 100644 --- a/examples/main.ts +++ b/examples/main.ts @@ -11,7 +11,7 @@ import { Kernel } from '../src/kernel.js' import { args } from '../src/decorators/args.js' import { flags } from '../src/decorators/flags.js' import { BaseCommand } from '../src/commands/base.js' -import { CommandsList } from '../src/loaders/list.js' +import { ListLoader } from '../src/loaders/list_loader.js' import { HelpCommand } from '../src/commands/help.js' const kernel = new Kernel() @@ -73,7 +73,7 @@ class DbWipe extends BaseCommand { } kernel.addLoader( - new CommandsList([ + new ListLoader([ HelpCommand, Configure, Build, diff --git a/index.ts b/index.ts index 0a6f1f1..e3f694e 100644 --- a/index.ts +++ b/index.ts @@ -15,5 +15,5 @@ export { flags } from './src/decorators/flags.js' export { BaseCommand } from './src/commands/base.js' export { HelpCommand } from './src/commands/help.js' export { ListCommand } from './src/commands/list.js' -export { CommandsList } from './src/loaders/list.js' +export { ListLoader } from './src/loaders/list_loader.js' export { ModulesLoader } from './src/loaders/modules_loader.js' diff --git a/src/kernel.ts b/src/kernel.ts index 422c3ef..566f13f 100644 --- a/src/kernel.ts +++ b/src/kernel.ts @@ -16,7 +16,7 @@ import { Parser } from './parser.js' import * as errors from './errors.js' import { ListCommand } from './commands/list.js' import { BaseCommand } from './commands/base.js' -import { CommandsList } from './loaders/list.js' +import { ListLoader } from './loaders/list_loader.js' import { sortAlphabetically, renderErrorWithSuggestions } from './helpers.js' import type { @@ -582,7 +582,7 @@ export class Kernel { /** * Registering the default command */ - this.addLoader(new CommandsList([this.#defaultCommand])) + this.addLoader(new ListLoader([this.#defaultCommand])) /** * Set state to booted diff --git a/src/loaders/list.ts b/src/loaders/list_loader.ts similarity index 94% rename from src/loaders/list.ts rename to src/loaders/list_loader.ts index ad7ede7..27154bc 100644 --- a/src/loaders/list.ts +++ b/src/loaders/list_loader.ts @@ -14,7 +14,7 @@ import type { CommandMetaData, LoadersContract } from '../types.js' * The CommandsList loader registers commands classes with the kernel. * The commands are kept within memory */ -export class CommandsList implements LoadersContract { +export class ListLoader implements LoadersContract { #commands: (typeof BaseCommand)[] constructor(commands: (typeof BaseCommand)[]) { diff --git a/tests/commands/list.spec.ts b/tests/commands/list.spec.ts index 96c72ec..99e0462 100644 --- a/tests/commands/list.spec.ts +++ b/tests/commands/list.spec.ts @@ -11,7 +11,7 @@ import { test } from '@japa/runner' import { Kernel } from '../../src/kernel.js' import { args } from '../../src/decorators/args.js' import { flags } from '../../src/decorators/flags.js' -import { CommandsList } from '../../src/loaders/list.js' +import { ListLoader } from '../../src/loaders/list_loader.js' import { BaseCommand } from '../../src/commands/base.js' test.group('List command', () => { @@ -36,7 +36,7 @@ test.group('List command', () => { static description: string = 'Make a new HTTP controller' } - kernel.addLoader(new CommandsList([Serve, MakeController])) + kernel.addLoader(new ListLoader([Serve, MakeController])) const command = await kernel.exec('list', []) assert.equal(command.exitCode, 0) @@ -92,7 +92,7 @@ test.group('List command', () => { static description: string = 'Make a new HTTP controller' } - kernel.addLoader(new CommandsList([Serve, MakeController])) + kernel.addLoader(new ListLoader([Serve, MakeController])) const command = await kernel.exec('list', ['make']) assert.equal(command.exitCode, 0) @@ -133,7 +133,7 @@ test.group('List command', () => { static description: string = 'Make a new HTTP controller' } - kernel.addLoader(new CommandsList([Serve, MakeController])) + kernel.addLoader(new ListLoader([Serve, MakeController])) const command = await kernel.exec('list', ['foo']) assert.equal(command.exitCode, 1) @@ -166,7 +166,7 @@ test.group('List command', () => { static description: string = 'Make a new HTTP controller' } - kernel.addLoader(new CommandsList([Serve, MakeController])) + kernel.addLoader(new ListLoader([Serve, MakeController])) kernel.defineFlag('help', { type: 'boolean', description: 'View help of a given command' }) const command = await kernel.exec('list', []) diff --git a/tests/kernel/boot.spec.ts b/tests/kernel/boot.spec.ts index 6b7a2b6..0bd9d50 100644 --- a/tests/kernel/boot.spec.ts +++ b/tests/kernel/boot.spec.ts @@ -11,7 +11,7 @@ import { test } from '@japa/runner' import { Kernel } from '../../src/kernel.js' import { BaseCommand } from '../../src/commands/base.js' -import { CommandsList } from '../../src/loaders/list.js' +import { ListLoader } from '../../src/loaders/list_loader.js' test.group('Kernel | boot', () => { test('load commands from loader during boot phase', async ({ assert }) => { @@ -25,7 +25,7 @@ test.group('Kernel | boot', () => { static commandName = 'make:model' } - kernel.addLoader(new CommandsList([MakeController, MakeModel])) + kernel.addLoader(new ListLoader([MakeController, MakeModel])) await kernel.boot() assert.deepEqual(kernel.getCommands(), [ @@ -46,7 +46,7 @@ test.group('Kernel | boot', () => { static commandName = 'make:model' } - kernel.addLoader(new CommandsList([MakeController, MakeModel])) + kernel.addLoader(new ListLoader([MakeController, MakeModel])) await kernel.boot() await kernel.boot() await kernel.boot() @@ -74,7 +74,7 @@ test.group('Kernel | boot', () => { static commandName = 'migration:run' } - kernel.addLoader(new CommandsList([MakeController, MakeModel, MigrationRun])) + kernel.addLoader(new ListLoader([MakeController, MakeModel, MigrationRun])) await kernel.boot() assert.deepEqual(kernel.getNamespaces(), ['make', 'migration']) @@ -98,7 +98,7 @@ test.group('Kernel | boot', () => { static aliases: string[] = ['migrate'] } - kernel.addLoader(new CommandsList([MakeController, MakeModel, MigrationRun])) + kernel.addLoader(new ListLoader([MakeController, MakeModel, MigrationRun])) await kernel.boot() assert.deepEqual(kernel.getAliases(), ['mc', 'mm', 'migrate']) diff --git a/tests/kernel/exec.spec.ts b/tests/kernel/exec.spec.ts index 9906ddb..c21e458 100644 --- a/tests/kernel/exec.spec.ts +++ b/tests/kernel/exec.spec.ts @@ -10,7 +10,7 @@ import { test } from '@japa/runner' import { Kernel } from '../../src/kernel.js' import { BaseCommand } from '../../src/commands/base.js' -import { CommandsList } from '../../src/loaders/list.js' +import { ListLoader } from '../../src/loaders/list_loader.js' test.group('Kernel | exec', () => { test('execute command', async ({ assert }) => { @@ -23,7 +23,7 @@ test.group('Kernel | exec', () => { } } - kernel.addLoader(new CommandsList([MakeController])) + kernel.addLoader(new ListLoader([MakeController])) const command = await kernel.exec('make:controller', []) assert.equal(command.result, 'executed') @@ -45,7 +45,7 @@ test.group('Kernel | exec', () => { } } - kernel.addLoader(new CommandsList([MakeController])) + kernel.addLoader(new ListLoader([MakeController])) kernel.executing((cmd) => { expectTypeOf(cmd).toEqualTypeOf() stack.push('executing') @@ -75,7 +75,7 @@ test.group('Kernel | exec', () => { } MakeController.defineArgument('name', { type: 'string' }) - kernel.addLoader(new CommandsList([MakeController])) + kernel.addLoader(new ListLoader([MakeController])) await assert.rejects( () => kernel.exec('make:controller', []), 'Missing required argument "name"' @@ -93,7 +93,7 @@ test.group('Kernel | exec', () => { } MakeController.defineArgument('name', { type: 'string' }) - kernel.addLoader(new CommandsList([MakeController])) + kernel.addLoader(new ListLoader([MakeController])) await assert.rejects(() => kernel.exec('foo', []), 'Command "foo" is not defined') assert.isUndefined(kernel.exitCode) assert.equal(kernel.getState(), 'booted') @@ -110,7 +110,7 @@ test.group('Kernel | exec', () => { } MakeController.defineArgument('name', { type: 'string' }) - kernel.addLoader(new CommandsList([MakeController])) + kernel.addLoader(new ListLoader([MakeController])) kernel.executing(() => { throw new Error('Pre hook failed') }) @@ -129,7 +129,7 @@ test.group('Kernel | exec', () => { } MakeController.defineArgument('name', { type: 'string' }) - kernel.addLoader(new CommandsList([MakeController])) + kernel.addLoader(new ListLoader([MakeController])) kernel.executed(() => { throw new Error('Post hook failed') }) @@ -151,7 +151,7 @@ test.group('Kernel | exec', () => { MakeController.defineArgument('name', { type: 'string' }) - kernel.addLoader(new CommandsList([MakeController])) + kernel.addLoader(new ListLoader([MakeController])) kernel.terminating(() => { stack.push('terminating') throw new Error('Never expected to run') @@ -193,7 +193,7 @@ test.group('Kernel | exec', () => { MakeModel.defineFlag('connection', { type: 'string' }) const kernel = new Kernel() - kernel.addLoader(new CommandsList([MakeModel])) + kernel.addLoader(new ListLoader([MakeModel])) kernel.registerExecutor({ create(Command, parsed, self) { @@ -242,7 +242,7 @@ test.group('Kernel | exec', () => { MakeController.defineFlag('connections', { type: 'string' }) - kernel.addLoader(new CommandsList([MakeController])) + kernel.addLoader(new ListLoader([MakeController])) kernel.on('connections', () => { throw new Error('Never expected to reach here') }) @@ -267,7 +267,7 @@ test.group('Kernel | exec', () => { } } - kernel.addLoader(new CommandsList([MakeController])) + kernel.addLoader(new ListLoader([MakeController])) kernel.defineFlag('help', { type: 'boolean' }) await assert.rejects( diff --git a/tests/kernel/find.spec.ts b/tests/kernel/find.spec.ts index c4558a7..b615c82 100644 --- a/tests/kernel/find.spec.ts +++ b/tests/kernel/find.spec.ts @@ -11,7 +11,7 @@ import { test } from '@japa/runner' import { Kernel } from '../../src/kernel.js' import { BaseCommand } from '../../src/commands/base.js' -import { CommandsList } from '../../src/loaders/list.js' +import { ListLoader } from '../../src/loaders/list_loader.js' import { CommandMetaData } from '../../src/types.js' test.group('Kernel | find', () => { @@ -26,7 +26,7 @@ test.group('Kernel | find', () => { static commandName = 'make:model' } - kernel.addLoader(new CommandsList([MakeController, MakeModel])) + kernel.addLoader(new ListLoader([MakeController, MakeModel])) await kernel.boot() const command = await kernel.find('make:controller') @@ -45,7 +45,7 @@ test.group('Kernel | find', () => { static commandName = 'make:model' } - kernel.addLoader(new CommandsList([MakeController, MakeModel])) + kernel.addLoader(new ListLoader([MakeController, MakeModel])) kernel.addAlias('controller', 'make:controller') await kernel.boot() @@ -65,7 +65,7 @@ test.group('Kernel | find', () => { static commandName = 'make:model' } - kernel.addLoader(new CommandsList([MakeController, MakeModel])) + kernel.addLoader(new ListLoader([MakeController, MakeModel])) await kernel.boot() await assert.rejects(() => kernel.find('foo'), 'Command "foo" is not defined') @@ -83,7 +83,7 @@ test.group('Kernel | find', () => { static commandName = 'make:model' } - class CustomLoader extends CommandsList { + class CustomLoader extends ListLoader { async getCommand(_: CommandMetaData): Promise { return null } @@ -107,8 +107,8 @@ test.group('Kernel | find', () => { static commandName = 'make:model' } - kernel.addLoader(new CommandsList([MakeController])) - kernel.addLoader(new CommandsList([MakeModel])) + kernel.addLoader(new ListLoader([MakeController])) + kernel.addLoader(new ListLoader([MakeModel])) await kernel.boot() const command = await kernel.find('make:model') @@ -128,8 +128,8 @@ test.group('Kernel | find', () => { static commandName = 'make:model' } - kernel.addLoader(new CommandsList([MakeController])) - kernel.addLoader(new CommandsList([MakeModel])) + kernel.addLoader(new ListLoader([MakeController])) + kernel.addLoader(new ListLoader([MakeModel])) await kernel.boot() kernel.finding((commandName) => { @@ -166,8 +166,8 @@ test.group('Kernel | find', () => { static commandName = 'make:model' } - kernel.addLoader(new CommandsList([MakeController])) - kernel.addLoader(new CommandsList([MakeModel])) + kernel.addLoader(new ListLoader([MakeController])) + kernel.addLoader(new ListLoader([MakeModel])) await kernel.boot() kernel.finding((commandName) => { diff --git a/tests/kernel/flag_listeners.spec.ts b/tests/kernel/flag_listeners.spec.ts index 0e29edd..d2a1c8b 100644 --- a/tests/kernel/flag_listeners.spec.ts +++ b/tests/kernel/flag_listeners.spec.ts @@ -10,7 +10,7 @@ import { test } from '@japa/runner' import { Kernel } from '../../src/kernel.js' import { BaseCommand } from '../../src/commands/base.js' -import { CommandsList } from '../../src/loaders/list.js' +import { ListLoader } from '../../src/loaders/list_loader.js' import { ParsedOutput, UIPrimitives } from '../../src/types.js' test.group('Kernel | handle', (group) => { @@ -29,7 +29,7 @@ test.group('Kernel | handle', (group) => { } MakeController.defineArgument('name', { type: 'string' }) - kernel.addLoader(new CommandsList([MakeController])) + kernel.addLoader(new ListLoader([MakeController])) kernel.defineFlag('verbose', { type: 'boolean' }) kernel.on('verbose', (Command, _, options) => { assert.strictEqual(Command, MakeController) @@ -54,7 +54,7 @@ test.group('Kernel | handle', (group) => { MakeController.defineArgument('name', { type: 'string' }) MakeController.defineFlag('connection', { type: 'string' }) - kernel.addLoader(new CommandsList([MakeController])) + kernel.addLoader(new ListLoader([MakeController])) kernel.defineFlag('verbose', { type: 'boolean' }) kernel.on('connection', (Command, _, options) => { assert.strictEqual(Command, MakeController) @@ -85,7 +85,7 @@ test.group('Kernel | handle', (group) => { } MakeController.defineArgument('name', { type: 'string' }) - kernel.addLoader(new CommandsList([MakeController])) + kernel.addLoader(new ListLoader([MakeController])) kernel.defineFlag('help', { type: 'boolean' }) kernel.on('help', () => { diff --git a/tests/kernel/handle.spec.ts b/tests/kernel/handle.spec.ts index da27fd8..cfb1219 100644 --- a/tests/kernel/handle.spec.ts +++ b/tests/kernel/handle.spec.ts @@ -12,7 +12,7 @@ import { cliui } from '@poppinss/cliui' import { Kernel } from '../../src/kernel.js' import { CommandOptions } from '../../src/types.js' import { BaseCommand } from '../../src/commands/base.js' -import { CommandsList } from '../../src/loaders/list.js' +import { ListLoader } from '../../src/loaders/list_loader.js' test.group('Kernel | handle', (group) => { group.each.teardown(() => { @@ -29,7 +29,7 @@ test.group('Kernel | handle', (group) => { } } - kernel.addLoader(new CommandsList([MakeController])) + kernel.addLoader(new ListLoader([MakeController])) await kernel.handle(['make:controller']) assert.equal(kernel.exitCode, 0) @@ -49,7 +49,7 @@ test.group('Kernel | handle', (group) => { } MakeController.defineArgument('name', { type: 'string' }) - kernel.addLoader(new CommandsList([MakeController])) + kernel.addLoader(new ListLoader([MakeController])) await kernel.handle(['make:controller']) assert.equal(kernel.getState(), 'terminated') @@ -75,7 +75,7 @@ test.group('Kernel | handle', (group) => { } MakeController.defineArgument('name', { type: 'string' }) - kernel.addLoader(new CommandsList([MakeController])) + kernel.addLoader(new ListLoader([MakeController])) await kernel.handle(['foo']) assert.equal(kernel.getState(), 'terminated') @@ -101,7 +101,7 @@ test.group('Kernel | handle', (group) => { } MakeController.defineArgument('name', { type: 'string' }) - kernel.addLoader(new CommandsList([MakeController])) + kernel.addLoader(new ListLoader([MakeController])) kernel.executing(() => { stack.push('executing') }) @@ -137,7 +137,7 @@ test.group('Kernel | handle', (group) => { } MakeController.defineArgument('name', { type: 'string' }) - kernel.addLoader(new CommandsList([MakeController])) + kernel.addLoader(new ListLoader([MakeController])) kernel.terminating(() => { stack.push('terminating') }) @@ -159,7 +159,7 @@ test.group('Kernel | handle', (group) => { } } MakeController.defineArgument('name', { type: 'string' }) - kernel.addLoader(new CommandsList([MakeController])) + kernel.addLoader(new ListLoader([MakeController])) await kernel.boot() await assert.rejects( @@ -184,7 +184,7 @@ test.group('Kernel | handle', (group) => { } } MakeController.defineArgument('name', { type: 'string' }) - kernel.addLoader(new CommandsList([MakeController])) + kernel.addLoader(new ListLoader([MakeController])) await kernel.handle(['make:controller', 'users']) await assert.rejects( @@ -203,7 +203,7 @@ test.group('Kernel | handle', (group) => { } } MakeController.defineArgument('name', { type: 'string' }) - kernel.addLoader(new CommandsList([MakeController])) + kernel.addLoader(new ListLoader([MakeController])) await kernel.handle(['make:controller', 'users']) await assert.rejects( diff --git a/tests/kernel/loaders.spec.ts b/tests/kernel/loaders.spec.ts index de4e6c4..6550572 100644 --- a/tests/kernel/loaders.spec.ts +++ b/tests/kernel/loaders.spec.ts @@ -11,7 +11,7 @@ import { test } from '@japa/runner' import { Kernel } from '../../src/kernel.js' import { BaseCommand } from '../../src/commands/base.js' -import { CommandsList } from '../../src/loaders/list.js' +import { ListLoader } from '../../src/loaders/list_loader.js' test.group('Kernel | loaders', () => { test('register commands using a loader', async ({ assert }) => { @@ -25,7 +25,7 @@ test.group('Kernel | loaders', () => { static commandName = 'make:model' } - kernel.addLoader(new CommandsList([MakeController, MakeModel])) + kernel.addLoader(new ListLoader([MakeController, MakeModel])) await kernel.boot() assert.deepEqual(kernel.getCommands(), [ @@ -46,8 +46,8 @@ test.group('Kernel | loaders', () => { static commandName = 'make:model' } - kernel.addLoader(new CommandsList([MakeController])) - kernel.addLoader(new CommandsList([MakeModel])) + kernel.addLoader(new ListLoader([MakeController])) + kernel.addLoader(new ListLoader([MakeModel])) await kernel.boot() assert.deepEqual(kernel.getCommands(), [ @@ -68,7 +68,7 @@ test.group('Kernel | loaders', () => { static commandName = 'make:model' } - kernel.addLoader(new CommandsList([MakeController])) + kernel.addLoader(new ListLoader([MakeController])) await kernel.boot() assert.deepEqual(kernel.getCommands(), [ @@ -77,7 +77,7 @@ test.group('Kernel | loaders', () => { ]) assert.throws( - () => kernel.addLoader(new CommandsList([MakeModel])), + () => kernel.addLoader(new ListLoader([MakeModel])), 'Cannot add loader in "booted" state' ) }) diff --git a/tests/kernel/main.spec.ts b/tests/kernel/main.spec.ts index df17291..3060ccb 100644 --- a/tests/kernel/main.spec.ts +++ b/tests/kernel/main.spec.ts @@ -11,7 +11,7 @@ import { test } from '@japa/runner' import { Kernel } from '../../src/kernel.js' import { BaseCommand } from '../../src/commands/base.js' -import { CommandsList } from '../../src/loaders/list.js' +import { ListLoader } from '../../src/loaders/list_loader.js' import { ListCommand } from '../../src/commands/list.js' test.group('Kernel', () => { @@ -26,7 +26,7 @@ test.group('Kernel', () => { static commandName = 'make:model' } - kernel.addLoader(new CommandsList([MakeModel, MakeController])) + kernel.addLoader(new ListLoader([MakeModel, MakeController])) await kernel.boot() const commands = kernel.getCommands() @@ -51,7 +51,7 @@ test.group('Kernel', () => { static commandName = 'make:model' } - kernel.addLoader(new CommandsList([MakeModel, MakeController, ListRoutes])) + kernel.addLoader(new ListLoader([MakeModel, MakeController, ListRoutes])) await kernel.boot() assert.deepEqual(kernel.getNamespaces(), ['list', 'make']) @@ -72,7 +72,7 @@ test.group('Kernel', () => { static commandName = 'make:model' } - kernel.addLoader(new CommandsList([MakeModel, MakeController, ListRoutes])) + kernel.addLoader(new ListLoader([MakeModel, MakeController, ListRoutes])) await kernel.boot() assert.deepEqual( @@ -105,7 +105,7 @@ test.group('Kernel', () => { static commandName = 'make:model' } - kernel.addLoader(new CommandsList([MakeModel, MakeController, ListRoutes, Migrate])) + kernel.addLoader(new ListLoader([MakeModel, MakeController, ListRoutes, Migrate])) await kernel.boot() assert.deepEqual( @@ -134,7 +134,7 @@ test.group('Kernel', () => { static commandName = 'make:model' } - kernel.addLoader(new CommandsList([MakeModel, MakeController, ListRoutes, Migrate])) + kernel.addLoader(new ListLoader([MakeModel, MakeController, ListRoutes, Migrate])) kernel.addAlias('mc', 'make:controller') kernel.addAlias('mm', 'make:model') await kernel.boot() @@ -162,7 +162,7 @@ test.group('Kernel', () => { static commandName = 'make:model' } - kernel.addLoader(new CommandsList([MakeModel, MakeController, ListRoutes, Migrate])) + kernel.addLoader(new ListLoader([MakeModel, MakeController, ListRoutes, Migrate])) kernel.addAlias('mc', 'make:controller') kernel.addAlias('mm', 'make:model') await kernel.boot() @@ -191,7 +191,7 @@ test.group('Kernel', () => { static commandName = 'make:model' } - kernel.addLoader(new CommandsList([MakeModel, MakeController, ListRoutes, Migrate])) + kernel.addLoader(new ListLoader([MakeModel, MakeController, ListRoutes, Migrate])) kernel.addAlias('mc', 'make:controller') kernel.addAlias('mm', 'unrecognized:command') await kernel.boot() @@ -208,7 +208,7 @@ test.group('Kernel', () => { static commandName = 'migrate' } - kernel.addLoader(new CommandsList([Migrate])) + kernel.addLoader(new ListLoader([Migrate])) await kernel.boot() assert.deepEqual(kernel.getCommand('migrate'), Migrate.serialize()) @@ -241,7 +241,7 @@ test.group('Kernel', () => { static commandName = 'make:model' } - kernel.addLoader(new CommandsList([MakeModel, MakeController, ListRoutes, Migrate])) + kernel.addLoader(new ListLoader([MakeModel, MakeController, ListRoutes, Migrate])) kernel.addAlias('mc', 'make:controller') kernel.addAlias('mm', 'unrecognized:command') await kernel.boot() @@ -269,7 +269,7 @@ test.group('Kernel', () => { static commandName = 'make:model' } - kernel.addLoader(new CommandsList([MakeModel, MakeController, ListRoutes, Migrate])) + kernel.addLoader(new ListLoader([MakeModel, MakeController, ListRoutes, Migrate])) kernel.addAlias('mc', 'make:controller') kernel.addAlias('mm', 'unrecognized:command') await kernel.boot() @@ -296,7 +296,7 @@ test.group('Kernel', () => { static commandName = 'make:model' } - kernel.addLoader(new CommandsList([MakeModel, MakeController, ListRoutes, Migrate])) + kernel.addLoader(new ListLoader([MakeModel, MakeController, ListRoutes, Migrate])) kernel.addAlias('mc', 'make:controller') await kernel.boot() diff --git a/tests/kernel/terminate.spec.ts b/tests/kernel/terminate.spec.ts index 31a541e..f83e14a 100644 --- a/tests/kernel/terminate.spec.ts +++ b/tests/kernel/terminate.spec.ts @@ -11,7 +11,7 @@ import { test } from '@japa/runner' import { Kernel } from '../../src/kernel.js' import { BaseCommand } from '../../src/commands/base.js' -import { CommandsList } from '../../src/loaders/list.js' +import { ListLoader } from '../../src/loaders/list_loader.js' test.group('Kernel | terminate', (group) => { group.each.teardown(() => { @@ -41,7 +41,7 @@ test.group('Kernel | terminate', (group) => { async run() {} } - kernel.addLoader(new CommandsList([MakeController, MakeModel])) + kernel.addLoader(new ListLoader([MakeController, MakeModel])) kernel.executed(async () => { await kernel.terminate( new MakeModel(kernel, { args: [], _: [], flags: {}, unknownFlags: [] }, kernel.ui) @@ -71,7 +71,7 @@ test.group('Kernel | terminate', (group) => { async run() {} } - kernel.addLoader(new CommandsList([MakeController, MakeModel])) + kernel.addLoader(new ListLoader([MakeController, MakeModel])) await kernel.handle(['make:controller']) assert.equal(kernel.exitCode, 0) @@ -95,7 +95,7 @@ test.group('Kernel | terminate', (group) => { async run() {} } - kernel.addLoader(new CommandsList([MakeController, MakeModel])) + kernel.addLoader(new ListLoader([MakeController, MakeModel])) await kernel.handle(['make:controller']) assert.isUndefined(kernel.exitCode) @@ -121,7 +121,7 @@ test.group('Kernel | terminate', (group) => { async run() {} } - kernel.addLoader(new CommandsList([MakeController, MakeModel])) + kernel.addLoader(new ListLoader([MakeController, MakeModel])) await kernel.handle(['make:controller']) assert.equal(kernel.exitCode, 0) @@ -145,7 +145,7 @@ test.group('Kernel | terminate', (group) => { async run() {} } - kernel.addLoader(new CommandsList([MakeController, MakeModel])) + kernel.addLoader(new ListLoader([MakeController, MakeModel])) kernel.defineFlag('help', { type: 'boolean', }) diff --git a/tests/loaders/list.spec.ts b/tests/loaders/list.spec.ts index 0ed2e17..81eb572 100644 --- a/tests/loaders/list.spec.ts +++ b/tests/loaders/list.spec.ts @@ -9,7 +9,7 @@ import { test } from '@japa/runner' import { BaseCommand } from '../../src/commands/base.js' -import { CommandsList } from '../../src/loaders/list.js' +import { ListLoader } from '../../src/loaders/list_loader.js' test.group('Loaders | list', () => { test('instantiate loader with commands', async ({ assert }) => { @@ -21,7 +21,7 @@ test.group('Loaders | list', () => { static commandName = 'make:model' } - const loader = new CommandsList([MakeController, MakeModel]) + const loader = new ListLoader([MakeController, MakeModel]) assert.strictEqual(await loader.getCommand(MakeController.serialize()), MakeController) assert.strictEqual(await loader.getCommand(MakeModel.serialize()), MakeModel) }) @@ -35,7 +35,7 @@ test.group('Loaders | list', () => { static commandName = 'make:model' } - const loader = new CommandsList([MakeModel]) + const loader = new ListLoader([MakeModel]) assert.isNull(await loader.getCommand(MakeController.serialize())) }) @@ -48,7 +48,7 @@ test.group('Loaders | list', () => { static commandName = 'make:model' } - const loader = new CommandsList([MakeController, MakeModel]) + const loader = new ListLoader([MakeController, MakeModel]) assert.deepEqual(await loader.getMetaData(), [ MakeController.serialize(), MakeModel.serialize(), From ab8a2a1e5c6eb7f4e1e24974b9a9a08850310761 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 24 Jan 2023 20:47:11 +0530 Subject: [PATCH 006/112] feat: add FsLoader for loading commands from a directory --- index.ts | 1 + src/helpers.ts | 8 +- src/loaders/fs_loader.ts | 77 ++++++ src/loaders/modules_loader.ts | 4 +- tests/loaders/fs_loader.spec.ts | 219 ++++++++++++++++++ .../{list.spec.ts => list_loader.spec.ts} | 0 tests/loaders/modules_loader.spec.ts | 2 +- 7 files changed, 303 insertions(+), 8 deletions(-) create mode 100644 src/loaders/fs_loader.ts create mode 100644 tests/loaders/fs_loader.spec.ts rename tests/loaders/{list.spec.ts => list_loader.spec.ts} (100%) diff --git a/index.ts b/index.ts index e3f694e..705052f 100644 --- a/index.ts +++ b/index.ts @@ -15,5 +15,6 @@ export { flags } from './src/decorators/flags.js' export { BaseCommand } from './src/commands/base.js' export { HelpCommand } from './src/commands/help.js' export { ListCommand } from './src/commands/list.js' +export { FsLoader } from './src/loaders/fs_loader.js' export { ListLoader } from './src/loaders/list_loader.js' export { ModulesLoader } from './src/loaders/modules_loader.js' diff --git a/src/helpers.ts b/src/helpers.ts index 65561bb..576cc84 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -59,9 +59,7 @@ export function validCommandMetaData( ): asserts command is CommandMetaData { if (!command || (typeof command !== 'object' && typeof command !== 'function')) { throw new RuntimeException( - `Invalid command exported from "${exportPath}" method. Expected object, received "${inspect( - command - )}"` + `Invalid command exported from ${exportPath}. Expected object, received "${inspect(command)}"` ) } @@ -76,7 +74,7 @@ export function validCommandMetaData( expectedProperties.forEach((prop) => { if (prop in command === false) { throw new RuntimeException( - `Invalid command exported from "${exportPath}" method. Missing property "${prop}"` + `Invalid command exported from ${exportPath}. Missing property "${prop}"` ) } }) @@ -94,7 +92,7 @@ export function validateCommand( validCommandMetaData(command, exportPath) if (typeof command !== 'function' && !command.toString().startsWith('class ')) { throw new RuntimeException( - `Invalid command exported from "${exportPath}". Expected command to be a class` + `Invalid command exported from ${exportPath}. Expected command to be a class` ) } } diff --git a/src/loaders/fs_loader.ts b/src/loaders/fs_loader.ts new file mode 100644 index 0000000..e62071d --- /dev/null +++ b/src/loaders/fs_loader.ts @@ -0,0 +1,77 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { extname } from 'node:path' +import { fsImportAll } from '@poppinss/utils' + +import { validateCommand } from '../helpers.js' +import { BaseCommand } from '../commands/base.js' +import type { CommandMetaData, LoadersContract } from '../types.js' + +const JS_MODULES = ['.js', '.cjs', '.mjs'] + +export class FsLoader implements LoadersContract { + /** + * Absolute path to directory from which to load files + */ + #comandsDirectory: string + + /** + * An array of loaded commands + */ + #commands: (typeof BaseCommand)[] = [] + + constructor(comandsDirectory: string) { + this.#comandsDirectory = comandsDirectory + } + + /** + * Returns a collection of commands. The command value + * is unknown and must be validated + */ + async #loadCommands(): Promise> { + return fsImportAll(this.#comandsDirectory, { + filter: (filePath: string) => { + const ext = extname(filePath) + if (JS_MODULES.includes(ext)) { + return true + } + + if (ext === '.ts' && !filePath.endsWith('.d.ts')) { + return true + } + + return false + }, + }) + } + + /** + * Returns the metadata of commands + */ + async getMetaData(): Promise { + const commandsCollection = await this.#loadCommands() + + Object.keys(commandsCollection).forEach((key) => { + const command = commandsCollection[key] + validateCommand(command, `"${key}" file`) + this.#commands.push(command) + }) + + return this.#commands.map((command) => command.serialize()) + } + + /** + * Returns the command class constructor for a given command. Null + * is returned when unable to lookup the command + */ + async getCommand(metaData: CommandMetaData): Promise { + return this.#commands.find((command) => command.commandName === metaData.commandName) || null + } +} diff --git a/src/loaders/modules_loader.ts b/src/loaders/modules_loader.ts index 0e2dc95..9df5da4 100644 --- a/src/loaders/modules_loader.ts +++ b/src/loaders/modules_loader.ts @@ -105,7 +105,7 @@ export class ModulesLoader implements LoadersContract { const list = await this.#getCommandsList(loader, sourcePath) list.forEach((metaData) => { - validCommandMetaData(metaData, `${sourcePath}.list`) + validCommandMetaData(metaData, `"${sourcePath}.list" method`) commands.push(metaData) this.#commandsLoaders!.set(metaData.commandName, { loader, sourcePath }) }) @@ -144,7 +144,7 @@ export class ModulesLoader implements LoadersContract { return null } - validateCommand(command, `${commandLoader.sourcePath}.load`) + validateCommand(command, `"${commandLoader.sourcePath}.load" method`) return command } } diff --git a/tests/loaders/fs_loader.spec.ts b/tests/loaders/fs_loader.spec.ts new file mode 100644 index 0000000..9435107 --- /dev/null +++ b/tests/loaders/fs_loader.spec.ts @@ -0,0 +1,219 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import fs from 'fs-extra' +import { test } from '@japa/runner' +import { fileURLToPath } from 'node:url' +import { FsLoader } from '../../src/loaders/fs_loader.js' +import { join } from 'node:path' + +const BASE_URL = new URL('./tmp/', import.meta.url) +const BASE_PATH = fileURLToPath(BASE_URL) + +test.group('Loaders | fs', (group) => { + group.each.setup(() => { + return () => fs.remove(BASE_PATH) + }) + + test('raise error when commands directory does not exists', async ({ assert }) => { + const loader = new FsLoader(join(BASE_PATH, './commands')) + await assert.rejects(() => loader.getMetaData(), /ENOENT: no such file or directory/) + }) + + test('raise error when there is no default export in command file', async ({ assert }) => { + await fs.outputFile( + join(BASE_PATH, 'commands', 'make_controller_v_1.ts'), + ` + export class MakeController {} + ` + ) + + const loader = new FsLoader(join(BASE_PATH, './commands')) + await assert.rejects( + () => loader.getMetaData(), + 'Invalid command exported from "make_controller_v_1" file. Missing property "commandName"' + ) + }) + + test('return commands metadata', async ({ assert }) => { + await fs.outputFile( + join(BASE_PATH, 'commands', 'make_controller_v_2.ts'), + ` + export default class MakeController { + static commandName = 'make:controller' + static args = [] + static flags = [] + static aliases = [] + static options = {} + + static serialize() { + return { + commandName: this.commandName, + args: this.args, + flags: this.flags, + options: this.options, + aliases: this.aliases, + } + } + } + ` + ) + + const loader = new FsLoader(join(BASE_PATH, './commands')) + const commands = await loader.getMetaData() + assert.deepEqual(commands, [ + { + commandName: 'make:controller', + args: [], + flags: [], + options: {}, + aliases: [], + }, + ]) + }) + + test('load command from .js files', async ({ assert }) => { + await fs.outputFile( + join(BASE_PATH, 'commands', 'make_controller.js'), + ` + export default class MakeController { + static commandName = 'make:controller' + static args = [] + static flags = [] + static aliases = [] + static options = {} + + static serialize() { + return { + commandName: this.commandName, + args: this.args, + flags: this.flags, + options: this.options, + aliases: this.aliases, + } + } + } + ` + ) + + const loader = new FsLoader(join(BASE_PATH, './commands')) + const commands = await loader.getMetaData() + assert.deepEqual(commands, [ + { + commandName: 'make:controller', + args: [], + flags: [], + options: {}, + aliases: [], + }, + ]) + }) + + test('ignore .json files', async ({ assert }) => { + await fs.outputFile( + join(BASE_PATH, 'commands', 'make_controller_v_3.ts'), + ` + export default class MakeController { + static commandName = 'make:controller' + static args = [] + static flags = [] + static aliases = [] + static options = {} + + static serialize() { + return { + commandName: this.commandName, + args: this.args, + flags: this.flags, + options: this.options, + aliases: this.aliases, + } + } + } + ` + ) + + await fs.outputFile(join(BASE_PATH, 'commands', 'foo.json'), `{}`) + + const loader = new FsLoader(join(BASE_PATH, './commands')) + const commands = await loader.getMetaData() + assert.deepEqual(commands, [ + { + commandName: 'make:controller', + args: [], + flags: [], + options: {}, + aliases: [], + }, + ]) + }) + + test('get command constructor for a given command', async ({ assert }) => { + await fs.outputFile( + join(BASE_PATH, 'commands', 'make_controller_v_5.ts'), + ` + export default class MakeController { + static commandName = 'make:controller' + static args = [] + static flags = [] + static aliases = [] + static options = {} + + static serialize() { + return { + commandName: this.commandName, + args: this.args, + flags: this.flags, + options: this.options, + aliases: this.aliases, + } + } + } + ` + ) + + await fs.outputFile(join(BASE_PATH, 'commands', 'foo.json'), `{}`) + + const loader = new FsLoader(join(BASE_PATH, './commands')) + const commands = await loader.getMetaData() + const command = await loader.getCommand(commands[0]) + assert.isFunction(command) + }) + + test('return null when unable to lookup command', async ({ assert }) => { + await fs.outputFile( + join(BASE_PATH, 'commands', 'make_controller_v_6.ts'), + ` + export default class MakeController { + static commandName = 'make:controller' + static args = [] + static flags = [] + static aliases = [] + static options = {} + + static serialize() { + return { + commandName: this.commandName, + args: this.args, + flags: this.flags, + options: this.options, + aliases: this.aliases, + } + } + } + ` + ) + + await fs.outputFile(join(BASE_PATH, 'commands', 'foo.json'), `{}`) + + const loader = new FsLoader(join(BASE_PATH, './commands')) + const command = await loader.getCommand({ commandName: 'make:model' } as any) + assert.isNull(command) + }) +}) diff --git a/tests/loaders/list.spec.ts b/tests/loaders/list_loader.spec.ts similarity index 100% rename from tests/loaders/list.spec.ts rename to tests/loaders/list_loader.spec.ts diff --git a/tests/loaders/modules_loader.spec.ts b/tests/loaders/modules_loader.spec.ts index 59621fa..e80677d 100644 --- a/tests/loaders/modules_loader.spec.ts +++ b/tests/loaders/modules_loader.spec.ts @@ -251,7 +251,7 @@ test.group('Loaders | modules', (group) => { const loader = new ModulesLoader(BASE_URL, ['./loader_two.js?v=3']) await assert.rejects( () => loader.getCommand({ commandName: 'make:controller' } as any), - 'Invalid command exported from "./loader_two.js?v=3.load". Expected command to be a class' + 'Invalid command exported from "./loader_two.js?v=3.load" method. Expected command to be a class' ) }) From 217a1d9171c12a7f59058df9651086985e890da2 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 24 Jan 2023 20:48:06 +0530 Subject: [PATCH 007/112] test: normalize test import path to a file URL --- bin/test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bin/test.ts b/bin/test.ts index 5b398e1..ee6d2e9 100644 --- a/bin/test.ts +++ b/bin/test.ts @@ -1,4 +1,5 @@ import { assert } from '@japa/assert' +import { pathToFileURL } from 'node:url' import { expectTypeOf } from '@japa/expect-type' import { specReporter } from '@japa/spec-reporter' import { runFailedTests } from '@japa/run-failed-tests' @@ -23,7 +24,7 @@ configure({ files: ['tests/**/*.spec.ts'], plugins: [assert(), runFailedTests(), expectTypeOf()], reporters: [specReporter()], - importer: (filePath: string) => import(filePath), + importer: (filePath: string) => import(pathToFileURL(filePath).href), }, }) From 7fdef55de38a73f8cf036ce590e103f4612f31c4 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 24 Jan 2023 20:51:12 +0530 Subject: [PATCH 008/112] fix: casing of decorators directory --- src/{Decorators => decorators}/args.ts | 0 src/{Decorators => decorators}/flags.ts | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/{Decorators => decorators}/args.ts (100%) rename src/{Decorators => decorators}/flags.ts (100%) diff --git a/src/Decorators/args.ts b/src/decorators/args.ts similarity index 100% rename from src/Decorators/args.ts rename to src/decorators/args.ts diff --git a/src/Decorators/flags.ts b/src/decorators/flags.ts similarity index 100% rename from src/Decorators/flags.ts rename to src/decorators/flags.ts From 4e656703b868599f0559d07737028760b18f4b66 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 24 Jan 2023 21:34:59 +0530 Subject: [PATCH 009/112] feat: add support for prompts --- examples/main.ts | 2 +- package.json | 1 + src/commands/base.ts | 13 +++++++--- src/kernel.ts | 11 ++++++++- tests/base_command/exec.spec.ts | 38 +++++++++++++++++++++++++++++ tests/base_command/main.spec.ts | 24 ++++++++++++------ tests/kernel/exec.spec.ts | 4 +-- tests/kernel/flag_listeners.spec.ts | 5 ++-- tests/kernel/terminate.spec.ts | 7 +++++- 9 files changed, 88 insertions(+), 17 deletions(-) diff --git a/examples/main.ts b/examples/main.ts index 03f7714..1690d88 100644 --- a/examples/main.ts +++ b/examples/main.ts @@ -120,7 +120,7 @@ kernel.on('ansi', (_, $kernel, options) => { kernel.on('help', async (command, $kernel, options) => { options.args.unshift(command.commandName) - await new HelpCommand($kernel, options, kernel.ui).exec() + await new HelpCommand($kernel, options, kernel.ui, kernel.prompt).exec() return true }) diff --git a/package.json b/package.json index 4a8df41..238e486 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "dependencies": { "@poppinss/cliui": "^6.1.1-0", "@poppinss/hooks": "^7.1.1-0", + "@poppinss/prompts": "^3.1.0-0", "@poppinss/utils": "^6.4.0-0", "string-similarity": "^4.0.4", "string-width": "^5.1.2", diff --git a/src/commands/base.ts b/src/commands/base.ts index 45c0855..609f60c 100644 --- a/src/commands/base.ts +++ b/src/commands/base.ts @@ -9,6 +9,7 @@ import string from '@poppinss/utils/string' import lodash from '@poppinss/utils/lodash' +import type { Prompt } from '@poppinss/prompts' import { defineStaticProperty, InvalidArgumentsException } from '@poppinss/utils' import * as errors from '../errors.js' @@ -382,7 +383,8 @@ export class BaseCommand { this: T, kernel: Kernel, parsed: ParsedOutput, - ui: UIPrimitives + ui: UIPrimitives, + prompt: Prompt ): InstanceType { this.validate(parsed) @@ -390,7 +392,7 @@ export class BaseCommand { * Type casting is needed because of this issue * https://github.com/microsoft/TypeScript/issues/5863 */ - return new this(kernel, parsed, ui) as InstanceType + return new this(kernel, parsed, ui, prompt) as InstanceType } /** @@ -424,7 +426,12 @@ export class BaseCommand { return this.ui.colors } - constructor(protected kernel: Kernel, protected parsed: ParsedOutput, public ui: UIPrimitives) { + constructor( + protected kernel: Kernel, + protected parsed: ParsedOutput, + public ui: UIPrimitives, + public prompt: Prompt + ) { this.#consumeParsedOutput() } diff --git a/src/kernel.ts b/src/kernel.ts index 566f13f..80ee4e6 100644 --- a/src/kernel.ts +++ b/src/kernel.ts @@ -9,6 +9,7 @@ import Hooks from '@poppinss/hooks' import { cliui } from '@poppinss/cliui' +import { Prompt } from '@poppinss/prompts' import { findBestMatch } from 'string-similarity' import { RuntimeException } from '@poppinss/utils' @@ -91,7 +92,7 @@ export class Kernel { */ #executor: ExecutorContract = { create(command, parsedArgs, kernel) { - return new command(kernel, parsedArgs, kernel.ui) + return new command(kernel, parsedArgs, kernel.ui, kernel.prompt) }, run(command) { return command.exec() @@ -147,6 +148,14 @@ export class Kernel { */ ui: UIPrimitives = cliui() + /** + * Instance of prompt to display CLI prompts. We share + * a single instance with all the commands. This + * allows trapping prompts for commands executed + * internally. + */ + prompt = new Prompt() + /** * CLI info map */ diff --git a/tests/base_command/exec.spec.ts b/tests/base_command/exec.spec.ts index 97090d1..d370bd4 100644 --- a/tests/base_command/exec.spec.ts +++ b/tests/base_command/exec.spec.ts @@ -87,6 +87,44 @@ test.group('Base command | execute', () => { assert.deepEqual(model.stack, ['prepare', 'interact', 'run', 'completed']) assert.equal(model.result, 'completed') }) + + test('display prompts', async ({ assert }) => { + class MakeModel extends BaseCommand { + name!: string + connection!: string + + async interact() { + if (!this.name) { + this.name = await this.prompt.ask('Enter model name') + } + + if (!this.connection) { + this.connection = await this.prompt.choice('Select command connection', [ + 'sqlite', + 'mysql', + ]) + } + } + + async run() {} + } + + MakeModel.defineArgument('name', { type: 'string', required: false }) + MakeModel.defineFlag('connection', { type: 'string' }) + + const kernel = new Kernel() + kernel.ui = cliui({ mode: 'raw' }) + + const model = await kernel.create(MakeModel, []) + + model.prompt.trap('Enter model name').replyWith('user') + model.prompt.trap('Select command connection').chooseOption(0) + + await model.exec() + + assert.equal(model.name, 'user') + assert.equal(model.connection, 'sqlite') + }) }) test.group('Base command | execute | prepare fails', () => { diff --git a/tests/base_command/main.spec.ts b/tests/base_command/main.spec.ts index 9d8fb46..6ca7060 100644 --- a/tests/base_command/main.spec.ts +++ b/tests/base_command/main.spec.ts @@ -24,7 +24,12 @@ test.group('Base command', () => { MakeModel.boot() const kernel = new Kernel() - const model = new MakeModel(kernel, { _: [], args: [], unknownFlags: [], flags: {} }, cliui()) + const model = new MakeModel( + kernel, + { _: [], args: [], unknownFlags: [], flags: {} }, + cliui(), + kernel.prompt + ) assert.strictEqual(model.logger, model.ui.logger) }) @@ -37,7 +42,12 @@ test.group('Base command', () => { MakeModel.boot() const kernel = new Kernel() - const model = new MakeModel(kernel, { _: [], args: [], unknownFlags: [], flags: {} }, cliui()) + const model = new MakeModel( + kernel, + { _: [], args: [], unknownFlags: [], flags: {} }, + cliui(), + kernel.prompt + ) assert.strictEqual(model.colors, model.ui.colors) }) }) @@ -55,7 +65,7 @@ test.group('Base command | consume args', () => { const parsed = new Parser(MakeModel.getParserOptions()).parse('user --connection=sqlite') const kernel = new Kernel() - const model = MakeModel.create(kernel, parsed, cliui()) + const model = MakeModel.create(kernel, parsed, cliui(), kernel.prompt) assert.equal(model.name, 'user') assert.equal(model.connection, 'sqlite') @@ -73,7 +83,7 @@ test.group('Base command | consume args', () => { const parsed = new Parser(MakeModel.getParserOptions()).parse('user post --connection=sqlite') const kernel = new Kernel() - const model = MakeModel.create(kernel, parsed, cliui()) + const model = MakeModel.create(kernel, parsed, cliui(), kernel.prompt) assert.deepEqual(model.names, ['user', 'post']) assert.equal(model.connection, 'sqlite') @@ -97,7 +107,7 @@ test.group('Base command | consume flags', () => { ) const kernel = new Kernel() - const model = MakeModel.create(kernel, parsed, cliui()) + const model = MakeModel.create(kernel, parsed, cliui(), kernel.prompt) assert.equal(model.name, 'user') assert.equal(model.connection, 'sqlite') @@ -120,7 +130,7 @@ test.group('Base command | consume flags', () => { ) const kernel = new Kernel() - const model = MakeModel.create(kernel, parsed, cliui()) + const model = MakeModel.create(kernel, parsed, cliui(), kernel.prompt) assert.equal(model.name, 'user') assert.deepEqual(model.connections, ['sqlite', 'mysql']) @@ -141,7 +151,7 @@ test.group('Base command | consume flags', () => { const parsed = new Parser(MakeModel.getParserOptions()).parse('user') const kernel = new Kernel() - const model = MakeModel.create(kernel, parsed, cliui()) + const model = MakeModel.create(kernel, parsed, cliui(), kernel.prompt) assert.equal(model.name, 'user') assert.deepEqual(model.connections, ['sqlite']) diff --git a/tests/kernel/exec.spec.ts b/tests/kernel/exec.spec.ts index c21e458..4b3f5ff 100644 --- a/tests/kernel/exec.spec.ts +++ b/tests/kernel/exec.spec.ts @@ -198,7 +198,7 @@ test.group('Kernel | exec', () => { kernel.registerExecutor({ create(Command, parsed, self) { stack.push('creating') - return new Command(self, parsed, self.ui) + return new Command(self, parsed, self.ui, self.prompt) }, run(command) { stack.push('running') @@ -220,7 +220,7 @@ test.group('Kernel | exec', () => { () => kernel.registerExecutor({ create(Command, parsed, self) { - return new Command(self, parsed, self.ui) + return new Command(self, parsed, self.ui, self.prompt) }, run(command) { return command.exec() diff --git a/tests/kernel/flag_listeners.spec.ts b/tests/kernel/flag_listeners.spec.ts index d2a1c8b..9770da9 100644 --- a/tests/kernel/flag_listeners.spec.ts +++ b/tests/kernel/flag_listeners.spec.ts @@ -8,6 +8,7 @@ */ import { test } from '@japa/runner' +import { Prompt } from '@poppinss/prompts' import { Kernel } from '../../src/kernel.js' import { BaseCommand } from '../../src/commands/base.js' import { ListLoader } from '../../src/loaders/list_loader.js' @@ -73,8 +74,8 @@ test.group('Kernel | handle', (group) => { class MakeController extends BaseCommand { static commandName = 'make:controller' - constructor($kernel: Kernel, parsed: ParsedOutput, ui: UIPrimitives) { - super($kernel, parsed, ui) + constructor($kernel: Kernel, parsed: ParsedOutput, ui: UIPrimitives, prompt: Prompt) { + super($kernel, parsed, ui, prompt) stack.push('constructor') } diff --git a/tests/kernel/terminate.spec.ts b/tests/kernel/terminate.spec.ts index f83e14a..0600343 100644 --- a/tests/kernel/terminate.spec.ts +++ b/tests/kernel/terminate.spec.ts @@ -44,7 +44,12 @@ test.group('Kernel | terminate', (group) => { kernel.addLoader(new ListLoader([MakeController, MakeModel])) kernel.executed(async () => { await kernel.terminate( - new MakeModel(kernel, { args: [], _: [], flags: {}, unknownFlags: [] }, kernel.ui) + new MakeModel( + kernel, + { args: [], _: [], flags: {}, unknownFlags: [] }, + kernel.ui, + kernel.prompt + ) ) }) kernel.executed(async () => { From f7094e49e88ba42081e46ca509e8db23a10c1161 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 24 Jan 2023 22:16:54 +0530 Subject: [PATCH 010/112] feat: use json schema to validate commands loaded by fs and modules loader FS and modules loader relies on dynamically scanning commands from the filesystem and therefore we need good runtime validation for them --- command_metadata_schema.json | 174 ++++++++++++++++++++++++++ package.json | 8 +- src/helpers.ts | 37 +++--- tests/loaders/fs_loader.spec.ts | 49 ++++---- tests/loaders/modules_loader.spec.ts | 75 ++++++++++- toolkit/commands/generate_manifest.ts | 15 +++ toolkit/main.ts | 12 ++ tsconfig.json | 1 + 8 files changed, 324 insertions(+), 47 deletions(-) create mode 100644 command_metadata_schema.json create mode 100644 toolkit/commands/generate_manifest.ts create mode 100644 toolkit/main.ts diff --git a/command_metadata_schema.json b/command_metadata_schema.json new file mode 100644 index 0000000..fc4351e --- /dev/null +++ b/command_metadata_schema.json @@ -0,0 +1,174 @@ +{ + "$ref": "#/definitions/CommandMetaData", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "CommandMetaData": { + "additionalProperties": false, + "description": "Command metdata required to display command help.", + "properties": { + "aliases": { + "description": "Command aliases. The same command can be run using these aliases as well.", + "items": { + "type": "string" + }, + "type": "array" + }, + "args": { + "description": "Args accepted by the command", + "items": { + "additionalProperties": false, + "properties": { + "allowEmptyValue": { + "description": "Whether or not to allow empty values. When set to false, the validation will fail if the argument is provided an empty string\n\nDefaults to false", + "type": "boolean" + }, + "argumentName": { + "type": "string" + }, + "default": {}, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "type": { + "enum": [ + "string", + "spread" + ], + "type": "string" + } + }, + "required": [ + "name", + "argumentName", + "type" + ], + "type": "object" + }, + "type": "array" + }, + "commandName": { + "description": "The name of the command", + "type": "string" + }, + "description": { + "description": "The command description to show on the help screen", + "type": "string" + }, + "flags": { + "description": "Flags accepted by the command", + "items": { + "additionalProperties": false, + "properties": { + "alias": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ] + }, + "allowEmptyValue": { + "description": "Whether or not to allow empty values. When set to false, the validation will fail if the flag is mentioned but no value is provided\n\nDefaults to false", + "type": "boolean" + }, + "default": {}, + "description": { + "type": "string" + }, + "flagName": { + "type": "string" + }, + "name": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "showNegatedVariantInHelp": { + "description": "Whether or not to display the negated variant in the help output.\n\nApplicable for boolean flags only\n\nDefaults to false", + "type": "boolean" + }, + "type": { + "enum": [ + "string", + "boolean", + "number", + "array" + ], + "type": "string" + } + }, + "required": [ + "name", + "flagName", + "type" + ], + "type": "object" + }, + "type": "array" + }, + "help": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ], + "description": "Help text for the command" + }, + "namespace": { + "description": "Command namespace. The namespace is extracted from the command name", + "type": [ + "string", + "null" + ] + }, + "options": { + "$ref": "#/definitions/CommandOptions", + "description": "Command configuration options" + } + }, + "required": [ + "commandName", + "description", + "namespace", + "aliases", + "flags", + "args", + "options" + ], + "type": "object" + }, + "CommandOptions": { + "additionalProperties": false, + "description": "Static set of command options", + "properties": { + "allowUnknownFlags": { + "description": "Whether or not to allow for unknown flags. If set to false, the command will not run when unknown flags are provided through the CLI\n\nDefaults to false", + "type": "boolean" + }, + "staysAlive": { + "description": "When flag set to true, the kernel will not trigger the termination process unless the command explicitly calls the terminate method.\n\nDefaults to false", + "type": "boolean" + } + }, + "type": "object" + } + } +} \ No newline at end of file diff --git a/package.json b/package.json index 238e486..aa77369 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "type": "module", "files": [ "build/src", + "build/command_metadata_schema.json", "build/index.d.ts", "build/index.js" ], @@ -17,7 +18,9 @@ "pretest": "npm run lint", "test": "cross-env NODE_DEBUG=adonisjs:ace c8 npm run vscode:test", "clean": "del-cli build", - "compile": "npm run lint && npm run clean && tsc", + "build:schema": "ts-json-schema-generator --path='src/types.ts' --type='CommandMetaData' --tsconfig='tsconfig.json' --out='command_metadata_schema.json'", + "copy:files": "copyfiles command_metadata_schema.json build", + "compile": "npm run lint && npm run clean && tsc && npm run build:schema && npm run copy:files", "build": "npm run compile", "release": "np", "version": "npm run build", @@ -40,6 +43,7 @@ "@poppinss/hooks": "^7.1.1-0", "@poppinss/prompts": "^3.1.0-0", "@poppinss/utils": "^6.4.0-0", + "jsonschema": "^1.4.1", "string-similarity": "^4.0.4", "string-width": "^5.1.2", "yargs-parser": "^21.1.1" @@ -59,6 +63,7 @@ "@types/sinon": "^10.0.13", "@types/string-similarity": "^4.0.0", "c8": "^7.12.0", + "copyfiles": "^2.4.1", "cross-env": "^7.0.3", "del-cli": "^5.0.0", "eslint": "^8.32.0", @@ -72,6 +77,7 @@ "prettier": "^2.8.3", "reflect-metadata": "^0.1.13", "sinon": "^15.0.1", + "ts-json-schema-generator": "^1.2.0", "ts-node": "^10.9.1", "typescript": "^4.8.2" }, diff --git a/src/helpers.ts b/src/helpers.ts index 576cc84..76c3c0a 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -8,9 +8,12 @@ */ import { inspect } from 'node:util' +import { Validator } from 'jsonschema' import { RuntimeException } from '@poppinss/utils' -import type { CommandMetaData, UIPrimitives } from './types.js' + import { BaseCommand } from '../index.js' +import type { CommandMetaData, UIPrimitives } from './types.js' +import schema from '../command_metadata_schema.json' assert { type: 'json' } /** * Helper to sort array of strings alphabetically. @@ -63,21 +66,11 @@ export function validCommandMetaData( ) } - const expectedProperties: (keyof CommandMetaData)[] = [ - 'commandName', - 'aliases', - 'flags', - 'args', - 'options', - ] - - expectedProperties.forEach((prop) => { - if (prop in command === false) { - throw new RuntimeException( - `Invalid command exported from ${exportPath}. Missing property "${prop}"` - ) - } - }) + try { + new Validator().validate(command, schema, { throwError: true }) + } catch (error) { + throw new RuntimeException(`Invalid command exported from ${exportPath}. ${error.message}`) + } } /** @@ -89,10 +82,18 @@ export function validateCommand( command: unknown, exportPath: string ): asserts command is typeof BaseCommand { - validCommandMetaData(command, exportPath) - if (typeof command !== 'function' && !command.toString().startsWith('class ')) { + if (typeof command !== 'function' || !command.toString().startsWith('class ')) { throw new RuntimeException( `Invalid command exported from ${exportPath}. Expected command to be a class` ) } + + const commandConstructor = command as Function & { serialize: () => unknown } + if (typeof commandConstructor.serialize !== 'function') { + throw new RuntimeException( + `Invalid command exported from ${exportPath}. Expected command to extend the "BaseCommand"` + ) + } + + validCommandMetaData(commandConstructor.serialize(), exportPath) } diff --git a/tests/loaders/fs_loader.spec.ts b/tests/loaders/fs_loader.spec.ts index 9435107..f7237d1 100644 --- a/tests/loaders/fs_loader.spec.ts +++ b/tests/loaders/fs_loader.spec.ts @@ -37,7 +37,7 @@ test.group('Loaders | fs', (group) => { const loader = new FsLoader(join(BASE_PATH, './commands')) await assert.rejects( () => loader.getMetaData(), - 'Invalid command exported from "make_controller_v_1" file. Missing property "commandName"' + 'Invalid command exported from "make_controller_v_1" file. Expected command to be a class' ) }) @@ -51,10 +51,14 @@ test.group('Loaders | fs', (group) => { static flags = [] static aliases = [] static options = {} + static description = '' + static namespace = 'make' static serialize() { return { commandName: this.commandName, + description: this.description, + namespace: this.namespace, args: this.args, flags: this.flags, options: this.options, @@ -70,6 +74,8 @@ test.group('Loaders | fs', (group) => { assert.deepEqual(commands, [ { commandName: 'make:controller', + description: '', + namespace: 'make', args: [], flags: [], options: {}, @@ -88,10 +94,14 @@ test.group('Loaders | fs', (group) => { static flags = [] static aliases = [] static options = {} + static description = '' + static namespace = 'make' static serialize() { return { commandName: this.commandName, + description: this.description, + namespace: this.namespace, args: this.args, flags: this.flags, options: this.options, @@ -107,6 +117,8 @@ test.group('Loaders | fs', (group) => { assert.deepEqual(commands, [ { commandName: 'make:controller', + description: '', + namespace: 'make', args: [], flags: [], options: {}, @@ -125,10 +137,14 @@ test.group('Loaders | fs', (group) => { static flags = [] static aliases = [] static options = {} + static description = '' + static namespace = 'make' static serialize() { return { commandName: this.commandName, + description: this.description, + namespace: this.namespace, args: this.args, flags: this.flags, options: this.options, @@ -146,6 +162,8 @@ test.group('Loaders | fs', (group) => { assert.deepEqual(commands, [ { commandName: 'make:controller', + description: '', + namespace: 'make', args: [], flags: [], options: {}, @@ -164,10 +182,14 @@ test.group('Loaders | fs', (group) => { static flags = [] static aliases = [] static options = {} + static description = '' + static namespace = 'make' static serialize() { return { commandName: this.commandName, + description: this.description, + namespace: this.namespace, args: this.args, flags: this.flags, options: this.options, @@ -187,31 +209,6 @@ test.group('Loaders | fs', (group) => { }) test('return null when unable to lookup command', async ({ assert }) => { - await fs.outputFile( - join(BASE_PATH, 'commands', 'make_controller_v_6.ts'), - ` - export default class MakeController { - static commandName = 'make:controller' - static args = [] - static flags = [] - static aliases = [] - static options = {} - - static serialize() { - return { - commandName: this.commandName, - args: this.args, - flags: this.flags, - options: this.options, - aliases: this.aliases, - } - } - } - ` - ) - - await fs.outputFile(join(BASE_PATH, 'commands', 'foo.json'), `{}`) - const loader = new FsLoader(join(BASE_PATH, './commands')) const command = await loader.getCommand({ commandName: 'make:model' } as any) assert.isNull(command) diff --git a/tests/loaders/modules_loader.spec.ts b/tests/loaders/modules_loader.spec.ts index e80677d..5d07852 100644 --- a/tests/loaders/modules_loader.spec.ts +++ b/tests/loaders/modules_loader.spec.ts @@ -103,7 +103,7 @@ test.group('Loaders | modules', (group) => { const loader = new ModulesLoader(BASE_URL, ['./loader_one.js?v=4']) await assert.rejects( () => loader.getMetaData(), - `Invalid command exported from "./loader_one.js?v=4.list" method. Missing property "commandName"` + `Invalid command exported from "./loader_one.js?v=4.list" method. requires property "commandName"` ) }) @@ -115,6 +115,8 @@ test.group('Loaders | modules', (group) => { return [ { commandName: 'make:controller', + description: '', + namespace: 'make', args: [], flags: [], aliases: [], @@ -133,6 +135,8 @@ test.group('Loaders | modules', (group) => { return [ { commandName: 'make:model', + description: '', + namespace: 'make', args: [], flags: [], aliases: [], @@ -150,6 +154,8 @@ test.group('Loaders | modules', (group) => { assert.deepEqual(commands, [ { commandName: 'make:controller', + description: '', + namespace: 'make', args: [], flags: [], aliases: [], @@ -157,6 +163,8 @@ test.group('Loaders | modules', (group) => { }, { commandName: 'make:model', + description: '', + namespace: 'make', args: [], flags: [], aliases: [], @@ -173,6 +181,8 @@ test.group('Loaders | modules', (group) => { return [ { commandName: 'make:controller', + description: '', + namespace: 'make', args: [], flags: [], aliases: [], @@ -203,6 +213,8 @@ test.group('Loaders | modules', (group) => { return [ { commandName: 'make:controller', + description: '', + namespace: null, args: [], flags: [], aliases: [], @@ -229,6 +241,8 @@ test.group('Loaders | modules', (group) => { return [ { commandName: 'make:controller', + description: '', + namespace: 'make', args: [], flags: [], aliases: [], @@ -236,9 +250,12 @@ test.group('Loaders | modules', (group) => { } ] } + export async function load() { return { commandName: 'make:controller', + description: '', + namespace: 'make', args: [], flags: [], aliases: [], @@ -255,7 +272,7 @@ test.group('Loaders | modules', (group) => { ) }) - test('get command constructor returned by the load method', async ({ assert }) => { + test('raise error when command class does not have serialize method', async ({ assert }) => { await fs.outputFile( join(BASE_PATH, './loader_two.js'), ` @@ -263,6 +280,8 @@ test.group('Loaders | modules', (group) => { return [ { commandName: 'make:controller', + description: '', + namespace: 'make', args: [], flags: [], aliases: [], @@ -277,12 +296,64 @@ test.group('Loaders | modules', (group) => { static flags = [] static aliases = [] static options = {} + static description = '' + static namespace = 'make' } } ` ) const loader = new ModulesLoader(BASE_URL, ['./loader_two.js?v=4']) + await assert.rejects( + () => loader.getCommand({ commandName: 'make:controller' } as any), + 'Invalid command exported from "./loader_two.js?v=4.load" method. Expected command to extend the "BaseCommand"' + ) + }) + + test('get command constructor returned by the load method', async ({ assert }) => { + await fs.outputFile( + join(BASE_PATH, './loader_two.js'), + ` + export async function list() { + return [ + { + commandName: 'make:controller', + description: '', + namespace: 'make', + args: [], + flags: [], + aliases: [], + options: {}, + } + ] + } + export async function load() { + return class Command { + static commandName = 'make:controller' + static args = [] + static flags = [] + static aliases = [] + static options = {} + static description = '' + static namespace = 'make' + + static serialize() { + return { + commandName: this.commandName, + args: this.args, + flags: this.flags, + aliases: this.aliases, + options: this.options, + description: this.description, + namespace: this.namespace, + } + } + } + } + ` + ) + + const loader = new ModulesLoader(BASE_URL, ['./loader_two.js?v=5']) const command = await loader.getCommand({ commandName: 'make:controller' } as any) assert.isFunction(command) }) diff --git a/toolkit/commands/generate_manifest.ts b/toolkit/commands/generate_manifest.ts new file mode 100644 index 0000000..93e2db1 --- /dev/null +++ b/toolkit/commands/generate_manifest.ts @@ -0,0 +1,15 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { BaseCommand } from '../../index.js' + +export class GenerateManifestCommand extends BaseCommand { + static commandName: string = 'generate:manifest' + static description: string = 'Generate a manifest JSO' +} diff --git a/toolkit/main.ts b/toolkit/main.ts new file mode 100644 index 0000000..33d66f0 --- /dev/null +++ b/toolkit/main.ts @@ -0,0 +1,12 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Kernel } from '../index.js' + +new Kernel() diff --git a/tsconfig.json b/tsconfig.json index a09cb7d..f2b7dda 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,7 @@ "module": "NodeNext", "lib": ["ESNext"], "useDefineForClassFields": true, + "resolveJsonModule": true, "noUnusedLocals": true, "noUnusedParameters": true, "isolatedModules": true, From 471f25feb90c43c5d8fe93d09f45a21071975480 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 25 Jan 2023 10:48:57 +0530 Subject: [PATCH 011/112] feat: make kernel accept any sort of base command --- examples/main.ts | 2 +- package.json | 3 +- src/commands/base.ts | 23 +- src/commands/help.ts | 4 +- src/kernel.ts | 92 ++-- src/types.ts | 19 +- tests/base_command/exec.spec.ts | 22 +- tests/base_command/main.spec.ts | 53 +-- tests/commands/help.spec.ts | 429 ++++++++++++++++++ tests/commands/list.spec.ts | 23 +- tests/kernel/boot.spec.ts | 8 +- tests/kernel/default_command.spec.ts | 18 +- tests/kernel/exec.spec.ts | 43 +- tests/kernel/find.spec.ts | 14 +- tests/kernel/flag_listeners.spec.ts | 23 +- tests/kernel/gloal_flags.spec.ts | 6 +- tests/kernel/handle.spec.ts | 22 +- tests/kernel/loaders.spec.ts | 6 +- tests/kernel/main.spec.ts | 24 +- tests/kernel/terminate.spec.ts | 12 +- toolkit/commands/generate_manifest.ts | 15 - toolkit/commands/index_command/main.ts | 26 ++ .../index_command/stubs/module_loader.stub | 18 + toolkit/main.ts | 3 +- 24 files changed, 652 insertions(+), 256 deletions(-) create mode 100644 tests/commands/help.spec.ts delete mode 100644 toolkit/commands/generate_manifest.ts create mode 100644 toolkit/commands/index_command/main.ts create mode 100644 toolkit/commands/index_command/stubs/module_loader.stub diff --git a/examples/main.ts b/examples/main.ts index 1690d88..6d061b6 100644 --- a/examples/main.ts +++ b/examples/main.ts @@ -14,7 +14,7 @@ import { BaseCommand } from '../src/commands/base.js' import { ListLoader } from '../src/loaders/list_loader.js' import { HelpCommand } from '../src/commands/help.js' -const kernel = new Kernel() +const kernel = Kernel.create() class Configure extends BaseCommand { static commandName: string = 'configure' diff --git a/package.json b/package.json index aa77369..a0ba5bb 100644 --- a/package.json +++ b/package.json @@ -141,7 +141,8 @@ ], "exclude": [ "tests/**", - "build/**" + "build/**", + "examples/**" ] } } diff --git a/src/commands/base.ts b/src/commands/base.ts index 609f60c..1e3a6be 100644 --- a/src/commands/base.ts +++ b/src/commands/base.ts @@ -374,27 +374,6 @@ export class BaseCommand { }) } - /** - * Create the command instance by validating the parsed input. It is - * recommended to use this method over create a new instance - * directly. - */ - static create( - this: T, - kernel: Kernel, - parsed: ParsedOutput, - ui: UIPrimitives, - prompt: Prompt - ): InstanceType { - this.validate(parsed) - - /** - * Type casting is needed because of this issue - * https://github.com/microsoft/TypeScript/issues/5863 - */ - return new this(kernel, parsed, ui, prompt) as InstanceType - } - /** * The exit code for the command */ @@ -427,7 +406,7 @@ export class BaseCommand { } constructor( - protected kernel: Kernel, + protected kernel: Kernel, protected parsed: ParsedOutput, public ui: UIPrimitives, public prompt: Prompt diff --git a/src/commands/help.ts b/src/commands/help.ts index 67c621f..6c4c3e3 100644 --- a/src/commands/help.ts +++ b/src/commands/help.ts @@ -86,8 +86,8 @@ export class HelpCommand extends BaseCommand { if (!command) { renderErrorWithSuggestions( this.ui, - `Command "${this.colors}" is not defined`, - this.kernel.getNamespaceSuggestions(this.commandName) + `Command "${this.commandName}" is not defined`, + this.kernel.getCommandSuggestions(this.commandName) ) return false } diff --git a/src/kernel.ts b/src/kernel.ts index 80ee4e6..176bb1a 100644 --- a/src/kernel.ts +++ b/src/kernel.ts @@ -51,12 +51,39 @@ const knowErrorCodes = Object.keys(errors) * The kernel is the main entry point of a console application, and * is tailored for a standard CLI environment. */ -export class Kernel { +export class Kernel { + /** + * The default executor for creating command's instance + * and running them + */ + static commandExecutor: ExecutorContract = { + create(command, parsedArgs, kernel) { + return new command(kernel, parsedArgs, kernel.ui, kernel.prompt) + }, + run(command) { + return command.exec() + }, + } + + /** + * The default command to use when creating kernel instance + * via "static create" method. + */ + static defaultCommand: typeof BaseCommand = ListCommand + + /** + * Creates an instance of kernel with the default executor + * and default command + */ + static create() { + return new Kernel(this.defaultCommand, this.commandExecutor) + } + /** * Listeners for CLI options. Executed for the main command * only */ - #optionListeners: Map = new Map() + #optionListeners: Map> = new Map() /** * The global command is used to register global flags applicable @@ -72,7 +99,7 @@ export class Kernel { * The default command to run when no command is mentioned. The default * command will also run when only flags are mentioned. */ - #defaultCommand: typeof BaseCommand = ListCommand + #defaultCommand: Command /** * Available hooks @@ -90,20 +117,13 @@ export class Kernel { * Executors are used to instantiate a command and execute * the run method. */ - #executor: ExecutorContract = { - create(command, parsedArgs, kernel) { - return new command(kernel, parsedArgs, kernel.ui, kernel.prompt) - }, - run(command) { - return command.exec() - }, - } + #executor: ExecutorContract /** * Keeping track of the main command. There are some action (like termination) * that only the main command can perform */ - #mainCommand?: BaseCommand + #mainCommand?: InstanceType /** * The current state of kernel. The `running` and `terminated` @@ -168,14 +188,16 @@ export class Kernel { return this.#globalCommand.flags } + constructor(defaultCommand: Command, executor: ExecutorContract) { + this.#defaultCommand = defaultCommand + this.#executor = executor + } + /** * Creates an instance of a command by parsing and validating * the command line arguments. */ - async #create( - Command: T, - argv: string[] - ): Promise> { + async #create(Command: T, argv: string | string[]): Promise> { /** * Parse CLI argv without global flags. When running commands directly, we * should not be using global flags anyways @@ -198,10 +220,7 @@ export class Kernel { * Executes a given command. The main commands are executed using the * "execMain" method. */ - async #exec( - commandName: string, - argv: string[] - ): Promise> { + async #exec(commandName: string, argv: string[]): Promise> { const Command = await this.find(commandName) const commandInstance = await this.#create(Command, argv) @@ -328,7 +347,7 @@ export class Kernel { * * The callbacks are only executed for the main command */ - on(option: string, callback: FlagListener): this { + on(option: string, callback: FlagListener): this { debug('registering flag listener for "%s" flag', option) this.#optionListeners.set(option, callback) return this @@ -349,31 +368,6 @@ export class Kernel { this.#globalCommand.defineFlag(name, options) } - /** - * Register a custom default command. Default command runs - * when no command is mentioned - */ - registerDefaultCommand(command: typeof BaseCommand): this { - if (this.#state !== 'idle') { - throw new RuntimeException(`Cannot register default command in "${this.#state}" state`) - } - - this.#defaultCommand = command - return this - } - - /** - * Register a custom executor to execute the command - */ - registerExecutor(executor: ExecutorContract): this { - if (this.#state !== 'idle') { - throw new RuntimeException(`Cannot register commands executor in "${this.#state}" state`) - } - - this.#executor = executor - return this - } - /** * Register a commands loader. The commands will be collected by * all the loaders. @@ -623,7 +617,7 @@ export class Kernel { /** * Find a command by its name */ - async find(commandName: string): Promise { + async find(commandName: string): Promise { /** * Get command name from the alias (if one exists) */ @@ -656,7 +650,7 @@ export class Kernel { * Execute a command. The second argument is an array of commandline * arguments (without the command name) */ - async exec(commandName: string, argv: string[]) { + async exec(commandName: string, argv: string[]) { /** * Boot if not already booted */ @@ -681,7 +675,7 @@ export class Kernel { * Creates a command instance by parsing and validating * the command-line arguments. */ - async create(command: T, argv: string[]): Promise> { + async create(command: T, argv: string | string[]): Promise> { /** * Boot if not already booted */ diff --git a/src/types.ts b/src/types.ts index 4f362f5..e73942b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -72,20 +72,23 @@ export interface LoadersContract { * Command executor is used to create a new instance of the command * and run it. */ -export interface ExecutorContract { +export interface ExecutorContract { /** * Create a new instance of the command */ create( - command: typeof BaseCommand, + command: Command, parsedOutput: ParsedOutput, - kernel: Kernel - ): Promise | BaseCommand + kernel: Kernel + ): Promise> | InstanceType /** * Run the command */ - run(command: Command, kernel: Kernel): Promise + run>( + command: Instance, + kernel: Kernel + ): Promise } /** @@ -350,9 +353,9 @@ export type TerminatingHookHandler = HookHandler = ( + command: Command, + kernel: Kernel, parsedOutput: ParsedOutput ) => any | Promise diff --git a/tests/base_command/exec.spec.ts b/tests/base_command/exec.spec.ts index d370bd4..5d69175 100644 --- a/tests/base_command/exec.spec.ts +++ b/tests/base_command/exec.spec.ts @@ -45,7 +45,7 @@ test.group('Base command | execute', () => { MakeModel.defineArgument('name', { type: 'string' }) MakeModel.defineFlag('connection', { type: 'string' }) - const kernel = new Kernel() + const kernel = Kernel.create() const model = await kernel.create(MakeModel, ['user', '--connection=sqlite']) await model.exec() @@ -79,7 +79,7 @@ test.group('Base command | execute', () => { MakeModel.defineArgument('name', { type: 'string' }) MakeModel.defineFlag('connection', { type: 'string' }) - const kernel = new Kernel() + const kernel = Kernel.create() kernel.ui = cliui({ mode: 'raw' }) const model = await kernel.create(MakeModel, ['user', '--connection=sqlite']) @@ -112,7 +112,7 @@ test.group('Base command | execute', () => { MakeModel.defineArgument('name', { type: 'string', required: false }) MakeModel.defineFlag('connection', { type: 'string' }) - const kernel = new Kernel() + const kernel = Kernel.create() kernel.ui = cliui({ mode: 'raw' }) const model = await kernel.create(MakeModel, []) @@ -146,7 +146,7 @@ test.group('Base command | execute | prepare fails', () => { MakeModel.defineArgument('name', { type: 'string' }) MakeModel.defineFlag('connection', { type: 'string' }) - const kernel = new Kernel() + const kernel = Kernel.create() kernel.ui = cliui({ mode: 'raw' }) const model = await kernel.create(MakeModel, ['user', '--connection=sqlite']) @@ -185,7 +185,7 @@ test.group('Base command | execute | prepare fails', () => { MakeModel.defineArgument('name', { type: 'string' }) MakeModel.defineFlag('connection', { type: 'string' }) - const kernel = new Kernel() + const kernel = Kernel.create() kernel.ui = cliui({ mode: 'raw' }) const model = await kernel.create(MakeModel, ['user', '--connection=sqlite']) @@ -213,7 +213,7 @@ test.group('Base command | execute | intertact fails', () => { MakeModel.defineArgument('name', { type: 'string' }) MakeModel.defineFlag('connection', { type: 'string' }) - const kernel = new Kernel() + const kernel = Kernel.create() kernel.ui = cliui({ mode: 'raw' }) const model = await kernel.create(MakeModel, ['user', '--connection=sqlite']) @@ -248,7 +248,7 @@ test.group('Base command | execute | intertact fails', () => { MakeModel.defineArgument('name', { type: 'string' }) MakeModel.defineFlag('connection', { type: 'string' }) - const kernel = new Kernel() + const kernel = Kernel.create() kernel.ui = cliui({ mode: 'raw' }) const model = await kernel.create(MakeModel, ['user', '--connection=sqlite']) @@ -272,7 +272,7 @@ test.group('Base command | execute | run fails', () => { MakeModel.defineArgument('name', { type: 'string' }) MakeModel.defineFlag('connection', { type: 'string' }) - const kernel = new Kernel() + const kernel = Kernel.create() kernel.ui = cliui({ mode: 'raw' }) const model = await kernel.create(MakeModel, ['user', '--connection=sqlite']) @@ -302,7 +302,7 @@ test.group('Base command | execute | run fails', () => { MakeModel.defineArgument('name', { type: 'string' }) MakeModel.defineFlag('connection', { type: 'string' }) - const kernel = new Kernel() + const kernel = Kernel.create() kernel.ui = cliui({ mode: 'raw' }) const model = await kernel.create(MakeModel, ['user', '--connection=sqlite']) @@ -329,7 +329,7 @@ test.group('Base command | execute | complete method', () => { MakeModel.defineArgument('name', { type: 'string' }) MakeModel.defineFlag('connection', { type: 'string' }) - const kernel = new Kernel() + const kernel = Kernel.create() kernel.ui = cliui({ mode: 'raw' }) const model = await kernel.create(MakeModel, ['user', '--connection=sqlite']) @@ -343,7 +343,7 @@ test.group('Base command | terminate', () => { class MakeModel extends BaseCommand {} MakeModel.boot() - const kernel = new Kernel() + const kernel = Kernel.create() kernel.ui = cliui({ mode: 'raw' }) const model = await kernel.create(MakeModel, []) diff --git a/tests/base_command/main.spec.ts b/tests/base_command/main.spec.ts index 6ca7060..99e25d9 100644 --- a/tests/base_command/main.spec.ts +++ b/tests/base_command/main.spec.ts @@ -10,7 +10,6 @@ import { test } from '@japa/runner' import { cliui } from '@poppinss/cliui' -import { Parser } from '../../src/parser.js' import { Kernel } from '../../src/kernel.js' import { BaseCommand } from '../../src/commands/base.js' @@ -23,7 +22,7 @@ test.group('Base command', () => { MakeModel.boot() - const kernel = new Kernel() + const kernel = Kernel.create() const model = new MakeModel( kernel, { _: [], args: [], unknownFlags: [], flags: {} }, @@ -41,7 +40,7 @@ test.group('Base command', () => { MakeModel.boot() - const kernel = new Kernel() + const kernel = Kernel.create() const model = new MakeModel( kernel, { _: [], args: [], unknownFlags: [], flags: {} }, @@ -53,7 +52,7 @@ test.group('Base command', () => { }) test.group('Base command | consume args', () => { - test('consume parsed output to set command properties', ({ assert }) => { + test('consume parsed output to set command properties', async ({ assert }) => { class MakeModel extends BaseCommand { name!: string connection!: string @@ -62,16 +61,14 @@ test.group('Base command | consume args', () => { MakeModel.defineArgument('name', { type: 'string' }) MakeModel.defineFlag('connection', { type: 'string' }) - const parsed = new Parser(MakeModel.getParserOptions()).parse('user --connection=sqlite') - - const kernel = new Kernel() - const model = MakeModel.create(kernel, parsed, cliui(), kernel.prompt) + const kernel = Kernel.create() + const model = await kernel.create(MakeModel, ['user', '--connection=sqlite']) assert.equal(model.name, 'user') assert.equal(model.connection, 'sqlite') }) - test('consume spread arg', ({ assert }) => { + test('consume spread arg', async ({ assert }) => { class MakeModel extends BaseCommand { names!: string[] connection!: string @@ -80,10 +77,8 @@ test.group('Base command | consume args', () => { MakeModel.defineArgument('names', { type: 'spread' }) MakeModel.defineFlag('connection', { type: 'string' }) - const parsed = new Parser(MakeModel.getParserOptions()).parse('user post --connection=sqlite') - - const kernel = new Kernel() - const model = MakeModel.create(kernel, parsed, cliui(), kernel.prompt) + const kernel = Kernel.create() + const model = await kernel.create(MakeModel, ['user', 'post', '--connection=sqlite']) assert.deepEqual(model.names, ['user', 'post']) assert.equal(model.connection, 'sqlite') @@ -91,7 +86,7 @@ test.group('Base command | consume args', () => { }) test.group('Base command | consume flags', () => { - test('consume boolean flag', ({ assert }) => { + test('consume boolean flag', async ({ assert }) => { class MakeModel extends BaseCommand { name!: string connection!: string @@ -102,19 +97,15 @@ test.group('Base command | consume flags', () => { MakeModel.defineFlag('connection', { type: 'string' }) MakeModel.defineFlag('dropAll', { type: 'boolean', default: true }) - const parsed = new Parser(MakeModel.getParserOptions()).parse( - 'user --connection=sqlite --drop-all' - ) - - const kernel = new Kernel() - const model = MakeModel.create(kernel, parsed, cliui(), kernel.prompt) + const kernel = Kernel.create() + const model = await kernel.create(MakeModel, ['user', '--connection=sqlite', '--drop-all']) assert.equal(model.name, 'user') assert.equal(model.connection, 'sqlite') assert.isTrue(model.dropAll) }) - test('consume array flag', ({ assert }) => { + test('consume array flag', async ({ assert }) => { class MakeModel extends BaseCommand { name!: string connections!: string[] @@ -125,19 +116,19 @@ test.group('Base command | consume flags', () => { MakeModel.defineFlag('connections', { type: 'array' }) MakeModel.defineFlag('dropAll', { type: 'boolean' }) - const parsed = new Parser(MakeModel.getParserOptions()).parse( - 'user --connections=sqlite --connections=mysql' - ) - - const kernel = new Kernel() - const model = MakeModel.create(kernel, parsed, cliui(), kernel.prompt) + const kernel = Kernel.create() + const model = await kernel.create(MakeModel, [ + 'user', + '--connections=sqlite', + '--connections=mysql', + ]) assert.equal(model.name, 'user') assert.deepEqual(model.connections, ['sqlite', 'mysql']) assert.isUndefined(model.dropAll) }) - test('use default value when array flag is missing', ({ assert }) => { + test('use default value when array flag is missing', async ({ assert }) => { class MakeModel extends BaseCommand { name!: string connections!: string[] @@ -148,10 +139,8 @@ test.group('Base command | consume flags', () => { MakeModel.defineFlag('connections', { type: 'array', default: ['sqlite'] }) MakeModel.defineFlag('dropAll', { type: 'boolean' }) - const parsed = new Parser(MakeModel.getParserOptions()).parse('user') - - const kernel = new Kernel() - const model = MakeModel.create(kernel, parsed, cliui(), kernel.prompt) + const kernel = Kernel.create() + const model = await kernel.create(MakeModel, ['user']) assert.equal(model.name, 'user') assert.deepEqual(model.connections, ['sqlite']) diff --git a/tests/commands/help.spec.ts b/tests/commands/help.spec.ts new file mode 100644 index 0000000..b6ec44f --- /dev/null +++ b/tests/commands/help.spec.ts @@ -0,0 +1,429 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { Kernel } from '../../src/kernel.js' +import { HelpCommand } from '../../index.js' +import { args } from '../../src/decorators/args.js' +import { flags } from '../../src/decorators/flags.js' +import { BaseCommand } from '../../src/commands/base.js' +import { ListLoader } from '../../src/loaders/list_loader.js' + +test.group('Help command', () => { + test('show help for a registered command', async ({ assert }) => { + const kernel = Kernel.create() + kernel.ui.switchMode('raw') + + class Serve extends BaseCommand { + static commandName: string = 'serve' + static description: string = 'Start the AdonisJS HTTP server' + } + + kernel.addLoader(new ListLoader([Serve])) + const command = await kernel.create(HelpCommand, ['serve']) + await command.exec() + + assert.equal(command.exitCode, 0) + assert.deepEqual(kernel.ui.logger.getLogs(), [ + { + message: '', + stream: 'stdout', + }, + { + message: 'yellow(Description:)', + stream: 'stdout', + }, + { + message: [' Start the AdonisJS HTTP server'].join('\n'), + stream: 'stdout', + }, + { + message: '', + stream: 'stdout', + }, + { + message: 'yellow(Usage:)', + stream: 'stdout', + }, + { + message: ' serve ', + stream: 'stdout', + }, + ]) + }) + + test('show command args in help output', async ({ assert }) => { + const kernel = Kernel.create() + kernel.ui.switchMode('raw') + + class MakeController extends BaseCommand { + @args.string({ description: 'Name of the controller' }) + name!: string + + static commandName: string = 'make:controller' + static description: string = 'Make a new HTTP controller' + } + + kernel.addLoader(new ListLoader([MakeController])) + const command = await kernel.create(HelpCommand, ['make:controller']) + await command.exec() + + assert.equal(command.exitCode, 0) + assert.deepEqual(kernel.ui.logger.getLogs(), [ + { + message: '', + stream: 'stdout', + }, + { + message: 'yellow(Description:)', + stream: 'stdout', + }, + { + message: [' Make a new HTTP controller'].join('\n'), + stream: 'stdout', + }, + { + message: '', + stream: 'stdout', + }, + { + message: 'yellow(Usage:)', + stream: 'stdout', + }, + { + message: ' make:controller dim()', + stream: 'stdout', + }, + { + message: '', + stream: 'stdout', + }, + { + message: 'yellow(Arguments:)', + stream: 'stdout', + }, + { + message: ' green(name) dim(Name of the controller)', + stream: 'stdout', + }, + ]) + }) + + test('show command optional arg in help output', async ({ assert }) => { + const kernel = Kernel.create() + kernel.ui.switchMode('raw') + + class MakeController extends BaseCommand { + @args.string({ description: 'Name of the controller', required: false }) + name!: string + + static commandName: string = 'make:controller' + static description: string = 'Make a new HTTP controller' + } + + kernel.addLoader(new ListLoader([MakeController])) + const command = await kernel.create(HelpCommand, ['make:controller']) + await command.exec() + + assert.equal(command.exitCode, 0) + assert.deepEqual(kernel.ui.logger.getLogs(), [ + { + message: '', + stream: 'stdout', + }, + { + message: 'yellow(Description:)', + stream: 'stdout', + }, + { + message: [' Make a new HTTP controller'].join('\n'), + stream: 'stdout', + }, + { + message: '', + stream: 'stdout', + }, + { + message: 'yellow(Usage:)', + stream: 'stdout', + }, + { + message: ' make:controller dim([])', + stream: 'stdout', + }, + { + message: '', + stream: 'stdout', + }, + { + message: 'yellow(Arguments:)', + stream: 'stdout', + }, + { + message: ' green([name]) dim(Name of the controller)', + stream: 'stdout', + }, + ]) + }) + + test('show command flags in the output', async ({ assert }) => { + const kernel = Kernel.create() + kernel.ui.switchMode('raw') + + class MakeController extends BaseCommand { + @flags.boolean({ description: 'Create resource methods' }) + resource!: boolean + + static commandName: string = 'make:controller' + static description: string = 'Make a new HTTP controller' + } + + kernel.addLoader(new ListLoader([MakeController])) + const command = await kernel.create(HelpCommand, ['make:controller']) + await command.exec() + + assert.equal(command.exitCode, 0) + assert.deepEqual(kernel.ui.logger.getLogs(), [ + { + message: '', + stream: 'stdout', + }, + { + message: 'yellow(Description:)', + stream: 'stdout', + }, + { + message: [' Make a new HTTP controller'].join('\n'), + stream: 'stdout', + }, + { + message: '', + stream: 'stdout', + }, + { + message: 'yellow(Usage:)', + stream: 'stdout', + }, + { + message: ' make:controller dim([options])', + stream: 'stdout', + }, + { + message: '', + stream: 'stdout', + }, + { + message: 'yellow(Options:)', + stream: 'stdout', + }, + { + message: ' green(--resource) dim(Create resource methods)', + stream: 'stdout', + }, + ]) + }) + + test('show required flags in the output', async ({ assert }) => { + const kernel = Kernel.create() + kernel.ui.switchMode('raw') + + class MakeController extends BaseCommand { + @flags.boolean({ description: 'Create resource methods', required: true }) + resource!: boolean + + static commandName: string = 'make:controller' + static description: string = 'Make a new HTTP controller' + } + + kernel.addLoader(new ListLoader([MakeController])) + const command = await kernel.create(HelpCommand, ['make:controller']) + await command.exec() + + assert.equal(command.exitCode, 0) + assert.deepEqual(kernel.ui.logger.getLogs(), [ + { + message: '', + stream: 'stdout', + }, + { + message: 'yellow(Description:)', + stream: 'stdout', + }, + { + message: [' Make a new HTTP controller'].join('\n'), + stream: 'stdout', + }, + { + message: '', + stream: 'stdout', + }, + { + message: 'yellow(Usage:)', + stream: 'stdout', + }, + { + message: ' make:controller dim([options])', + stream: 'stdout', + }, + { + message: '', + stream: 'stdout', + }, + { + message: 'yellow(Options:)', + stream: 'stdout', + }, + { + message: ' green(--resource) dim(Create resource methods)', + stream: 'stdout', + }, + ]) + }) + + test('show command aliases', async ({ assert }) => { + const kernel = Kernel.create() + kernel.ui.switchMode('raw') + + class MakeController extends BaseCommand { + static aliases: string[] = ['mc', 'controller'] + static commandName: string = 'make:controller' + static description: string = 'Make a new HTTP controller' + } + + kernel.addLoader(new ListLoader([MakeController])) + const command = await kernel.create(HelpCommand, ['make:controller']) + await command.exec() + + assert.equal(command.exitCode, 0) + assert.deepEqual(kernel.ui.logger.getLogs(), [ + { + message: '', + stream: 'stdout', + }, + { + message: 'yellow(Description:)', + stream: 'stdout', + }, + { + message: [' Make a new HTTP controller'].join('\n'), + stream: 'stdout', + }, + { + message: '', + stream: 'stdout', + }, + { + message: 'yellow(Usage:)', + stream: 'stdout', + }, + { + message: [' make:controller ', ' mc ', ' controller '].join('\n'), + stream: 'stdout', + }, + ]) + }) + + test('show command help', async ({ assert }) => { + const kernel = Kernel.create() + kernel.ui.switchMode('raw') + + class MakeController extends BaseCommand { + static aliases: string[] = ['mc', 'controller'] + static commandName: string = 'make:controller' + static description: string = 'Make a new HTTP controller' + static help?: string | string[] | undefined = '{{ binaryName }}make:controller' + } + + kernel.addLoader(new ListLoader([MakeController])) + kernel.info.set('binary', 'node ace') + const command = await kernel.create(HelpCommand, ['make:controller']) + await command.exec() + + assert.equal(command.exitCode, 0) + assert.deepEqual(kernel.ui.logger.getLogs(), [ + { + message: '', + stream: 'stdout', + }, + { + message: 'yellow(Description:)', + stream: 'stdout', + }, + { + message: [' Make a new HTTP controller'].join('\n'), + stream: 'stdout', + }, + { + message: '', + stream: 'stdout', + }, + { + message: 'yellow(Usage:)', + stream: 'stdout', + }, + { + message: [' node ace make:controller ', ' node ace mc ', ' node ace controller '].join( + '\n' + ), + stream: 'stdout', + }, + { + message: '', + stream: 'stdout', + }, + { + message: 'yellow(Help:)', + stream: 'stdout', + }, + { + message: ' node ace make:controller', + stream: 'stdout', + }, + ]) + }) + + test('error when command not found', async ({ assert }) => { + const kernel = Kernel.create() + kernel.ui.switchMode('raw') + + kernel.addLoader(new ListLoader([])) + const command = await kernel.create(HelpCommand, ['serve']) + await command.exec() + + assert.equal(command.exitCode, 1) + assert.deepEqual(kernel.ui.logger.getLogs(), [ + { + message: 'red(Command "serve" is not defined)', + stream: 'stderr', + }, + ]) + }) + + test('display suggestions when there are matching commands', async ({ assert }) => { + const kernel = Kernel.create() + kernel.ui.switchMode('raw') + + class Serve extends BaseCommand { + static commandName: string = 'serve' + static description: string = 'Start the AdonisJS HTTP server' + } + + kernel.addLoader(new ListLoader([Serve])) + const command = await kernel.create(HelpCommand, ['srve']) + await command.exec() + + assert.equal(command.exitCode, 1) + assert.deepEqual(kernel.ui.logger.getLogs(), [ + { + message: ['red(Command "srve" is not defined)', '', 'dim(Did you mean?) serve'].join('\n'), + stream: 'stderr', + }, + ]) + }) +}) diff --git a/tests/commands/list.spec.ts b/tests/commands/list.spec.ts index 99e0462..3552eb0 100644 --- a/tests/commands/list.spec.ts +++ b/tests/commands/list.spec.ts @@ -9,14 +9,15 @@ import { test } from '@japa/runner' import { Kernel } from '../../src/kernel.js' +import { ListCommand } from '../../index.js' import { args } from '../../src/decorators/args.js' import { flags } from '../../src/decorators/flags.js' -import { ListLoader } from '../../src/loaders/list_loader.js' import { BaseCommand } from '../../src/commands/base.js' +import { ListLoader } from '../../src/loaders/list_loader.js' test.group('List command', () => { test('show list of all the registered commands', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() kernel.ui.switchMode('raw') class Serve extends BaseCommand { @@ -37,7 +38,8 @@ test.group('List command', () => { } kernel.addLoader(new ListLoader([Serve, MakeController])) - const command = await kernel.exec('list', []) + const command = await kernel.create(ListCommand, []) + await command.exec() assert.equal(command.exitCode, 0) assert.deepEqual(kernel.ui.logger.getLogs(), [ @@ -72,7 +74,7 @@ test.group('List command', () => { }) test('show list of all the registered commands for a namespace', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() kernel.ui.switchMode('raw') class Serve extends BaseCommand { @@ -93,7 +95,8 @@ test.group('List command', () => { } kernel.addLoader(new ListLoader([Serve, MakeController])) - const command = await kernel.exec('list', ['make']) + const command = await kernel.create(ListCommand, ['make']) + await command.exec() assert.equal(command.exitCode, 0) assert.deepEqual(kernel.ui.logger.getLogs(), [ @@ -113,7 +116,7 @@ test.group('List command', () => { }) test('display error when namespace is invalid', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() kernel.ui.switchMode('raw') class Serve extends BaseCommand { @@ -134,7 +137,8 @@ test.group('List command', () => { } kernel.addLoader(new ListLoader([Serve, MakeController])) - const command = await kernel.exec('list', ['foo']) + const command = await kernel.create(ListCommand, ['foo']) + await command.exec() assert.equal(command.exitCode, 1) assert.deepEqual(kernel.ui.logger.getLogs(), [ @@ -146,7 +150,7 @@ test.group('List command', () => { }) test('show list of all kernel global flags', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() kernel.ui.switchMode('raw') class Serve extends BaseCommand { @@ -168,7 +172,8 @@ test.group('List command', () => { kernel.addLoader(new ListLoader([Serve, MakeController])) kernel.defineFlag('help', { type: 'boolean', description: 'View help of a given command' }) - const command = await kernel.exec('list', []) + const command = await kernel.create(ListCommand, []) + await command.exec() assert.equal(command.exitCode, 0) assert.deepEqual(kernel.ui.logger.getLogs(), [ diff --git a/tests/kernel/boot.spec.ts b/tests/kernel/boot.spec.ts index 0bd9d50..83bc3b2 100644 --- a/tests/kernel/boot.spec.ts +++ b/tests/kernel/boot.spec.ts @@ -15,7 +15,7 @@ import { ListLoader } from '../../src/loaders/list_loader.js' test.group('Kernel | boot', () => { test('load commands from loader during boot phase', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() class MakeController extends BaseCommand { static commandName = 'make:controller' @@ -36,7 +36,7 @@ test.group('Kernel | boot', () => { }) test('multiple calls to boot method should be a noop', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() class MakeController extends BaseCommand { static commandName = 'make:controller' @@ -60,7 +60,7 @@ test.group('Kernel | boot', () => { }) test('collect namespaces from the loaded commands', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() class MakeController extends BaseCommand { static commandName = 'make:controller' @@ -81,7 +81,7 @@ test.group('Kernel | boot', () => { }) test('collect command aliases', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() class MakeController extends BaseCommand { static commandName = 'make:controller' diff --git a/tests/kernel/default_command.spec.ts b/tests/kernel/default_command.spec.ts index d38cf9b..530adaf 100644 --- a/tests/kernel/default_command.spec.ts +++ b/tests/kernel/default_command.spec.ts @@ -14,29 +14,13 @@ import { BaseCommand } from '../../src/commands/base.js' test.group('Kernel | default command', () => { test('use a custom default command', async ({ assert }) => { - const kernel = new Kernel() - class VerboseHelp extends BaseCommand { static commandName = 'help' } - kernel.registerDefaultCommand(VerboseHelp) + const kernel = new Kernel(VerboseHelp, Kernel.commandExecutor) await kernel.boot() assert.strictEqual(kernel.getDefaultCommand(), VerboseHelp) }) - - test('disallow registering default command after kernel is booted', async ({ assert }) => { - const kernel = new Kernel() - await kernel.boot() - - class VerboseHelp extends BaseCommand { - static commandName = 'help' - } - - assert.throws( - () => kernel.registerDefaultCommand(VerboseHelp), - 'Cannot register default command in "booted" state' - ) - }) }) diff --git a/tests/kernel/exec.spec.ts b/tests/kernel/exec.spec.ts index 4b3f5ff..a7ee315 100644 --- a/tests/kernel/exec.spec.ts +++ b/tests/kernel/exec.spec.ts @@ -14,7 +14,7 @@ import { ListLoader } from '../../src/loaders/list_loader.js' test.group('Kernel | exec', () => { test('execute command', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() class MakeController extends BaseCommand { static commandName = 'make:controller' @@ -35,7 +35,7 @@ test.group('Kernel | exec', () => { }) test('run executing and executed hooks', async ({ assert, expectTypeOf }) => { - const kernel = new Kernel() + const kernel = Kernel.create() const stack: string[] = [] class MakeController extends BaseCommand { @@ -65,7 +65,7 @@ test.group('Kernel | exec', () => { }) test('report error when command validation fails', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() class MakeController extends BaseCommand { static commandName = 'make:controller' @@ -83,7 +83,7 @@ test.group('Kernel | exec', () => { }) test('report error when unable to find command', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() class MakeController extends BaseCommand { static commandName = 'make:controller' @@ -100,7 +100,7 @@ test.group('Kernel | exec', () => { }) test('report error when executing hook fails', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() class MakeController extends BaseCommand { static commandName = 'make:controller' @@ -119,7 +119,7 @@ test.group('Kernel | exec', () => { }) test('report error when executed hook fails', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() class MakeController extends BaseCommand { static commandName = 'make:controller' @@ -138,7 +138,7 @@ test.group('Kernel | exec', () => { }) test('do not allow termination from non-main commands', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() const stack: string[] = [] class MakeController extends BaseCommand { @@ -192,10 +192,7 @@ test.group('Kernel | exec', () => { MakeModel.defineArgument('name', { type: 'string' }) MakeModel.defineFlag('connection', { type: 'string' }) - const kernel = new Kernel() - kernel.addLoader(new ListLoader([MakeModel])) - - kernel.registerExecutor({ + const kernel = new Kernel(Kernel.defaultCommand, { create(Command, parsed, self) { stack.push('creating') return new Command(self, parsed, self.ui, self.prompt) @@ -206,32 +203,16 @@ test.group('Kernel | exec', () => { }, }) + kernel.addLoader(new ListLoader([MakeModel])) + await kernel.exec('make:model', ['users']) assert.deepEqual(stack, ['creating', 'running', 'prepare', 'interact', 'run', 'completed']) assert.isUndefined(kernel.exitCode) assert.equal(kernel.getState(), 'booted') }) - test('do not register executor after kernel is booted', async ({ assert }) => { - const kernel = new Kernel() - await kernel.boot() - - assert.throws( - () => - kernel.registerExecutor({ - create(Command, parsed, self) { - return new Command(self, parsed, self.ui, self.prompt) - }, - run(command) { - return command.exec() - }, - }), - 'Cannot register commands executor in "booted" state' - ) - }) - test('do not trigger flag listeners when not executing a main command', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() class MakeController extends BaseCommand { static commandName = 'make:controller' @@ -258,7 +239,7 @@ test.group('Kernel | exec', () => { }) test('disallow using global flags when executing commands', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() class MakeController extends BaseCommand { static commandName = 'make:controller' diff --git a/tests/kernel/find.spec.ts b/tests/kernel/find.spec.ts index b615c82..179edbc 100644 --- a/tests/kernel/find.spec.ts +++ b/tests/kernel/find.spec.ts @@ -16,7 +16,7 @@ import { CommandMetaData } from '../../src/types.js' test.group('Kernel | find', () => { test('find commands registered using a loader', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() class MakeController extends BaseCommand { static commandName = 'make:controller' @@ -34,7 +34,7 @@ test.group('Kernel | find', () => { }) test('find command using the command alias', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() class MakeController extends BaseCommand { static commandName = 'make:controller' @@ -54,7 +54,7 @@ test.group('Kernel | find', () => { }) test('raise error when unable to find command', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() class MakeController extends BaseCommand { static commandName = 'make:controller' @@ -72,7 +72,7 @@ test.group('Kernel | find', () => { }) test('raise error when loader is not able to lookup command', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() class MakeController extends BaseCommand { static commandName = 'make:controller' @@ -96,7 +96,7 @@ test.group('Kernel | find', () => { }) test('find command when using multiple loaders', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() class MakeController extends BaseCommand { static commandName = 'make:controller' @@ -116,7 +116,7 @@ test.group('Kernel | find', () => { }) test('execute finding, loading and loaded hooks', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() const stack: string[] = [] class MakeController extends BaseCommand { @@ -154,7 +154,7 @@ test.group('Kernel | find', () => { }) test('do not execute loading hook when command not found', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() const stack: string[] = [] class MakeController extends BaseCommand { diff --git a/tests/kernel/flag_listeners.spec.ts b/tests/kernel/flag_listeners.spec.ts index 9770da9..9b189a9 100644 --- a/tests/kernel/flag_listeners.spec.ts +++ b/tests/kernel/flag_listeners.spec.ts @@ -20,7 +20,7 @@ test.group('Kernel | handle', (group) => { }) test('execute flag listener on a global flag', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() class MakeController extends BaseCommand { static commandName = 'make:controller' @@ -44,7 +44,7 @@ test.group('Kernel | handle', (group) => { }) test('execute flag listener on a command flag', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() class MakeController extends BaseCommand { static commandName = 'make:controller' @@ -69,12 +69,17 @@ test.group('Kernel | handle', (group) => { }) test('terminate from the flag listener', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() const stack: string[] = [] class MakeController extends BaseCommand { static commandName = 'make:controller' - constructor($kernel: Kernel, parsed: ParsedOutput, ui: UIPrimitives, prompt: Prompt) { + constructor( + $kernel: Kernel, + parsed: ParsedOutput, + ui: UIPrimitives, + prompt: Prompt + ) { super($kernel, parsed, ui, prompt) stack.push('constructor') } @@ -100,9 +105,6 @@ test.group('Kernel | handle', (group) => { }) test('execute flag listener for the default command', async ({ assert }) => { - const kernel = new Kernel() - const stack: string[] = [] - class Help extends BaseCommand { static commandName = 'help' async run() { @@ -110,7 +112,9 @@ test.group('Kernel | handle', (group) => { } } - kernel.registerDefaultCommand(Help) + const kernel = new Kernel(Help, Kernel.commandExecutor) + const stack: string[] = [] + kernel.defineFlag('help', { type: 'boolean' }) kernel.on('help', (Command, _, options) => { assert.strictEqual(Command, Help) @@ -125,7 +129,6 @@ test.group('Kernel | handle', (group) => { }) test('terminate from the flag listener for the default command', async ({ assert }) => { - const kernel = new Kernel() const stack: string[] = [] class Help extends BaseCommand { @@ -135,7 +138,7 @@ test.group('Kernel | handle', (group) => { } } - kernel.registerDefaultCommand(Help) + const kernel = new Kernel(Help, Kernel.commandExecutor) kernel.defineFlag('help', { type: 'boolean' }) kernel.on('help', (Command, _, options) => { diff --git a/tests/kernel/gloal_flags.spec.ts b/tests/kernel/gloal_flags.spec.ts index 07f4dcd..5b32313 100644 --- a/tests/kernel/gloal_flags.spec.ts +++ b/tests/kernel/gloal_flags.spec.ts @@ -12,8 +12,8 @@ import { Kernel } from '../../src/kernel.js' test.group('Kernel | global flags', () => { test('define global flags', async ({ assert }) => { - const kernel = new Kernel() - const kernel1 = new Kernel() + const kernel = Kernel.create() + const kernel1 = Kernel.create() kernel.defineFlag('help', { type: 'boolean' }) kernel1.defineFlag('version', { type: 'boolean' }) @@ -37,7 +37,7 @@ test.group('Kernel | global flags', () => { }) test('disallow registering global flags after kernel is booted', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() await kernel.boot() assert.throws( diff --git a/tests/kernel/handle.spec.ts b/tests/kernel/handle.spec.ts index cfb1219..04700a4 100644 --- a/tests/kernel/handle.spec.ts +++ b/tests/kernel/handle.spec.ts @@ -20,7 +20,7 @@ test.group('Kernel | handle', (group) => { }) test('execute command as main command', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() class MakeController extends BaseCommand { static commandName = 'make:controller' async run() { @@ -38,7 +38,7 @@ test.group('Kernel | handle', (group) => { }) test('report error using logger command validation fails', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() kernel.ui = cliui({ mode: 'raw' }) class MakeController extends BaseCommand { @@ -64,7 +64,7 @@ test.group('Kernel | handle', (group) => { }) test('report error using logger when unable to find command', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() kernel.ui = cliui({ mode: 'raw' }) class MakeController extends BaseCommand { @@ -89,7 +89,7 @@ test.group('Kernel | handle', (group) => { }) test('report error when command hooks fails', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() const stack: string[] = [] class MakeController extends BaseCommand { @@ -121,7 +121,7 @@ test.group('Kernel | handle', (group) => { }) test('report error when command completed method fails', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() const stack: string[] = [] class MakeController extends BaseCommand { @@ -150,7 +150,7 @@ test.group('Kernel | handle', (group) => { }) test('disallow calling handle method twice in parallel', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() class MakeController extends BaseCommand { static commandName = 'make:controller' @@ -175,7 +175,7 @@ test.group('Kernel | handle', (group) => { test('disallow calling handle method after the process has been terminated', async ({ assert, }) => { - const kernel = new Kernel() + const kernel = Kernel.create() class MakeController extends BaseCommand { static commandName = 'make:controller' @@ -194,7 +194,7 @@ test.group('Kernel | handle', (group) => { }) test('disallow calling exec method after the process has been terminated', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() class MakeController extends BaseCommand { static commandName = 'make:controller' @@ -213,7 +213,6 @@ test.group('Kernel | handle', (group) => { }) test('run default command when args are provided', async ({ assert }) => { - const kernel = new Kernel() const stack: string[] = [] class Help extends BaseCommand { @@ -223,7 +222,7 @@ test.group('Kernel | handle', (group) => { } } - kernel.registerDefaultCommand(Help) + const kernel = new Kernel(Help, Kernel.commandExecutor) await kernel.handle([]) assert.deepEqual(stack, ['run help']) @@ -233,7 +232,6 @@ test.group('Kernel | handle', (group) => { }) test('run default command when only flags are provided', async ({ assert }) => { - const kernel = new Kernel() const stack: string[] = [] class Help extends BaseCommand { @@ -247,7 +245,7 @@ test.group('Kernel | handle', (group) => { } } - kernel.registerDefaultCommand(Help) + const kernel = new Kernel(Help, Kernel.commandExecutor) await kernel.handle(['--help']) assert.deepEqual(stack, ['run help']) diff --git a/tests/kernel/loaders.spec.ts b/tests/kernel/loaders.spec.ts index 6550572..266919e 100644 --- a/tests/kernel/loaders.spec.ts +++ b/tests/kernel/loaders.spec.ts @@ -15,7 +15,7 @@ import { ListLoader } from '../../src/loaders/list_loader.js' test.group('Kernel | loaders', () => { test('register commands using a loader', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() class MakeController extends BaseCommand { static commandName = 'make:controller' @@ -36,7 +36,7 @@ test.group('Kernel | loaders', () => { }) test('register commands using multiple loaders', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() class MakeController extends BaseCommand { static commandName = 'make:controller' @@ -58,7 +58,7 @@ test.group('Kernel | loaders', () => { }) test('disallow adding a loader after kernel is booted', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() class MakeController extends BaseCommand { static commandName = 'make:controller' diff --git a/tests/kernel/main.spec.ts b/tests/kernel/main.spec.ts index 3060ccb..5fc1136 100644 --- a/tests/kernel/main.spec.ts +++ b/tests/kernel/main.spec.ts @@ -16,7 +16,7 @@ import { ListCommand } from '../../src/commands/list.js' test.group('Kernel', () => { test('get alphabetically sorted list of commands', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() class MakeController extends BaseCommand { static commandName = 'make:controller' @@ -37,7 +37,7 @@ test.group('Kernel', () => { }) test('get alphabetically sorted list of namespaces', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() class ListRoutes extends BaseCommand { static commandName = 'list:routes' @@ -58,7 +58,7 @@ test.group('Kernel', () => { }) test('get list of commands for a given namespace', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() class ListRoutes extends BaseCommand { static commandName = 'list:routes' @@ -87,7 +87,7 @@ test.group('Kernel', () => { }) test('get list of top level commands without a namespace', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() class Migrate extends BaseCommand { static commandName = 'migrate' @@ -115,7 +115,7 @@ test.group('Kernel', () => { }) test('get an array of registered aliases', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() class Migrate extends BaseCommand { static commandName = 'migrate' @@ -143,7 +143,7 @@ test.group('Kernel', () => { }) test('get aliases for a given command', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() class Migrate extends BaseCommand { static commandName = 'migrate' @@ -172,7 +172,7 @@ test.group('Kernel', () => { }) test('get command for an alias', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() class Migrate extends BaseCommand { static commandName = 'migrate' @@ -202,7 +202,7 @@ test.group('Kernel', () => { }) test('get command metadata', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() class Migrate extends BaseCommand { static commandName = 'migrate' @@ -216,14 +216,14 @@ test.group('Kernel', () => { }) test('get the default command', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() await kernel.boot() assert.strictEqual(kernel.getDefaultCommand(), ListCommand) }) test('get commands suggestions for a given keyword', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() class Migrate extends BaseCommand { static commandName = 'migration:run' @@ -251,7 +251,7 @@ test.group('Kernel', () => { }) test('get commands suggestions for a namespace', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() class Migrate extends BaseCommand { static commandName = 'migration:run' @@ -278,7 +278,7 @@ test.group('Kernel', () => { }) test('get namespaces suggestions for a given keyword', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() class Migrate extends BaseCommand { static commandName = 'migration:run' diff --git a/tests/kernel/terminate.spec.ts b/tests/kernel/terminate.spec.ts index 0600343..6757ee4 100644 --- a/tests/kernel/terminate.spec.ts +++ b/tests/kernel/terminate.spec.ts @@ -19,7 +19,7 @@ test.group('Kernel | terminate', (group) => { }) test('do not terminate when not in running state', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() await kernel.boot() await kernel.terminate() @@ -29,7 +29,7 @@ test.group('Kernel | terminate', (group) => { }) test('do not terminate from a non-main command', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() class MakeController extends BaseCommand { static commandName = 'make:controller' @@ -64,7 +64,7 @@ test.group('Kernel | terminate', (group) => { }) test('terminate automatically after running the main command', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() class MakeController extends BaseCommand { static commandName = 'make:controller' @@ -85,7 +85,7 @@ test.group('Kernel | terminate', (group) => { }) test('do not terminate if command options.staysAlive is true', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() class MakeController extends BaseCommand { static commandName = 'make:controller' @@ -109,7 +109,7 @@ test.group('Kernel | terminate', (group) => { }) test('terminate when alive command calls terminate method', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() class MakeController extends BaseCommand { static commandName = 'make:controller' @@ -135,7 +135,7 @@ test.group('Kernel | terminate', (group) => { }) test('terminate from flag listener', async ({ assert }) => { - const kernel = new Kernel() + const kernel = Kernel.create() class MakeController extends BaseCommand { static commandName = 'make:controller' diff --git a/toolkit/commands/generate_manifest.ts b/toolkit/commands/generate_manifest.ts deleted file mode 100644 index 93e2db1..0000000 --- a/toolkit/commands/generate_manifest.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * @adonisjs/ace - * - * (c) AdonisJS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { BaseCommand } from '../../index.js' - -export class GenerateManifestCommand extends BaseCommand { - static commandName: string = 'generate:manifest' - static description: string = 'Generate a manifest JSO' -} diff --git a/toolkit/commands/index_command/main.ts b/toolkit/commands/index_command/main.ts new file mode 100644 index 0000000..4e8cdba --- /dev/null +++ b/toolkit/commands/index_command/main.ts @@ -0,0 +1,26 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { join } from 'node:path' +import { args, BaseCommand, FsLoader } from '../../../index.js' + +export class IndexCommand extends BaseCommand { + static commandName: string = 'index' + static description: string = + 'Generate JSON index and module loader for commands from a given directory' + + @args.string({ description: 'Path to the commands directory. Should be relative from "cwd"' }) + commandsDir!: string + + async run(): Promise { + const loader = new FsLoader(join(process.cwd(), this.commandsDir)) + const commandsMetaData = await loader.getMetaData() + JSON.stringify(commandsMetaData) + } +} diff --git a/toolkit/commands/index_command/stubs/module_loader.stub b/toolkit/commands/index_command/stubs/module_loader.stub new file mode 100644 index 0000000..22a5c22 --- /dev/null +++ b/toolkit/commands/index_command/stubs/module_loader.stub @@ -0,0 +1,18 @@ +import { readFile } from 'node:fs/promise' + +export async funnction list() { + const commandsIndex = await readFile('./commands_index.json', 'utf-8') + return JSON.parse(commandsIndex).commands +} + +export async funnction load(metaData) { + const commands = await this.list() + const command = commands.find(({ commandName }) => metaData.commandName) + + if (!command) { + return null + } + + const { default: commandConstructor } = await import(command.importPath) + return commandConstructor +} diff --git a/toolkit/main.ts b/toolkit/main.ts index 33d66f0..8f70068 100644 --- a/toolkit/main.ts +++ b/toolkit/main.ts @@ -9,4 +9,5 @@ import { Kernel } from '../index.js' -new Kernel() +const kernel = Kernel.create() +kernel.handle() From 88ea74efd2acbe3cb37201b6fa77d0c9a118f89a Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 25 Jan 2023 13:25:55 +0530 Subject: [PATCH 012/112] feat: remove termination logic from kernel --- command_metadata_schema.json | 1 - src/commands/base.ts | 9 +- src/helpers.ts | 8 +- src/kernel.ts | 128 ++++++--------------- src/loaders/fs_loader.ts | 11 +- src/loaders/list_loader.ts | 11 +- src/loaders/modules_loader.ts | 11 +- src/types.ts | 51 ++++++--- tests/base_command/exec.spec.ts | 20 ---- tests/kernel/exec.spec.ts | 26 ----- tests/kernel/find.spec.ts | 2 +- tests/kernel/flag_listeners.spec.ts | 10 +- tests/kernel/handle.spec.ts | 30 ++--- tests/kernel/terminate.spec.ts | 168 ---------------------------- toolkit/main.ts | 2 +- 15 files changed, 106 insertions(+), 382 deletions(-) delete mode 100644 tests/kernel/terminate.spec.ts diff --git a/command_metadata_schema.json b/command_metadata_schema.json index fc4351e..ef6c84c 100644 --- a/command_metadata_schema.json +++ b/command_metadata_schema.json @@ -156,7 +156,6 @@ "type": "object" }, "CommandOptions": { - "additionalProperties": false, "description": "Static set of command options", "properties": { "allowUnknownFlags": { diff --git a/src/commands/base.ts b/src/commands/base.ts index 1e3a6be..57580c3 100644 --- a/src/commands/base.ts +++ b/src/commands/base.ts @@ -406,7 +406,7 @@ export class BaseCommand { } constructor( - protected kernel: Kernel, + protected kernel: Kernel, protected parsed: ParsedOutput, public ui: UIPrimitives, public prompt: Prompt @@ -507,11 +507,4 @@ export class BaseCommand { return this.result } - - /** - * Invokes the terminate method on the kernel - */ - async terminate() { - this.kernel.terminate(this) - } } diff --git a/src/helpers.ts b/src/helpers.ts index 76c3c0a..754bc3d 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -10,10 +10,8 @@ import { inspect } from 'node:util' import { Validator } from 'jsonschema' import { RuntimeException } from '@poppinss/utils' - -import { BaseCommand } from '../index.js' -import type { CommandMetaData, UIPrimitives } from './types.js' import schema from '../command_metadata_schema.json' assert { type: 'json' } +import type { AbstractBaseCommand, CommandMetaData, UIPrimitives } from './types.js' /** * Helper to sort array of strings alphabetically. @@ -78,10 +76,10 @@ export function validCommandMetaData( * class, because the ace version mis-match could make the validation * fail. */ -export function validateCommand( +export function validateCommand( command: unknown, exportPath: string -): asserts command is typeof BaseCommand { +): asserts command is Command { if (typeof command !== 'function' || !command.toString().startsWith('class ')) { throw new RuntimeException( `Invalid command exported from ${exportPath}. Expected command to be a class` diff --git a/src/kernel.ts b/src/kernel.ts index 176bb1a..cb7c1d4 100644 --- a/src/kernel.ts +++ b/src/kernel.ts @@ -13,6 +13,7 @@ import { Prompt } from '@poppinss/prompts' import { findBestMatch } from 'string-similarity' import { RuntimeException } from '@poppinss/utils' +import debug from './debug.js' import { Parser } from './parser.js' import * as errors from './errors.js' import { ListCommand } from './commands/list.js' @@ -36,12 +37,10 @@ import type { ExecutingHookArgs, LoadingHookHandler, FindingHookHandler, - TerminatingHookArgs, + AbstractBaseCommand, ExecutedHookHandler, ExecutingHookHandler, - TerminatingHookHandler, } from './types.js' -import debug from './debug.js' const knowErrorCodes = Object.keys(errors) @@ -51,7 +50,7 @@ const knowErrorCodes = Object.keys(errors) * The kernel is the main entry point of a console application, and * is tailored for a standard CLI environment. */ -export class Kernel { +export class Kernel { /** * The default executor for creating command's instance * and running them @@ -107,10 +106,9 @@ export class Kernel { #hooks: Hooks<{ finding: FindingHookArgs loading: LoadingHookArgs - loaded: LoadedHookArgs - executing: ExecutingHookArgs - executed: ExecutedHookArgs - terminating: TerminatingHookArgs + loaded: LoadedHookArgs + executing: ExecutingHookArgs> + executed: ExecutedHookArgs> }> = new Hooks() /** @@ -129,12 +127,12 @@ export class Kernel { * The current state of kernel. The `running` and `terminated` * states are only set when kernel takes over the process. */ - #state: 'idle' | 'booted' | 'running' | 'terminated' = 'idle' + #state: 'idle' | 'booted' | 'running' | 'completed' = 'idle' /** * Collection of loaders to use for loading commands */ - #loaders: LoadersContract[] = [] + #loaders: LoadersContract[] = [] /** * An array of registered namespaces. Sorted alphabetically @@ -155,7 +153,8 @@ export class Kernel { * the unique commands and also keep the loader reference to know which * loader to ask for loading the command. */ - #commands: Map = new Map() + #commands: Map }> = + new Map() /** * The exit code for the kernel. The exit code is inferred @@ -275,11 +274,12 @@ export class Kernel { Command.validate(parsed) /** - * Terminate if a flag listener ends the process + * Return early if a flag listener shortcircuits */ if (shortcircuit) { debug('short circuiting from flag listener') - await this.terminate() + this.exitCode = this.exitCode ?? 0 + this.#state = 'completed' return } @@ -291,17 +291,14 @@ export class Kernel { /** * Execute the command using the executor */ - await this.#hooks.runner('executing').run(this.#mainCommand, true) - await this.#executor.run(this.#mainCommand, this) - await this.#hooks.runner('executed').run(this.#mainCommand, true) - - /** - * Terminate the process unless command wants to stay alive - */ - if (!Command.options.staysAlive) { - await this.terminate(this.#mainCommand) - } + await this.#hooks.runner('executing').run(this.#mainCommand!, true) + await this.#executor.run(this.#mainCommand!, this) + await this.#hooks.runner('executed').run(this.#mainCommand!, true) + this.exitCode = this.exitCode ?? this.#mainCommand!.exitCode ?? 0 + this.#state = 'completed' } catch (error) { + this.exitCode = 1 + this.#state = 'completed' await this.#handleError(error) } } @@ -313,12 +310,6 @@ export class Kernel { * handling errors of the main command */ async #handleError(error: any) { - /** - * Exit code will always be 1 if a hard exception was raised - * during command execution. - */ - this.exitCode = 1 - /** * Reporting errors with the best UI possible based upon the error * type @@ -334,11 +325,6 @@ export class Kernel { } else { console.log(error.stack) } - - /** - * Start termination - */ - await this.terminate(this.#mainCommand) } /** @@ -375,7 +361,7 @@ export class Kernel { * Incase multiple loaders returns a single command, the command from the * most recent loader will be used. */ - addLoader(loader: LoadersContract): this { + addLoader(loader: LoadersContract): this { if (this.#state !== 'idle') { throw new RuntimeException(`Cannot add loader in "${this.#state}" state`) } @@ -446,6 +432,13 @@ export class Kernel { return this.#defaultCommand } + /** + * Returns reference to the main command + */ + getMainCommand() { + return this.#mainCommand + } + /** * Returns an array of aliases registered. * @@ -539,7 +532,7 @@ export class Kernel { /** * Listen for the event when the command has been imported */ - loaded(callback: LoadedHookHandler) { + loaded(callback: LoadedHookHandler) { this.#hooks.add('loaded', callback) return this } @@ -547,7 +540,7 @@ export class Kernel { /** * Listen for the event before we start to execute the command. */ - executing(callback: ExecutingHookHandler) { + executing(callback: ExecutingHookHandler>) { this.#hooks.add('executing', callback) return this } @@ -555,19 +548,11 @@ export class Kernel { /** * Listen for the event after the command has been executed */ - executed(callback: ExecutedHookHandler) { + executed(callback: ExecutedHookHandler>) { this.#hooks.add('executed', callback) return this } - /** - * Listen for the event before we start to terminate the kernel - */ - terminating(callback: TerminatingHookHandler) { - this.#hooks.add('terminating', callback) - return this - } - /** * Loads commands from all the registered loaders. The "addLoader" method * must be called before calling the "load" method. @@ -659,10 +644,9 @@ export class Kernel { } /** - * Disallow calling commands if main commands was executed once and - * terminated + * Cannot execute commands after the main command has exited */ - if (this.#state === 'terminated') { + if (this.#state === 'completed') { throw new RuntimeException( 'The kernel has been terminated. Create a fresh instance to execute commands' ) @@ -699,9 +683,9 @@ export class Kernel { } /** - * Cannot run main command once the kernel has already been terminated + * Cannot run multiple main commands from the same instance */ - if (this.#state === 'terminated') { + if (this.#state === 'completed') { throw new RuntimeException( 'The kernel has been terminated. Create a fresh instance to execute commands' ) @@ -732,46 +716,4 @@ export class Kernel { debug('running main command "%s"', commandName) return this.#execMain(commandName, args) } - - /** - * Trigger process termination. The terminate method needs the command - * instance to know if the main command is triggering the termination - * or not. - * - * Only main commands can trigger the termination. - */ - async terminate(command?: BaseCommand) { - /** - * Do not terminate when the state is not running. The state - * is always running when we execute the handle method - */ - if (this.#state !== 'running') { - debug('denied terminating, since kernel.handle was never called') - return - } - - /** - * If we know about the command and the command trying - * to exit is not same as the main command, then - * do not terminate - */ - if (this.#mainCommand && command !== this.#mainCommand) { - debug('denied terminating, since command other than main command attempted to terminate') - return - } - - /** - * Started the termination process - */ - debug('terminating') - await this.#hooks.runner('terminating').run(this.#mainCommand) - this.#state = 'terminated' - - /** - * Set exit code if not already set. Also try to infer - * from the main command if exists - */ - this.exitCode = this.exitCode ?? this.#mainCommand?.exitCode ?? 0 - process.exitCode = this.exitCode - } } diff --git a/src/loaders/fs_loader.ts b/src/loaders/fs_loader.ts index e62071d..aa2b4c3 100644 --- a/src/loaders/fs_loader.ts +++ b/src/loaders/fs_loader.ts @@ -11,12 +11,11 @@ import { extname } from 'node:path' import { fsImportAll } from '@poppinss/utils' import { validateCommand } from '../helpers.js' -import { BaseCommand } from '../commands/base.js' -import type { CommandMetaData, LoadersContract } from '../types.js' +import type { AbstractBaseCommand, CommandMetaData, LoadersContract } from '../types.js' const JS_MODULES = ['.js', '.cjs', '.mjs'] -export class FsLoader implements LoadersContract { +export class FsLoader implements LoadersContract { /** * Absolute path to directory from which to load files */ @@ -25,7 +24,7 @@ export class FsLoader implements LoadersContract { /** * An array of loaded commands */ - #commands: (typeof BaseCommand)[] = [] + #commands: Command[] = [] constructor(comandsDirectory: string) { this.#comandsDirectory = comandsDirectory @@ -60,7 +59,7 @@ export class FsLoader implements LoadersContract { Object.keys(commandsCollection).forEach((key) => { const command = commandsCollection[key] - validateCommand(command, `"${key}" file`) + validateCommand(command, `"${key}" file`) this.#commands.push(command) }) @@ -71,7 +70,7 @@ export class FsLoader implements LoadersContract { * Returns the command class constructor for a given command. Null * is returned when unable to lookup the command */ - async getCommand(metaData: CommandMetaData): Promise { + async getCommand(metaData: CommandMetaData): Promise { return this.#commands.find((command) => command.commandName === metaData.commandName) || null } } diff --git a/src/loaders/list_loader.ts b/src/loaders/list_loader.ts index 27154bc..0ee325b 100644 --- a/src/loaders/list_loader.ts +++ b/src/loaders/list_loader.ts @@ -7,17 +7,16 @@ * file that was distributed with this source code. */ -import { BaseCommand } from '../commands/base.js' -import type { CommandMetaData, LoadersContract } from '../types.js' +import type { AbstractBaseCommand, CommandMetaData, LoadersContract } from '../types.js' /** * The CommandsList loader registers commands classes with the kernel. * The commands are kept within memory */ -export class ListLoader implements LoadersContract { - #commands: (typeof BaseCommand)[] +export class ListLoader implements LoadersContract { + #commands: Command[] - constructor(commands: (typeof BaseCommand)[]) { + constructor(commands: Command[]) { this.#commands = commands } @@ -32,7 +31,7 @@ export class ListLoader implements LoadersContract { * Returns the command class constructor for a given command. Null * is returned when unable to lookup the command */ - async getCommand(metaData: CommandMetaData): Promise { + async getCommand(metaData: CommandMetaData): Promise { return this.#commands.find((command) => command.commandName === metaData.commandName) || null } } diff --git a/src/loaders/modules_loader.ts b/src/loaders/modules_loader.ts index 9df5da4..7edb90a 100644 --- a/src/loaders/modules_loader.ts +++ b/src/loaders/modules_loader.ts @@ -9,9 +9,8 @@ import { RuntimeException } from '@poppinss/utils' -import { BaseCommand } from '../commands/base.js' -import type { CommandMetaData, LoadersContract } from '../types.js' import { validateCommand, validCommandMetaData } from '../helpers.js' +import type { AbstractBaseCommand, CommandMetaData, LoadersContract } from '../types.js' /** * Module based command loader must implement the following methods. @@ -28,7 +27,9 @@ type CommandsLoader = { * The modules have to implement the `list` and the `load` * methods */ -export class ModulesLoader implements LoadersContract { +export class ModulesLoader + implements LoadersContract +{ /** * The import root is the base path to use when resolving * modules mentioned in the command source. @@ -125,7 +126,7 @@ export class ModulesLoader implements LoadersContract { * Returns the command class constructor for a given command. Null * is returned when unable to lookup the command */ - async getCommand(metaData: CommandMetaData): Promise { + async getCommand(metaData: CommandMetaData): Promise { /** * Running "loadCommands" method instantiates the commands loader * collection @@ -144,7 +145,7 @@ export class ModulesLoader implements LoadersContract { return null } - validateCommand(command, `"${commandLoader.sourcePath}.load" method`) + validateCommand(command, `"${commandLoader.sourcePath}.load" method`) return command } } diff --git a/src/types.ts b/src/types.ts index e73942b..9597978 100644 --- a/src/types.ts +++ b/src/types.ts @@ -12,7 +12,6 @@ import type { Arguments, Options } from 'yargs-parser' import type { HookHandler } from '@poppinss/hooks/types' import type { Kernel } from './kernel.js' -import type { BaseCommand } from './commands/base.js' /** * Parsed output of yargs @@ -55,7 +54,7 @@ export type UIPrimitives = ReturnType /** * All loaders must adhere to the LoadersContract */ -export interface LoadersContract { +export interface LoadersContract { /** * The method should return an array of commands metadata */ @@ -65,14 +64,14 @@ export interface LoadersContract { * The method should return the command instance by command * name */ - getCommand(command: CommandMetaData): Promise + getCommand(command: CommandMetaData): Promise } /** * Command executor is used to create a new instance of the command * and run it. */ -export interface ExecutorContract { +export interface ExecutorContract { /** * Create a new instance of the command */ @@ -312,7 +311,7 @@ export type CommandOptions = { * Defaults to false */ staysAlive?: boolean -} +} & Record /** * Finding hook handler and data @@ -329,31 +328,31 @@ export type LoadingHookHandler = HookHandler +export type LoadedHookArgs = [[Command], [Command]] +export type LoadedHookHandler = HookHandler< + LoadedHookArgs[0], + LoadedHookArgs[1] +> /** * Executing hook handler and data */ -export type ExecutingHookArgs = [[BaseCommand, boolean], [BaseCommand, boolean]] -export type ExecutingHookHandler = HookHandler +export type ExecutingHookArgs = [[Command, boolean], [Command, boolean]] +export type ExecutingHookHandler = HookHandler< + ExecutingHookArgs[0], + ExecutingHookArgs[1] +> /** * Executed hook handler and data */ -export type ExecutedHookArgs = ExecutingHookArgs -export type ExecutedHookHandler = ExecutingHookHandler - -/** - * Terminating hook handler and data - */ -export type TerminatingHookArgs = [[BaseCommand?], [BaseCommand?]] -export type TerminatingHookHandler = HookHandler +export type ExecutedHookArgs = ExecutingHookArgs +export type ExecutedHookHandler = ExecutingHookHandler /** * A listener that listeners for flags when they are mentioned. */ -export type FlagListener = ( +export type FlagListener = ( command: Command, kernel: Kernel, parsedOutput: ParsedOutput @@ -374,3 +373,19 @@ export type ListTable = { * A union of data types allowed for the info key-value pair */ export type AllowedInfoValues = number | boolean | string | string[] | number[] | boolean[] + +/** + * Abstract command defines the mandatory properties on a + * command class needed by the internals of ace. + */ +export type AbstractBaseCommand = { + commandName: string + options: CommandOptions + serialize(): CommandMetaData + validate(parsedOutput: ParsedOutput): void + getParserOptions(options?: FlagsParserOptions): { + flagsParserOptions: Required + argumentsParserOptions: ArgumentsParserOptions[] + } + new (...args: any[]): any +} diff --git a/tests/base_command/exec.spec.ts b/tests/base_command/exec.spec.ts index 5d69175..5cf044e 100644 --- a/tests/base_command/exec.spec.ts +++ b/tests/base_command/exec.spec.ts @@ -7,7 +7,6 @@ * file that was distributed with this source code. */ -import sinon from 'sinon' import { test } from '@japa/runner' import { cliui } from '@poppinss/cliui' @@ -337,22 +336,3 @@ test.group('Base command | execute | complete method', () => { assert.lengthOf(model.ui.logger.getRenderer().getLogs(), 0) }) }) - -test.group('Base command | terminate', () => { - test('call terminate method on kernel', async ({ cleanup }) => { - class MakeModel extends BaseCommand {} - MakeModel.boot() - - const kernel = Kernel.create() - kernel.ui = cliui({ mode: 'raw' }) - const model = await kernel.create(MakeModel, []) - - const terminate = sinon.stub(kernel, 'terminate') - cleanup(() => { - terminate.restore() - }) - - await model.terminate() - terminate.calledWith(model) - }) -}) diff --git a/tests/kernel/exec.spec.ts b/tests/kernel/exec.spec.ts index a7ee315..4afac2d 100644 --- a/tests/kernel/exec.spec.ts +++ b/tests/kernel/exec.spec.ts @@ -137,32 +137,6 @@ test.group('Kernel | exec', () => { await assert.rejects(() => kernel.exec('make:controller', ['users']), 'Post hook failed') }) - test('do not allow termination from non-main commands', async ({ assert }) => { - const kernel = Kernel.create() - const stack: string[] = [] - - class MakeController extends BaseCommand { - static commandName = 'make:controller' - async run() { - await this.terminate() - return 'executed' - } - } - - MakeController.defineArgument('name', { type: 'string' }) - - kernel.addLoader(new ListLoader([MakeController])) - kernel.terminating(() => { - stack.push('terminating') - throw new Error('Never expected to run') - }) - - await kernel.exec('make:controller', ['users']) - assert.deepEqual(stack, []) - assert.isUndefined(kernel.exitCode) - assert.equal(kernel.getState(), 'booted') - }) - test('use custom executor', async ({ assert }) => { const stack: string[] = [] diff --git a/tests/kernel/find.spec.ts b/tests/kernel/find.spec.ts index 179edbc..34ca8ad 100644 --- a/tests/kernel/find.spec.ts +++ b/tests/kernel/find.spec.ts @@ -83,7 +83,7 @@ test.group('Kernel | find', () => { static commandName = 'make:model' } - class CustomLoader extends ListLoader { + class CustomLoader extends ListLoader { async getCommand(_: CommandMetaData): Promise { return null } diff --git a/tests/kernel/flag_listeners.spec.ts b/tests/kernel/flag_listeners.spec.ts index 9b189a9..b98698b 100644 --- a/tests/kernel/flag_listeners.spec.ts +++ b/tests/kernel/flag_listeners.spec.ts @@ -39,7 +39,7 @@ test.group('Kernel | handle', (group) => { await kernel.handle(['make:controller', 'users', '--verbose']) - assert.equal(kernel.getState(), 'terminated') + assert.equal(kernel.getState(), 'completed') assert.equal(kernel.exitCode, 0) }) @@ -64,7 +64,7 @@ test.group('Kernel | handle', (group) => { await kernel.handle(['make:controller', 'users', '--connection', 'sqlite']) - assert.equal(kernel.getState(), 'terminated') + assert.equal(kernel.getState(), 'completed') assert.equal(kernel.exitCode, 0) }) @@ -99,7 +99,7 @@ test.group('Kernel | handle', (group) => { }) await kernel.handle(['make:controller', 'users', '--help']) - assert.equal(kernel.getState(), 'terminated') + assert.equal(kernel.getState(), 'completed') assert.equal(kernel.exitCode, 0) assert.deepEqual(stack, []) }) @@ -123,7 +123,7 @@ test.group('Kernel | handle', (group) => { await kernel.handle(['--help']) - assert.equal(kernel.getState(), 'terminated') + assert.equal(kernel.getState(), 'completed') assert.equal(kernel.exitCode, 0) assert.deepEqual(stack, ['run help']) }) @@ -149,7 +149,7 @@ test.group('Kernel | handle', (group) => { await kernel.handle(['--help']) - assert.equal(kernel.getState(), 'terminated') + assert.equal(kernel.getState(), 'completed') assert.equal(kernel.exitCode, 0) assert.deepEqual(stack, []) }) diff --git a/tests/kernel/handle.spec.ts b/tests/kernel/handle.spec.ts index 04700a4..d138c21 100644 --- a/tests/kernel/handle.spec.ts +++ b/tests/kernel/handle.spec.ts @@ -25,6 +25,7 @@ test.group('Kernel | handle', (group) => { static commandName = 'make:controller' async run() { assert.equal(this.kernel.getState(), 'running') + assert.strictEqual(this.kernel.getMainCommand(), this) return 'executed' } } @@ -33,8 +34,7 @@ test.group('Kernel | handle', (group) => { await kernel.handle(['make:controller']) assert.equal(kernel.exitCode, 0) - assert.equal(process.exitCode, 0) - assert.equal(kernel.getState(), 'terminated') + assert.equal(kernel.getState(), 'completed') }) test('report error using logger command validation fails', async ({ assert }) => { @@ -52,7 +52,7 @@ test.group('Kernel | handle', (group) => { kernel.addLoader(new ListLoader([MakeController])) await kernel.handle(['make:controller']) - assert.equal(kernel.getState(), 'terminated') + assert.equal(kernel.getState(), 'completed') assert.equal(kernel.exitCode, 1) assert.deepEqual(kernel.ui.logger.getRenderer().getLogs(), [ @@ -78,7 +78,7 @@ test.group('Kernel | handle', (group) => { kernel.addLoader(new ListLoader([MakeController])) await kernel.handle(['foo']) - assert.equal(kernel.getState(), 'terminated') + assert.equal(kernel.getState(), 'completed') assert.equal(kernel.exitCode, 1) assert.deepEqual(kernel.ui.logger.getRenderer().getLogs(), [ { @@ -109,15 +109,12 @@ test.group('Kernel | handle', (group) => { stack.push('executed') throw new Error('Post hook failed') }) - kernel.terminating(() => { - stack.push('terminating') - }) await kernel.handle(['make:controller', 'users']) - assert.equal(kernel.getState(), 'terminated') + assert.equal(kernel.getState(), 'completed') assert.equal(kernel.exitCode, 1) - assert.deepEqual(stack, ['executing', 'run', 'executed', 'terminating']) + assert.deepEqual(stack, ['executing', 'run', 'executed']) }) test('report error when command completed method fails', async ({ assert }) => { @@ -138,15 +135,12 @@ test.group('Kernel | handle', (group) => { MakeController.defineArgument('name', { type: 'string' }) kernel.addLoader(new ListLoader([MakeController])) - kernel.terminating(() => { - stack.push('terminating') - }) await kernel.handle(['make:controller', 'users']) - assert.equal(kernel.getState(), 'terminated') + assert.equal(kernel.getState(), 'completed') assert.equal(kernel.exitCode, 1) - assert.deepEqual(stack, ['run', 'terminating']) + assert.deepEqual(stack, ['run']) }) test('disallow calling handle method twice in parallel', async ({ assert }) => { @@ -172,7 +166,7 @@ test.group('Kernel | handle', (group) => { ) }) - test('disallow calling handle method after the process has been terminated', async ({ + test('disallow calling handle method after the process has been completed', async ({ assert, }) => { const kernel = Kernel.create() @@ -227,8 +221,7 @@ test.group('Kernel | handle', (group) => { assert.deepEqual(stack, ['run help']) assert.equal(kernel.exitCode, 0) - assert.equal(process.exitCode, 0) - assert.equal(kernel.getState(), 'terminated') + assert.equal(kernel.getState(), 'completed') }) test('run default command when only flags are provided', async ({ assert }) => { @@ -250,7 +243,6 @@ test.group('Kernel | handle', (group) => { assert.deepEqual(stack, ['run help']) assert.equal(kernel.exitCode, 0) - assert.equal(process.exitCode, 0) - assert.equal(kernel.getState(), 'terminated') + assert.equal(kernel.getState(), 'completed') }) }) diff --git a/tests/kernel/terminate.spec.ts b/tests/kernel/terminate.spec.ts deleted file mode 100644 index 6757ee4..0000000 --- a/tests/kernel/terminate.spec.ts +++ /dev/null @@ -1,168 +0,0 @@ -/* - * @adonisjs/ace - * - * (c) AdonisJS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { test } from '@japa/runner' - -import { Kernel } from '../../src/kernel.js' -import { BaseCommand } from '../../src/commands/base.js' -import { ListLoader } from '../../src/loaders/list_loader.js' - -test.group('Kernel | terminate', (group) => { - group.each.teardown(() => { - process.exitCode = undefined - }) - - test('do not terminate when not in running state', async ({ assert }) => { - const kernel = Kernel.create() - await kernel.boot() - await kernel.terminate() - - assert.isUndefined(kernel.exitCode) - assert.isUndefined(process.exitCode) - assert.equal(kernel.getState(), 'booted') - }) - - test('do not terminate from a non-main command', async ({ assert }) => { - const kernel = Kernel.create() - - class MakeController extends BaseCommand { - static commandName = 'make:controller' - async run() {} - } - - class MakeModel extends BaseCommand { - static commandName = 'make:model' - async run() {} - } - - kernel.addLoader(new ListLoader([MakeController, MakeModel])) - kernel.executed(async () => { - await kernel.terminate( - new MakeModel( - kernel, - { args: [], _: [], flags: {}, unknownFlags: [] }, - kernel.ui, - kernel.prompt - ) - ) - }) - kernel.executed(async () => { - assert.equal(kernel.getState(), 'running') - }) - - await kernel.handle(['make:controller']) - - assert.equal(kernel.exitCode, 0) - assert.equal(process.exitCode, 0) - assert.equal(kernel.getState(), 'terminated') - }) - - test('terminate automatically after running the main command', async ({ assert }) => { - const kernel = Kernel.create() - - class MakeController extends BaseCommand { - static commandName = 'make:controller' - async run() {} - } - - class MakeModel extends BaseCommand { - static commandName = 'make:model' - async run() {} - } - - kernel.addLoader(new ListLoader([MakeController, MakeModel])) - await kernel.handle(['make:controller']) - - assert.equal(kernel.exitCode, 0) - assert.equal(process.exitCode, 0) - assert.equal(kernel.getState(), 'terminated') - }) - - test('do not terminate if command options.staysAlive is true', async ({ assert }) => { - const kernel = Kernel.create() - - class MakeController extends BaseCommand { - static commandName = 'make:controller' - static options = { - staysAlive: true, - } - async run() {} - } - - class MakeModel extends BaseCommand { - static commandName = 'make:model' - async run() {} - } - - kernel.addLoader(new ListLoader([MakeController, MakeModel])) - await kernel.handle(['make:controller']) - - assert.isUndefined(kernel.exitCode) - assert.isUndefined(process.exitCode) - assert.equal(kernel.getState(), 'running') - }) - - test('terminate when alive command calls terminate method', async ({ assert }) => { - const kernel = Kernel.create() - - class MakeController extends BaseCommand { - static commandName = 'make:controller' - static options = { - staysAlive: true, - } - async run() { - await this.terminate() - } - } - - class MakeModel extends BaseCommand { - static commandName = 'make:model' - async run() {} - } - - kernel.addLoader(new ListLoader([MakeController, MakeModel])) - await kernel.handle(['make:controller']) - - assert.equal(kernel.exitCode, 0) - assert.equal(process.exitCode, 0) - assert.equal(kernel.getState(), 'terminated') - }) - - test('terminate from flag listener', async ({ assert }) => { - const kernel = Kernel.create() - - class MakeController extends BaseCommand { - static commandName = 'make:controller' - static options = { - staysAlive: true, - } - async run() {} - } - - class MakeModel extends BaseCommand { - static commandName = 'make:model' - async run() {} - } - - kernel.addLoader(new ListLoader([MakeController, MakeModel])) - kernel.defineFlag('help', { - type: 'boolean', - }) - - kernel.on('help', () => { - return true - }) - - await kernel.handle(['make:controller', '--help']) - - assert.equal(kernel.exitCode, 0) - assert.equal(process.exitCode, 0) - assert.equal(kernel.getState(), 'terminated') - }) -}) diff --git a/toolkit/main.ts b/toolkit/main.ts index 8f70068..f629116 100644 --- a/toolkit/main.ts +++ b/toolkit/main.ts @@ -10,4 +10,4 @@ import { Kernel } from '../index.js' const kernel = Kernel.create() -kernel.handle() +kernel.handle(process.argv) From 1ed81fcb966744e249a8c152d71b94a07397c53b Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 26 Jan 2023 08:38:43 +0530 Subject: [PATCH 013/112] fix: use declare modifier for defining property names --- package.json | 2 +- src/commands/base.ts | 8 +- src/commands/help.ts | 2 +- src/commands/list.ts | 2 +- src/kernel.ts | 8 ++ tests/base_command/main.spec.ts | 34 ++++----- tests/base_command/serialize.spec.ts | 105 +++++++++++++++++++++++++++ tests/commands/help.spec.ts | 8 +- tests/decorators/args.spec.ts | 10 +-- tests/decorators/flags.spec.ts | 8 +- tests/formatters/arg.spec.ts | 14 ++-- tests/formatters/command.spec.ts | 32 ++++---- tests/formatters/flag.spec.ts | 34 ++++----- 13 files changed, 190 insertions(+), 77 deletions(-) diff --git a/package.json b/package.json index a0ba5bb..98d8146 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "@japa/runner": "^2.1.1", "@japa/spec-reporter": "^1.2.0", "@poppinss/dev-utils": "^2.0.3", - "@swc/core": "^1.3.27", + "@swc/core": "^1.3.28", "@types/fs-extra": "^11.0.1", "@types/node": "^18.7.15", "@types/sinon": "^10.0.13", diff --git a/src/commands/base.ts b/src/commands/base.ts index 57580c3..d47d6f8 100644 --- a/src/commands/base.ts +++ b/src/commands/base.ts @@ -81,10 +81,10 @@ export class BaseCommand { this.booted = true defineStaticProperty(this, 'args', { initialValue: [], strategy: 'inherit' }) defineStaticProperty(this, 'flags', { initialValue: [], strategy: 'inherit' }) - defineStaticProperty(this, 'aliases', { initialValue: [], strategy: 'define' }) - defineStaticProperty(this, 'commandName', { initialValue: '', strategy: 'define' }) - defineStaticProperty(this, 'description', { initialValue: '', strategy: 'define' }) - defineStaticProperty(this, 'help', { initialValue: '', strategy: 'define' }) + defineStaticProperty(this, 'aliases', { initialValue: [], strategy: 'inherit' }) + defineStaticProperty(this, 'commandName', { initialValue: '', strategy: 'inherit' }) + defineStaticProperty(this, 'description', { initialValue: '', strategy: 'inherit' }) + defineStaticProperty(this, 'help', { initialValue: '', strategy: 'inherit' }) defineStaticProperty(this, 'options', { initialValue: { staysAlive: false, allowUnknownFlags: false }, strategy: 'inherit', diff --git a/src/commands/help.ts b/src/commands/help.ts index 6c4c3e3..3dbdda8 100644 --- a/src/commands/help.ts +++ b/src/commands/help.ts @@ -32,7 +32,7 @@ export class HelpCommand extends BaseCommand { * The command name argument */ @args.string({ description: 'Command name', argumentName: 'command' }) - commandName!: string + declare commandName: string /** * Returns the command arguments table diff --git a/src/commands/list.ts b/src/commands/list.ts index 794010c..67f7c36 100644 --- a/src/commands/list.ts +++ b/src/commands/list.ts @@ -39,7 +39,7 @@ export class ListCommand extends BaseCommand { description: 'Filter list by namespace', required: false, }) - namespaces?: string[] + declare namespaces?: string[] /** * Returns a table for an array of commands. diff --git a/src/kernel.ts b/src/kernel.ts index cb7c1d4..d6689f0 100644 --- a/src/kernel.ts +++ b/src/kernel.ts @@ -716,4 +716,12 @@ export class Kernel { debug('running main command "%s"', commandName) return this.#execMain(commandName, args) } + + /** + * A named function that returns true. To be used + * by flag listeners + */ + shortcircuit() { + return true + } } diff --git a/tests/base_command/main.spec.ts b/tests/base_command/main.spec.ts index 99e25d9..b52cdd8 100644 --- a/tests/base_command/main.spec.ts +++ b/tests/base_command/main.spec.ts @@ -16,8 +16,8 @@ import { BaseCommand } from '../../src/commands/base.js' test.group('Base command', () => { test('access the ui logger from the logger property', ({ assert }) => { class MakeModel extends BaseCommand { - name!: string - connection!: string + declare name: string + declare connection: string } MakeModel.boot() @@ -34,8 +34,8 @@ test.group('Base command', () => { test('access the ui colors from the colors property', ({ assert }) => { class MakeModel extends BaseCommand { - name!: string - connection!: string + declare name: string + declare connection: string } MakeModel.boot() @@ -54,8 +54,8 @@ test.group('Base command', () => { test.group('Base command | consume args', () => { test('consume parsed output to set command properties', async ({ assert }) => { class MakeModel extends BaseCommand { - name!: string - connection!: string + declare name: string + declare connection: string } MakeModel.defineArgument('name', { type: 'string' }) @@ -70,8 +70,8 @@ test.group('Base command | consume args', () => { test('consume spread arg', async ({ assert }) => { class MakeModel extends BaseCommand { - names!: string[] - connection!: string + declare names: string[] + declare connection: string } MakeModel.defineArgument('names', { type: 'spread' }) @@ -88,9 +88,9 @@ test.group('Base command | consume args', () => { test.group('Base command | consume flags', () => { test('consume boolean flag', async ({ assert }) => { class MakeModel extends BaseCommand { - name!: string - connection!: string - dropAll!: boolean + declare name: string + declare connection: string + declare dropAll: boolean } MakeModel.defineArgument('name', { type: 'string' }) @@ -107,9 +107,9 @@ test.group('Base command | consume flags', () => { test('consume array flag', async ({ assert }) => { class MakeModel extends BaseCommand { - name!: string - connections!: string[] - dropAll!: boolean + declare name: string + declare connections: string[] + declare dropAll: boolean } MakeModel.defineArgument('name', { type: 'string' }) @@ -130,9 +130,9 @@ test.group('Base command | consume flags', () => { test('use default value when array flag is missing', async ({ assert }) => { class MakeModel extends BaseCommand { - name!: string - connections!: string[] - dropAll!: boolean + declare name: string + declare connections: string[] + declare dropAll: boolean } MakeModel.defineArgument('name', { type: 'string' }) diff --git a/tests/base_command/serialize.spec.ts b/tests/base_command/serialize.spec.ts index 517ef93..25cb8a8 100644 --- a/tests/base_command/serialize.spec.ts +++ b/tests/base_command/serialize.spec.ts @@ -131,4 +131,109 @@ test.group('Base command | serialize', () => { 'Cannot serialize command "MakeModel". Missing static property "commandName"' ) }) + + test('serialize inherited command with args', ({ assert }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'make:model' + static description: string = 'Make a new model' + } + + MakeModel.defineArgument('name', { type: 'string', description: 'Name of the argument' }) + + class MakeNewModel extends MakeModel {} + + assert.deepEqual(MakeNewModel.serialize(), { + commandName: 'make:model', + namespace: 'make', + description: 'Make a new model', + help: '', + args: [ + { + name: 'name', + argumentName: 'name', + required: true, + type: 'string', + description: 'Name of the argument', + }, + ], + flags: [], + aliases: [], + options: { + allowUnknownFlags: false, + staysAlive: false, + }, + }) + }) + + test('override command name from inherited command', ({ assert }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'make:model' + static description: string = 'Make a new model' + } + + MakeModel.defineArgument('name', { type: 'string', description: 'Name of the argument' }) + + class MakeNewModel extends MakeModel { + static commandName: string = 'make:new:model' + } + + assert.deepEqual(MakeNewModel.serialize(), { + commandName: 'make:new:model', + namespace: 'make', + description: 'Make a new model', + help: '', + args: [ + { + name: 'name', + argumentName: 'name', + required: true, + type: 'string', + description: 'Name of the argument', + }, + ], + flags: [], + aliases: [], + options: { + allowUnknownFlags: false, + staysAlive: false, + }, + }) + }) + + test('override aliases from inherited command', ({ assert }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'make:model' + static aliases: string[] = ['mm'] + static description: string = 'Make a new model' + } + + MakeModel.defineArgument('name', { type: 'string', description: 'Name of the argument' }) + + class MakeNewModel extends MakeModel { + static commandName: string = 'make:new:model' + static aliases: string[] = ['mnm'] + } + + assert.deepEqual(MakeNewModel.serialize(), { + commandName: 'make:new:model', + namespace: 'make', + description: 'Make a new model', + help: '', + args: [ + { + name: 'name', + argumentName: 'name', + required: true, + type: 'string', + description: 'Name of the argument', + }, + ], + flags: [], + aliases: ['mnm'], + options: { + allowUnknownFlags: false, + staysAlive: false, + }, + }) + }) }) diff --git a/tests/commands/help.spec.ts b/tests/commands/help.spec.ts index b6ec44f..442a61c 100644 --- a/tests/commands/help.spec.ts +++ b/tests/commands/help.spec.ts @@ -64,7 +64,7 @@ test.group('Help command', () => { class MakeController extends BaseCommand { @args.string({ description: 'Name of the controller' }) - name!: string + declare name: string static commandName: string = 'make:controller' static description: string = 'Make a new HTTP controller' @@ -121,7 +121,7 @@ test.group('Help command', () => { class MakeController extends BaseCommand { @args.string({ description: 'Name of the controller', required: false }) - name!: string + declare name: string static commandName: string = 'make:controller' static description: string = 'Make a new HTTP controller' @@ -178,7 +178,7 @@ test.group('Help command', () => { class MakeController extends BaseCommand { @flags.boolean({ description: 'Create resource methods' }) - resource!: boolean + declare resource: boolean static commandName: string = 'make:controller' static description: string = 'Make a new HTTP controller' @@ -235,7 +235,7 @@ test.group('Help command', () => { class MakeController extends BaseCommand { @flags.boolean({ description: 'Create resource methods', required: true }) - resource!: boolean + declare resource: boolean static commandName: string = 'make:controller' static description: string = 'Make a new HTTP controller' diff --git a/tests/decorators/args.spec.ts b/tests/decorators/args.spec.ts index 0a05a14..7fb96db 100644 --- a/tests/decorators/args.spec.ts +++ b/tests/decorators/args.spec.ts @@ -15,7 +15,7 @@ test.group('Decorators | args', () => { test('define string argument', ({ assert }) => { class MakeModel extends BaseCommand { @args.string() - name!: string + declare name: string } assert.deepEqual(MakeModel.args, [ @@ -26,7 +26,7 @@ test.group('Decorators | args', () => { test('define argument with inheritance', ({ assert }) => { class MakeEntity extends BaseCommand { @args.string() - name!: string + declare name: string } class MakeModel extends MakeEntity { @@ -36,7 +36,7 @@ test.group('Decorators | args', () => { class MakeController extends MakeEntity { @args.string() - resourceName!: string + declare resourceName: string } assert.deepEqual(MakeModel.args, [ @@ -52,10 +52,10 @@ test.group('Decorators | args', () => { test('define spread argument', ({ assert }) => { class MakeModel extends BaseCommand { @args.string() - name!: string + declare name: string @args.spread() - connections!: string[] + declare connections: string[] } assert.deepEqual(MakeModel.args, [ diff --git a/tests/decorators/flags.spec.ts b/tests/decorators/flags.spec.ts index 5745d76..63588f8 100644 --- a/tests/decorators/flags.spec.ts +++ b/tests/decorators/flags.spec.ts @@ -15,16 +15,16 @@ test.group('Base command | flags', () => { test('define flags using decorators', ({ assert }) => { class MakeModel extends BaseCommand { @flags.string() - connection?: string + declare connection?: string @flags.boolean() - dropAll?: boolean + declare dropAll?: boolean @flags.number() - batchSize?: number + declare batchSize?: number @flags.array() - files?: string[] + declare files?: string[] } assert.deepEqual(MakeModel.getParserOptions().flagsParserOptions, { diff --git a/tests/formatters/arg.spec.ts b/tests/formatters/arg.spec.ts index 7d792fd..3cc3bda 100644 --- a/tests/formatters/arg.spec.ts +++ b/tests/formatters/arg.spec.ts @@ -17,7 +17,7 @@ test.group('Formatters | arg', () => { test('format string arg', ({ assert }) => { class MakeController extends BaseCommand { @args.string() - name!: string + declare name: string } const formatter = new ArgumentFormatter(MakeController.args[0], colors.raw()) @@ -28,7 +28,7 @@ test.group('Formatters | arg', () => { test('format optional string arg', ({ assert }) => { class MakeController extends BaseCommand { @args.string({ required: false }) - name!: string + declare name: string } const formatter = new ArgumentFormatter(MakeController.args[0], colors.raw()) @@ -39,7 +39,7 @@ test.group('Formatters | arg', () => { test('format spread arg', ({ assert }) => { class MakeController extends BaseCommand { @args.spread() - name!: string[] + declare name: string[] } const formatter = new ArgumentFormatter(MakeController.args[0], colors.raw()) @@ -50,7 +50,7 @@ test.group('Formatters | arg', () => { test('format optional spread arg', ({ assert }) => { class MakeController extends BaseCommand { @args.spread({ required: false }) - name!: string[] + declare name: string[] } const formatter = new ArgumentFormatter(MakeController.args[0], colors.raw()) @@ -61,7 +61,7 @@ test.group('Formatters | arg', () => { test('format arg description', ({ assert }) => { class MakeController extends BaseCommand { @args.string({ description: 'The name of the controller' }) - name!: string + declare name: string } const formatter = new ArgumentFormatter(MakeController.args[0], colors.raw()) @@ -71,7 +71,7 @@ test.group('Formatters | arg', () => { test('format description with flag default value', ({ assert }) => { class MakeController extends BaseCommand { @args.string({ description: 'The name of the controller', default: 'posts' }) - name!: string + declare name: string } const formatter = new ArgumentFormatter(MakeController.args[0], colors.raw()) @@ -81,7 +81,7 @@ test.group('Formatters | arg', () => { test('format empty description with flag default value', ({ assert }) => { class MakeController extends BaseCommand { @args.string({ default: 'posts' }) - name!: string + declare name: string } const formatter = new ArgumentFormatter(MakeController.args[0], colors.raw()) diff --git a/tests/formatters/command.spec.ts b/tests/formatters/command.spec.ts index cc331fa..0a8f830 100644 --- a/tests/formatters/command.spec.ts +++ b/tests/formatters/command.spec.ts @@ -72,7 +72,7 @@ test.group('Formatters | command', () => { static description: string = 'Make an HTTP controller' @args.string() - name!: string + declare name: string } const formatter = new CommandFormatter(MakeController.serialize(), colors.raw()) @@ -85,10 +85,10 @@ test.group('Formatters | command', () => { static description: string = 'Make an HTTP controller' @args.string() - name!: string + declare name: string @flags.boolean() - resource!: boolean + declare resource: boolean } const formatter = new CommandFormatter(MakeController.serialize(), colors.raw()) @@ -103,7 +103,7 @@ test.group('Formatters | command', () => { static description: string = 'Make an HTTP controller' @flags.boolean() - resource!: boolean + declare resource: boolean } const formatter = new CommandFormatter(MakeController.serialize(), colors.raw()) @@ -116,10 +116,10 @@ test.group('Formatters | command', () => { static description: string = 'Make an HTTP controller' @args.string() - name!: string + declare name: string @flags.boolean() - resource!: boolean + declare resource: boolean } const formatter = new CommandFormatter(MakeController.serialize(), colors.raw()) @@ -135,10 +135,10 @@ test.group('Formatters | command', () => { static description: string = 'Make an HTTP controller' @args.string() - name!: string + declare name: string @flags.boolean() - resource!: boolean + declare resource: boolean } const formatter = new CommandFormatter(MakeController.serialize(), colors.raw()) @@ -153,10 +153,10 @@ test.group('Formatters | command', () => { static commandName: string = 'make:controller' @args.string() - name!: string + declare name: string @flags.boolean() - resource!: boolean + declare resource: boolean } const formatter = new CommandFormatter(MakeController.serialize(), colors.raw()) @@ -169,10 +169,10 @@ test.group('Formatters | command', () => { static help = 'Make a new HTTP controller make:controller ' @args.string() - name!: string + declare name: string @flags.boolean() - resource!: boolean + declare resource: boolean } const formatter = new CommandFormatter(MakeController.serialize(), colors.raw()) @@ -188,10 +188,10 @@ test.group('Formatters | command', () => { static help = 'Make a new HTTP controller {{binaryName}}make:controller ' @args.string() - name!: string + declare name: string @flags.boolean() - resource!: boolean + declare resource: boolean } const formatter = new CommandFormatter(MakeController.serialize(), colors.raw()) @@ -213,10 +213,10 @@ test.group('Formatters | command', () => { ] @args.string() - name!: string + declare name: string @flags.boolean() - resource!: boolean + declare resource: boolean } const formatter = new CommandFormatter(MakeController.serialize(), colors.raw()) diff --git a/tests/formatters/flag.spec.ts b/tests/formatters/flag.spec.ts index 538f90c..e1cd4d9 100644 --- a/tests/formatters/flag.spec.ts +++ b/tests/formatters/flag.spec.ts @@ -17,7 +17,7 @@ test.group('Formatters | flag', () => { test('format string flag name', ({ assert }) => { class MakeController extends BaseCommand { @flags.string() - connection!: string + declare connection: string } const formatter = new FlagFormatter(MakeController.flags[0], colors.raw()) @@ -27,7 +27,7 @@ test.group('Formatters | flag', () => { test('format required string flag name', ({ assert }) => { class MakeController extends BaseCommand { @flags.string({ required: true }) - connection!: string + declare connection: string } const formatter = new FlagFormatter(MakeController.flags[0], colors.raw()) @@ -37,7 +37,7 @@ test.group('Formatters | flag', () => { test('format flag with alias', ({ assert }) => { class MakeController extends BaseCommand { @flags.string({ required: true, alias: 'c' }) - connection!: string + declare connection: string } const formatter = new FlagFormatter(MakeController.flags[0], colors.raw()) @@ -47,7 +47,7 @@ test.group('Formatters | flag', () => { test('format flag with mutliple aliases', ({ assert }) => { class MakeController extends BaseCommand { @flags.string({ required: true, alias: ['c', 'o'] }) - connection!: string + declare connection: string } const formatter = new FlagFormatter(MakeController.flags[0], colors.raw()) @@ -57,7 +57,7 @@ test.group('Formatters | flag', () => { test('show negated flag', ({ assert }) => { class MakeController extends BaseCommand { @flags.boolean({ required: true, showNegatedVariantInHelp: true }) - resource!: boolean + declare resource: boolean } const formatter = new FlagFormatter(MakeController.flags[0], colors.raw()) @@ -67,7 +67,7 @@ test.group('Formatters | flag', () => { test('format array flag name', ({ assert }) => { class MakeController extends BaseCommand { @flags.array() - connections!: string[] + declare connections: string[] } const formatter = new FlagFormatter(MakeController.flags[0], colors.raw()) @@ -77,7 +77,7 @@ test.group('Formatters | flag', () => { test('format array flag with aliases', ({ assert }) => { class MakeController extends BaseCommand { @flags.array({ alias: ['c'] }) - connections!: string[] + declare connections: string[] } const formatter = new FlagFormatter(MakeController.flags[0], colors.raw()) @@ -87,7 +87,7 @@ test.group('Formatters | flag', () => { test('format required array flag name', ({ assert }) => { class MakeController extends BaseCommand { @flags.array({ required: true }) - connections!: string[] + declare connections: string[] } const formatter = new FlagFormatter(MakeController.flags[0], colors.raw()) @@ -97,7 +97,7 @@ test.group('Formatters | flag', () => { test('format numeric flag name', ({ assert }) => { class MakeController extends BaseCommand { @flags.number() - actions!: number + declare actions: number } const formatter = new FlagFormatter(MakeController.flags[0], colors.raw()) @@ -107,7 +107,7 @@ test.group('Formatters | flag', () => { test('format numeric flag with alias', ({ assert }) => { class MakeController extends BaseCommand { @flags.number({ alias: 'a' }) - actions!: number + declare actions: number } const formatter = new FlagFormatter(MakeController.flags[0], colors.raw()) @@ -117,7 +117,7 @@ test.group('Formatters | flag', () => { test('format required numeric flag name', ({ assert }) => { class MakeController extends BaseCommand { @flags.number({ required: true }) - actions!: number + declare actions: number } const formatter = new FlagFormatter(MakeController.flags[0], colors.raw()) @@ -127,7 +127,7 @@ test.group('Formatters | flag', () => { test('format boolean flag name', ({ assert }) => { class MakeController extends BaseCommand { @flags.boolean() - resource!: boolean + declare resource: boolean } const formatter = new FlagFormatter(MakeController.flags[0], colors.raw()) @@ -137,7 +137,7 @@ test.group('Formatters | flag', () => { test('format boolean flag with alias', ({ assert }) => { class MakeController extends BaseCommand { @flags.boolean({ alias: ['r'] }) - resource!: boolean + declare resource: boolean } const formatter = new FlagFormatter(MakeController.flags[0], colors.raw()) @@ -147,7 +147,7 @@ test.group('Formatters | flag', () => { test('format required boolean flag name', ({ assert }) => { class MakeController extends BaseCommand { @flags.boolean({ required: true }) - resource!: boolean + declare resource: boolean } const formatter = new FlagFormatter(MakeController.flags[0], colors.raw()) @@ -157,7 +157,7 @@ test.group('Formatters | flag', () => { test('format description', ({ assert }) => { class MakeController extends BaseCommand { @flags.boolean({ description: 'Generate resource actions' }) - resource!: boolean + declare resource: boolean } const formatter = new FlagFormatter(MakeController.flags[0], colors.raw()) @@ -167,7 +167,7 @@ test.group('Formatters | flag', () => { test('format description with flag default value', ({ assert }) => { class MakeController extends BaseCommand { @flags.boolean({ description: 'Generate resource actions', default: true }) - resource!: boolean + declare resource: boolean } const formatter = new FlagFormatter(MakeController.flags[0], colors.raw()) @@ -177,7 +177,7 @@ test.group('Formatters | flag', () => { test('format empty description with flag default value', ({ assert }) => { class MakeController extends BaseCommand { @flags.boolean({ default: true }) - resource!: boolean + declare resource: boolean } const formatter = new FlagFormatter(MakeController.flags[0], colors.raw()) From 7992de9059ddd30fef2b8c609159a155debbddcb Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 26 Jan 2023 13:46:45 +0530 Subject: [PATCH 014/112] refactor: remove template methods prepare, interact and completed --- src/commands/base.ts | 44 +------ tests/base_command/exec.spec.ts | 224 +------------------------------- tests/kernel/exec.spec.ts | 22 +--- tests/kernel/handle.spec.ts | 26 ---- tests/kernel/main.spec.ts | 5 + 5 files changed, 16 insertions(+), 305 deletions(-) diff --git a/src/commands/base.ts b/src/commands/base.ts index d47d6f8..7f5144d 100644 --- a/src/commands/base.ts +++ b/src/commands/base.ts @@ -445,49 +445,17 @@ export class BaseCommand { }) } - /** - * The prepare template method is used to prepare the - * state for the command. This is the first method - * executed on a given command instance. - */ - async prepare() {} - - /** - * The interact template method is used to display the prompts - * to the user. The method is called after the prepare - * method. - */ - async interact() {} - /** * The run method should include the implementation for the * command. */ - async run(): Promise {} - - /** - * The completed method is the method invoked after the command - * finishes or results in an error. - * - * You can access the command error using the `this.error` property. - * Returning `true` from completed method supresses the error - * reporting to the kernel layer. - */ - async completed(): Promise {} + async run(..._: any[]): Promise {} /** - * Executes the commands by running the command template methods. - * The following methods are executed in order they are mentioned. - * - * - prepare - * - interact - * - run - * - completed (runs regardless of error) + * Executes the commands by running the command's run method. */ async exec() { try { - await this.prepare() - await this.interact() this.result = await this.run() this.exitCode = this.exitCode ?? 0 } catch (error) { @@ -495,13 +463,7 @@ export class BaseCommand { this.exitCode = this.exitCode ?? 1 } - const errorHandled = await this.completed() - - /** - * Print the error if the completed method has not - * handled it already - */ - if (!errorHandled && this.error) { + if (this.error) { this.logger.fatal(this.error) } diff --git a/tests/base_command/exec.spec.ts b/tests/base_command/exec.spec.ts index 5cf044e..1f6e8d8 100644 --- a/tests/base_command/exec.spec.ts +++ b/tests/base_command/exec.spec.ts @@ -14,30 +14,15 @@ import { Kernel } from '../../src/kernel.js' import { BaseCommand } from '../../src/commands/base.js' test.group('Base command | execute', () => { - test('execute command and its template methods', async ({ assert }) => { + test('execute command', async ({ assert }) => { class MakeModel extends BaseCommand { name!: string connection!: string stack: string[] = [] - async prepare() { - this.stack.push('prepare') - super.prepare() - } - - async interact() { - this.stack.push('interact') - super.interact() - } - - async completed() { - this.stack.push('completed') - super.completed() - } - async run() { this.stack.push('run') - super.run() + return super.run() } } @@ -48,29 +33,15 @@ test.group('Base command | execute', () => { const model = await kernel.create(MakeModel, ['user', '--connection=sqlite']) await model.exec() - assert.deepEqual(model.stack, ['prepare', 'interact', 'run', 'completed']) + assert.deepEqual(model.stack, ['run']) }) test('store run method return value in the result property', async ({ assert }) => { class MakeModel extends BaseCommand { name!: string connection!: string - stack: string[] = [] - - async prepare() { - this.stack.push('prepare') - } - - async interact() { - this.stack.push('interact') - } - - async completed() { - this.stack.push('completed') - } async run() { - this.stack.push('run') return 'completed' } } @@ -83,7 +54,6 @@ test.group('Base command | execute', () => { const model = await kernel.create(MakeModel, ['user', '--connection=sqlite']) await model.exec() - assert.deepEqual(model.stack, ['prepare', 'interact', 'run', 'completed']) assert.equal(model.result, 'completed') }) @@ -92,7 +62,7 @@ test.group('Base command | execute', () => { name!: string connection!: string - async interact() { + async run() { if (!this.name) { this.name = await this.prompt.ask('Enter model name') } @@ -104,8 +74,6 @@ test.group('Base command | execute', () => { ]) } } - - async run() {} } MakeModel.defineArgument('name', { type: 'string', required: false }) @@ -126,136 +94,6 @@ test.group('Base command | execute', () => { }) }) -test.group('Base command | execute | prepare fails', () => { - test('fail command when prepare method fails', async ({ assert }) => { - class MakeModel extends BaseCommand { - name!: string - connection!: string - stack: string[] = [] - - async prepare() { - throw new Error('Something went wrong') - } - - async run() { - return 'completed' - } - } - - MakeModel.defineArgument('name', { type: 'string' }) - MakeModel.defineFlag('connection', { type: 'string' }) - - const kernel = Kernel.create() - kernel.ui = cliui({ mode: 'raw' }) - const model = await kernel.create(MakeModel, ['user', '--connection=sqlite']) - - await model.exec() - assert.isUndefined(model.result) - assert.equal(model.error?.message, 'Something went wrong') - assert.lengthOf(model.ui.logger.getRenderer().getLogs(), 1) - assert.equal(model.exitCode, 1) - }) - - test('run completed template method when prepare method fails', async ({ assert }) => { - class MakeModel extends BaseCommand { - name!: string - connection!: string - stack: string[] = [] - - async prepare() { - this.stack.push('prepare') - throw new Error('Something went wrong') - } - - async interact() { - this.stack.push('interact') - } - - async completed() { - this.stack.push('completed') - } - - async run() { - this.stack.push('run') - return 'completed' - } - } - - MakeModel.defineArgument('name', { type: 'string' }) - MakeModel.defineFlag('connection', { type: 'string' }) - - const kernel = Kernel.create() - kernel.ui = cliui({ mode: 'raw' }) - const model = await kernel.create(MakeModel, ['user', '--connection=sqlite']) - - await model.exec() - assert.deepEqual(model.stack, ['prepare', 'completed']) - }) -}) - -test.group('Base command | execute | intertact fails', () => { - test('fail command when intertact method fails', async ({ assert }) => { - class MakeModel extends BaseCommand { - name!: string - connection!: string - stack: string[] = [] - - async interact() { - throw new Error('Something went wrong') - } - - async run() { - return 'completed' - } - } - - MakeModel.defineArgument('name', { type: 'string' }) - MakeModel.defineFlag('connection', { type: 'string' }) - - const kernel = Kernel.create() - kernel.ui = cliui({ mode: 'raw' }) - const model = await kernel.create(MakeModel, ['user', '--connection=sqlite']) - - await model.exec() - assert.isUndefined(model.result) - assert.equal(model.error?.message, 'Something went wrong') - assert.lengthOf(model.ui.logger.getRenderer().getLogs(), 1) - assert.equal(model.exitCode, 1) - }) - - test('run completed template method when intertact method fails', async ({ assert }) => { - class MakeModel extends BaseCommand { - name!: string - connection!: string - stack: string[] = [] - - async interact() { - this.stack.push('interact') - throw new Error('Something went wrong') - } - - async completed() { - this.stack.push('completed') - } - - async run() { - this.stack.push('run') - return 'completed' - } - } - - MakeModel.defineArgument('name', { type: 'string' }) - MakeModel.defineFlag('connection', { type: 'string' }) - - const kernel = Kernel.create() - kernel.ui = cliui({ mode: 'raw' }) - const model = await kernel.create(MakeModel, ['user', '--connection=sqlite']) - - await model.exec() - assert.deepEqual(model.stack, ['interact', 'completed']) - }) -}) - test.group('Base command | execute | run fails', () => { test('fail command when run method fails', async ({ assert }) => { class MakeModel extends BaseCommand { @@ -281,58 +119,4 @@ test.group('Base command | execute | run fails', () => { assert.lengthOf(model.ui.logger.getRenderer().getLogs(), 1) assert.equal(model.exitCode, 1) }) - - test('run completed template method when run method fails', async ({ assert }) => { - class MakeModel extends BaseCommand { - name!: string - connection!: string - stack: string[] = [] - - async completed() { - this.stack.push('completed') - } - - async run() { - this.stack.push('run') - throw new Error('Something went wrong') - } - } - - MakeModel.defineArgument('name', { type: 'string' }) - MakeModel.defineFlag('connection', { type: 'string' }) - - const kernel = Kernel.create() - kernel.ui = cliui({ mode: 'raw' }) - const model = await kernel.create(MakeModel, ['user', '--connection=sqlite']) - - await model.exec() - assert.deepEqual(model.stack, ['run', 'completed']) - }) -}) - -test.group('Base command | execute | complete method', () => { - test('do not report command error if complete method handles it', async ({ assert }) => { - class MakeModel extends BaseCommand { - name!: string - connection!: string - - async completed() { - return true - } - - async run() { - throw new Error('Something went wrong') - } - } - - MakeModel.defineArgument('name', { type: 'string' }) - MakeModel.defineFlag('connection', { type: 'string' }) - - const kernel = Kernel.create() - kernel.ui = cliui({ mode: 'raw' }) - const model = await kernel.create(MakeModel, ['user', '--connection=sqlite']) - - await model.exec() - assert.lengthOf(model.ui.logger.getRenderer().getLogs(), 0) - }) }) diff --git a/tests/kernel/exec.spec.ts b/tests/kernel/exec.spec.ts index 4afac2d..b56aaeb 100644 --- a/tests/kernel/exec.spec.ts +++ b/tests/kernel/exec.spec.ts @@ -34,7 +34,7 @@ test.group('Kernel | exec', () => { assert.equal(kernel.getState(), 'booted') }) - test('run executing and executed hooks', async ({ assert, expectTypeOf }) => { + test('run executing and executed hooks', async ({ assert }) => { const kernel = Kernel.create() const stack: string[] = [] @@ -46,12 +46,10 @@ test.group('Kernel | exec', () => { } kernel.addLoader(new ListLoader([MakeController])) - kernel.executing((cmd) => { - expectTypeOf(cmd).toEqualTypeOf() + kernel.executing(() => { stack.push('executing') }) - kernel.executed((cmd) => { - expectTypeOf(cmd).toEqualTypeOf() + kernel.executed(() => { stack.push('executed') }) @@ -146,18 +144,6 @@ test.group('Kernel | exec', () => { name!: string connection!: string - async prepare() { - stack.push('prepare') - } - - async interact() { - stack.push('interact') - } - - async completed() { - stack.push('completed') - } - async run() { stack.push('run') } @@ -180,7 +166,7 @@ test.group('Kernel | exec', () => { kernel.addLoader(new ListLoader([MakeModel])) await kernel.exec('make:model', ['users']) - assert.deepEqual(stack, ['creating', 'running', 'prepare', 'interact', 'run', 'completed']) + assert.deepEqual(stack, ['creating', 'running', 'run']) assert.isUndefined(kernel.exitCode) assert.equal(kernel.getState(), 'booted') }) diff --git a/tests/kernel/handle.spec.ts b/tests/kernel/handle.spec.ts index d138c21..17137fe 100644 --- a/tests/kernel/handle.spec.ts +++ b/tests/kernel/handle.spec.ts @@ -117,32 +117,6 @@ test.group('Kernel | handle', (group) => { assert.deepEqual(stack, ['executing', 'run', 'executed']) }) - test('report error when command completed method fails', async ({ assert }) => { - const kernel = Kernel.create() - const stack: string[] = [] - - class MakeController extends BaseCommand { - static commandName = 'make:controller' - async run() { - stack.push('run') - return 'executed' - } - - async completed(): Promise { - throw new Error('Something went wrong') - } - } - MakeController.defineArgument('name', { type: 'string' }) - - kernel.addLoader(new ListLoader([MakeController])) - - await kernel.handle(['make:controller', 'users']) - - assert.equal(kernel.getState(), 'completed') - assert.equal(kernel.exitCode, 1) - assert.deepEqual(stack, ['run']) - }) - test('disallow calling handle method twice in parallel', async ({ assert }) => { const kernel = Kernel.create() diff --git a/tests/kernel/main.spec.ts b/tests/kernel/main.spec.ts index 5fc1136..6acebf8 100644 --- a/tests/kernel/main.spec.ts +++ b/tests/kernel/main.spec.ts @@ -302,4 +302,9 @@ test.group('Kernel', () => { assert.deepEqual(kernel.getNamespaceSuggestions('migrate'), ['migration']) }) + + test('return true when shorthand shortcircuit method', async ({ assert }) => { + const kernel = Kernel.create() + assert.isTrue(kernel.shortcircuit()) + }) }) From 1580a1aaffd6366f6c05e66301c65c748660a89c Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 26 Jan 2023 15:39:33 +0530 Subject: [PATCH 015/112] feat: add importPath to command metadata import using FsLoader --- src/loaders/fs_loader.ts | 51 +++++++++++++++++++++++++++------ src/loaders/list_loader.ts | 3 +- src/loaders/modules_loader.ts | 15 +++++----- tests/loaders/fs_loader.spec.ts | 49 ++++++++++++++++++++++++++++++- 4 files changed, 100 insertions(+), 18 deletions(-) diff --git a/src/loaders/fs_loader.ts b/src/loaders/fs_loader.ts index aa2b4c3..22d730d 100644 --- a/src/loaders/fs_loader.ts +++ b/src/loaders/fs_loader.ts @@ -7,14 +7,20 @@ * file that was distributed with this source code. */ -import { extname } from 'node:path' -import { fsImportAll } from '@poppinss/utils' +import { fileURLToPath } from 'node:url' +import { extname, relative } from 'node:path' +import { fsReadAll, RuntimeException } from '@poppinss/utils' import { validateCommand } from '../helpers.js' import type { AbstractBaseCommand, CommandMetaData, LoadersContract } from '../types.js' const JS_MODULES = ['.js', '.cjs', '.mjs'] +/** + * Fs loader exposes the API to load commands from a directory. All files + * ending with ".js", ".cjs", ".mjs", ".ts" and ".mts" are considered + * as commands + */ export class FsLoader implements LoadersContract { /** * Absolute path to directory from which to load files @@ -24,7 +30,7 @@ export class FsLoader implements LoadersCon /** * An array of loaded commands */ - #commands: Command[] = [] + #commands: { command: Command; filePath: string }[] = [] constructor(comandsDirectory: string) { this.#comandsDirectory = comandsDirectory @@ -35,7 +41,13 @@ export class FsLoader implements LoadersCon * is unknown and must be validated */ async #loadCommands(): Promise> { - return fsImportAll(this.#comandsDirectory, { + const commands: Record = {} + + /** + * Scanning all files + */ + const commandFiles = await fsReadAll(this.#comandsDirectory, { + pathType: 'url', filter: (filePath: string) => { const ext = extname(filePath) if (JS_MODULES.includes(ext)) { @@ -49,21 +61,41 @@ export class FsLoader implements LoadersCon return false }, }) + + /** + * Importing files and validating the exports to have a default + * export + */ + for (let file of commandFiles) { + const fileRelativeName = relative(this.#comandsDirectory, fileURLToPath(file)) + const commandFileExports = await import(file) + if (!commandFileExports.default) { + throw new RuntimeException( + `Invalid command exported from "${fileRelativeName}" file. Missing export default` + ) + } + + commands[fileRelativeName] = commandFileExports.default + } + + return commands } /** * Returns the metadata of commands */ - async getMetaData(): Promise { + async getMetaData(): Promise<(CommandMetaData & { filePath: string })[]> { const commandsCollection = await this.#loadCommands() Object.keys(commandsCollection).forEach((key) => { const command = commandsCollection[key] validateCommand(command, `"${key}" file`) - this.#commands.push(command) + this.#commands.push({ command, filePath: key }) }) - return this.#commands.map((command) => command.serialize()) + return this.#commands.map(({ command, filePath }) => { + return Object.assign({}, command.serialize(), { filePath }) + }) } /** @@ -71,6 +103,9 @@ export class FsLoader implements LoadersCon * is returned when unable to lookup the command */ async getCommand(metaData: CommandMetaData): Promise { - return this.#commands.find((command) => command.commandName === metaData.commandName) || null + return ( + this.#commands.find(({ command }) => command.commandName === metaData.commandName)?.command || + null + ) } } diff --git a/src/loaders/list_loader.ts b/src/loaders/list_loader.ts index 0ee325b..d87e99f 100644 --- a/src/loaders/list_loader.ts +++ b/src/loaders/list_loader.ts @@ -10,8 +10,7 @@ import type { AbstractBaseCommand, CommandMetaData, LoadersContract } from '../types.js' /** - * The CommandsList loader registers commands classes with the kernel. - * The commands are kept within memory + * List loader exposes the API to register commands as classes */ export class ListLoader implements LoadersContract { #commands: Command[] diff --git a/src/loaders/modules_loader.ts b/src/loaders/modules_loader.ts index 7edb90a..91ead7e 100644 --- a/src/loaders/modules_loader.ts +++ b/src/loaders/modules_loader.ts @@ -15,17 +15,16 @@ import type { AbstractBaseCommand, CommandMetaData, LoadersContract } from '../t /** * Module based command loader must implement the following methods. */ -type CommandsLoader = { +interface CommandsModuleLoader { list: () => Promise load: (command: CommandMetaData) => Promise } /** * Modules loader exposes the API to lazy load commands from - * one or more ES modules. + * ES modules. * - * The modules have to implement the `list` and the `load` - * methods + * The modules have to implement the [[CommandsModuleLoader]] interface */ export class ModulesLoader implements LoadersContract @@ -45,7 +44,7 @@ export class ModulesLoader * A collection of commands with their loaders. The key is the * command name and the value is the imported loader. */ - #commandsLoaders?: Map + #commandsLoaders?: Map constructor(importRoot: string | URL, commandSources: string[]) { this.#importRoot = importRoot @@ -55,7 +54,9 @@ export class ModulesLoader /** * Imports the source by first resolving its import path. */ - async #importSource(sourcePath: string): Promise<{ loader: CommandsLoader; importPath: string }> { + async #importSource( + sourcePath: string + ): Promise<{ loader: CommandsModuleLoader; importPath: string }> { const importPath = await import.meta.resolve!(sourcePath, this.#importRoot) const loader = await import(importPath) @@ -83,7 +84,7 @@ export class ModulesLoader /** * Returns an array of commands returns by the loader list method */ - async #getCommandsList(loader: CommandsLoader, importPath: string): Promise { + async #getCommandsList(loader: CommandsModuleLoader, importPath: string): Promise { const list = await loader.list() if (!Array.isArray(list)) { throw new RuntimeException( diff --git a/tests/loaders/fs_loader.spec.ts b/tests/loaders/fs_loader.spec.ts index f7237d1..25e8af4 100644 --- a/tests/loaders/fs_loader.spec.ts +++ b/tests/loaders/fs_loader.spec.ts @@ -37,7 +37,7 @@ test.group('Loaders | fs', (group) => { const loader = new FsLoader(join(BASE_PATH, './commands')) await assert.rejects( () => loader.getMetaData(), - 'Invalid command exported from "make_controller_v_1" file. Expected command to be a class' + 'Invalid command exported from "make_controller_v_1.ts" file. Missing export default' ) }) @@ -73,6 +73,7 @@ test.group('Loaders | fs', (group) => { const commands = await loader.getMetaData() assert.deepEqual(commands, [ { + filePath: 'make_controller_v_2.ts', commandName: 'make:controller', description: '', namespace: 'make', @@ -117,6 +118,7 @@ test.group('Loaders | fs', (group) => { assert.deepEqual(commands, [ { commandName: 'make:controller', + filePath: 'make_controller.js', description: '', namespace: 'make', args: [], @@ -162,6 +164,7 @@ test.group('Loaders | fs', (group) => { assert.deepEqual(commands, [ { commandName: 'make:controller', + filePath: 'make_controller_v_3.ts', description: '', namespace: 'make', args: [], @@ -213,4 +216,48 @@ test.group('Loaders | fs', (group) => { const command = await loader.getCommand({ commandName: 'make:model' } as any) assert.isNull(command) }) + + test('load commands from nested directories', async ({ assert }) => { + await fs.outputFile( + join(BASE_PATH, 'commands', 'make', 'controller.ts'), + ` + export default class MakeController { + static commandName = 'make:controller' + static args = [] + static flags = [] + static aliases = [] + static options = {} + static description = '' + static namespace = 'make' + + static serialize() { + return { + commandName: this.commandName, + description: this.description, + namespace: this.namespace, + args: this.args, + flags: this.flags, + options: this.options, + aliases: this.aliases, + } + } + } + ` + ) + + const loader = new FsLoader(join(BASE_PATH, './commands')) + const commands = await loader.getMetaData() + assert.deepEqual(commands, [ + { + commandName: 'make:controller', + filePath: 'make/controller.ts', + description: '', + namespace: 'make', + args: [], + flags: [], + options: {}, + aliases: [], + }, + ]) + }) }) From 99c10e9568db4230cda5f0502bb4e3d7c5a1a78c Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 26 Jan 2023 15:47:54 +0530 Subject: [PATCH 016/112] refactor: cleanup modules loader source --- src/loaders/modules_loader.ts | 52 ++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/src/loaders/modules_loader.ts b/src/loaders/modules_loader.ts index 91ead7e..dc67d79 100644 --- a/src/loaders/modules_loader.ts +++ b/src/loaders/modules_loader.ts @@ -42,9 +42,10 @@ export class ModulesLoader /** * A collection of commands with their loaders. The key is the - * command name and the value is the imported loader. + * command name and the value is the loader that loaded the + * command. */ - #commandsLoaders?: Map + #commands?: Map constructor(importRoot: string | URL, commandSources: string[]) { this.#importRoot = importRoot @@ -52,11 +53,9 @@ export class ModulesLoader } /** - * Imports the source by first resolving its import path. + * Imports the loader from the loader source path. */ - async #importSource( - sourcePath: string - ): Promise<{ loader: CommandsModuleLoader; importPath: string }> { + async #importLoader(sourcePath: string): Promise { const importPath = await import.meta.resolve!(sourcePath, this.#importRoot) const loader = await import(importPath) @@ -78,7 +77,7 @@ export class ModulesLoader ) } - return { loader, importPath } + return loader } /** @@ -86,6 +85,7 @@ export class ModulesLoader */ async #getCommandsList(loader: CommandsModuleLoader, importPath: string): Promise { const list = await loader.list() + if (!Array.isArray(list)) { throw new RuntimeException( `Invalid commands list. The "${importPath}.list" method must return an array of commands` @@ -99,23 +99,39 @@ export class ModulesLoader * Loads commands from the registered sources */ async #loadCommands() { - this.#commandsLoaders = new Map() + this.#commands = new Map() const commands: CommandMetaData[] = [] for (let sourcePath of this.#commandSources) { - const { loader } = await this.#importSource(sourcePath) + const loader = await this.#importLoader(sourcePath) const list = await this.#getCommandsList(loader, sourcePath) list.forEach((metaData) => { validCommandMetaData(metaData, `"${sourcePath}.list" method`) commands.push(metaData) - this.#commandsLoaders!.set(metaData.commandName, { loader, sourcePath }) + this.#commands!.set(metaData.commandName, { loader, sourcePath }) }) } return commands } + /** + * Returns the loader for a given command + */ + async #getCommandLoader(commandName: string) { + if (!this.#commands) { + await this.#loadCommands() + } + + const loader = this.#commands!.get(commandName) + if (!loader) { + return null + } + + return loader + } + /** * Returns an array of command's metadata */ @@ -128,25 +144,17 @@ export class ModulesLoader * is returned when unable to lookup the command */ async getCommand(metaData: CommandMetaData): Promise { - /** - * Running "loadCommands" method instantiates the commands loader - * collection - */ - if (!this.#commandsLoaders) { - await this.#loadCommands() - } - - const commandLoader = this.#commandsLoaders!.get(metaData.commandName) - if (!commandLoader) { + const loader = await this.#getCommandLoader(metaData.commandName) + if (!loader) { return null } - const command = await commandLoader.loader.load(metaData) + const command = await loader.loader.load(metaData) if (command === null || command === undefined) { return null } - validateCommand(command, `"${commandLoader.sourcePath}.load" method`) + validateCommand(command, `"${loader.sourcePath}.load" method`) return command } } From d0816b0570e40c9f4413ed4097f675b44c75328b Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 27 Jan 2023 21:31:08 +0530 Subject: [PATCH 017/112] refactor: remove module and add toolkit commands --- commands/index_command.ts | 27 ++ commands/main.ts | 17 + index.ts | 2 +- package.json | 22 +- .../command_metadata_schema.json | 7 +- src/commands/base.ts | 6 +- src/generators/index_generator.ts | 52 +++ src/helpers.ts | 13 +- src/kernel.ts | 22 +- src/loaders/fs_loader.ts | 35 +- src/loaders/modules_loader.ts | 160 -------- src/types.ts | 2 +- stubs/commands_loader.stub | 36 ++ {toolkit => stubs}/main.ts | 5 +- tests/helpers.spec.ts | 92 ++++- tests/index_generator.spec.ts | 79 ++++ tests/kernel/loaders.spec.ts | 21 + tests/loaders/fs_loader.spec.ts | 82 +++- tests/loaders/modules_loader.spec.ts | 360 ------------------ toolkit/commands/index_command/main.ts | 26 -- .../index_command/stubs/module_loader.stub | 18 - 21 files changed, 471 insertions(+), 613 deletions(-) create mode 100644 commands/index_command.ts create mode 100644 commands/main.ts rename command_metadata_schema.json => schemas/command_metadata_schema.json (99%) create mode 100644 src/generators/index_generator.ts delete mode 100644 src/loaders/modules_loader.ts create mode 100644 stubs/commands_loader.stub rename {toolkit => stubs}/main.ts (64%) create mode 100644 tests/index_generator.spec.ts delete mode 100644 tests/loaders/modules_loader.spec.ts delete mode 100644 toolkit/commands/index_command/main.ts delete mode 100644 toolkit/commands/index_command/stubs/module_loader.stub diff --git a/commands/index_command.ts b/commands/index_command.ts new file mode 100644 index 0000000..c37512a --- /dev/null +++ b/commands/index_command.ts @@ -0,0 +1,27 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { join } from 'node:path' +import { args, BaseCommand, IndexGenerator } from '../index.js' + +/** + * Generates index of commands with a loader. Must be called against + * the TypeScript compiled output. + */ +export default class IndexCommand extends BaseCommand { + static commandName = 'index' + static description: string = 'Create an index of commands along with a lazy loader' + + @args.string({ description: 'Relative path from cwd to the commands directory' }) + declare commandsDir: string + + async run(): Promise { + await new IndexGenerator(join(process.cwd(), this.commandsDir)).generate() + } +} diff --git a/commands/main.ts b/commands/main.ts new file mode 100644 index 0000000..ae0333d --- /dev/null +++ b/commands/main.ts @@ -0,0 +1,17 @@ +#!/usr/bin/env node + +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Kernel, ListLoader } from '../index.js' +import IndexCommand from './index_command.js' + +const kernel = Kernel.create() +kernel.addLoader(new ListLoader([IndexCommand])) +await kernel.handle(process.argv.splice(2)) diff --git a/index.ts b/index.ts index 705052f..c6cfd90 100644 --- a/index.ts +++ b/index.ts @@ -17,4 +17,4 @@ export { HelpCommand } from './src/commands/help.js' export { ListCommand } from './src/commands/list.js' export { FsLoader } from './src/loaders/fs_loader.js' export { ListLoader } from './src/loaders/list_loader.js' -export { ModulesLoader } from './src/loaders/modules_loader.js' +export { IndexGenerator } from './src/generators/index_generator.js' diff --git a/package.json b/package.json index 98d8146..4b58d20 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,10 @@ "main": "build/index.js", "type": "module", "files": [ + "build/commands", + "build/schemas", "build/src", - "build/command_metadata_schema.json", + "build/stubs", "build/index.d.ts", "build/index.js" ], @@ -14,13 +16,18 @@ ".": "./build/index.js", "./types": "./build/src/types.js" }, + "bin": { + "ace-toolkit": "./build/commands/main.js" + }, "scripts": { "pretest": "npm run lint", "test": "cross-env NODE_DEBUG=adonisjs:ace c8 npm run vscode:test", "clean": "del-cli build", - "build:schema": "ts-json-schema-generator --path='src/types.ts' --type='CommandMetaData' --tsconfig='tsconfig.json' --out='command_metadata_schema.json'", - "copy:files": "copyfiles command_metadata_schema.json build", - "compile": "npm run lint && npm run clean && tsc && npm run build:schema && npm run copy:files", + "build:schema": "ts-json-schema-generator --path='src/types.ts' --type='CommandMetaData' --tsconfig='tsconfig.json' --out='schemas/command_metadata_schema.json'", + "copy:files": "copyfiles schemas/* stubs/*.stub build", + "precompile": "npm run lint && npm run clean", + "compile": "tsc", + "postcompile": "npm run build:schema && npm run copy:files", "build": "npm run compile", "release": "np", "version": "npm run build", @@ -28,7 +35,7 @@ "lint": "eslint . --ext=.ts", "format": "prettier --write .", "sync-labels": "github-label-sync --labels .github/labels.json adonisjs/ace", - "vscode:test": "node --loader=ts-node/esm --experimental-import-meta-resolve bin/test.ts" + "vscode:test": "node --loader=ts-node/esm bin/test.ts" }, "keywords": [ "adonisjs", @@ -41,8 +48,9 @@ "dependencies": { "@poppinss/cliui": "^6.1.1-0", "@poppinss/hooks": "^7.1.1-0", + "@poppinss/macroable": "^1.0.0-2", "@poppinss/prompts": "^3.1.0-0", - "@poppinss/utils": "^6.4.0-0", + "@poppinss/utils": "^6.5.0-0", "jsonschema": "^1.4.1", "string-similarity": "^4.0.4", "string-width": "^5.1.2", @@ -57,7 +65,7 @@ "@japa/runner": "^2.1.1", "@japa/spec-reporter": "^1.2.0", "@poppinss/dev-utils": "^2.0.3", - "@swc/core": "^1.3.28", + "@swc/core": "^1.3.29", "@types/fs-extra": "^11.0.1", "@types/node": "^18.7.15", "@types/sinon": "^10.0.13", diff --git a/command_metadata_schema.json b/schemas/command_metadata_schema.json similarity index 99% rename from command_metadata_schema.json rename to schemas/command_metadata_schema.json index ef6c84c..ab84ed3 100644 --- a/command_metadata_schema.json +++ b/schemas/command_metadata_schema.json @@ -3,7 +3,6 @@ "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "CommandMetaData": { - "additionalProperties": false, "description": "Command metdata required to display command help.", "properties": { "aliases": { @@ -145,12 +144,12 @@ } }, "required": [ + "aliases", + "args", "commandName", "description", - "namespace", - "aliases", "flags", - "args", + "namespace", "options" ], "type": "object" diff --git a/src/commands/base.ts b/src/commands/base.ts index 7f5144d..fc42fcd 100644 --- a/src/commands/base.ts +++ b/src/commands/base.ts @@ -8,10 +8,12 @@ */ import string from '@poppinss/utils/string' +import Macroable from '@poppinss/macroable' import lodash from '@poppinss/utils/lodash' import type { Prompt } from '@poppinss/prompts' import { defineStaticProperty, InvalidArgumentsException } from '@poppinss/utils' +import debug from '../debug.js' import * as errors from '../errors.js' import type { Kernel } from '../kernel.js' import type { @@ -24,13 +26,12 @@ import type { FlagsParserOptions, ArgumentsParserOptions, } from '../types.js' -import debug from '../debug.js' /** * The base command sets the foundation for defining ace commands. * Every command should inherit from the base command. */ -export class BaseCommand { +export class BaseCommand extends Macroable { static booted: boolean = false /** @@ -411,6 +412,7 @@ export class BaseCommand { public ui: UIPrimitives, public prompt: Prompt ) { + super() this.#consumeParsedOutput() } diff --git a/src/generators/index_generator.ts b/src/generators/index_generator.ts new file mode 100644 index 0000000..9ff5fba --- /dev/null +++ b/src/generators/index_generator.ts @@ -0,0 +1,52 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { join } from 'node:path' +import { copyFile, mkdir, writeFile } from 'node:fs/promises' + +import { stubsRoot } from '../../stubs/main.js' +import { FsLoader } from '../loaders/fs_loader.js' + +/** + * The index generators creates a commands laoder that can be lazily + * imported. + * + * Also, a command.json index file is created that has metadata for all + * the files. Doing so, speeds up the commands lookup, as we do not + * have to import all the classes just to find if a command exists + * or not. + */ +export class IndexGenerator { + #commandsDir: string + + constructor(commandsDir: string) { + this.#commandsDir = commandsDir + } + + /** + * Generate index + */ + async generate(): Promise { + const commandsMetaData = await new FsLoader(this.#commandsDir, ['main.js']).getMetaData() + const indexJSON = JSON.stringify({ commands: commandsMetaData, version: 1 }) + const indexFile = join(this.#commandsDir, 'commands.json') + + const loaderFile = join(this.#commandsDir, 'main.js') + const loaderStub = join(stubsRoot, 'commands_loader.stub') + + await mkdir(this.#commandsDir, { recursive: true }) + console.log(`artifacts directory: ${this.#commandsDir}`) + + await writeFile(indexFile, indexJSON) + console.log('create commands.json') + + await copyFile(loaderStub, loaderFile) + console.log('create main.js') + } +} diff --git a/src/helpers.ts b/src/helpers.ts index 754bc3d..a887731 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -7,10 +7,9 @@ * file that was distributed with this source code. */ -import { inspect } from 'node:util' import { Validator } from 'jsonschema' import { RuntimeException } from '@poppinss/utils' -import schema from '../command_metadata_schema.json' assert { type: 'json' } +import schema from '../schemas/command_metadata_schema.json' assert { type: 'json' } import type { AbstractBaseCommand, CommandMetaData, UIPrimitives } from './types.js' /** @@ -54,14 +53,12 @@ export function renderErrorWithSuggestions( * Validates the metadata of a command to ensure it has all the neccessary * properties */ -export function validCommandMetaData( +export function validateCommandMetaData( command: unknown, exportPath: string ): asserts command is CommandMetaData { - if (!command || (typeof command !== 'object' && typeof command !== 'function')) { - throw new RuntimeException( - `Invalid command exported from ${exportPath}. Expected object, received "${inspect(command)}"` - ) + if (!command || typeof command !== 'object') { + throw new RuntimeException(`Invalid command metadata exported from ${exportPath}`) } try { @@ -93,5 +90,5 @@ export function validateCommand( ) } - validCommandMetaData(commandConstructor.serialize(), exportPath) + validateCommandMetaData(commandConstructor.serialize(), exportPath) } diff --git a/src/kernel.ts b/src/kernel.ts index d6689f0..6028b22 100644 --- a/src/kernel.ts +++ b/src/kernel.ts @@ -132,7 +132,7 @@ export class Kernel { /** * Collection of loaders to use for loading commands */ - #loaders: LoadersContract[] = [] + #loaders: (LoadersContract | (() => Promise>))[] = [] /** * An array of registered namespaces. Sorted alphabetically @@ -294,7 +294,7 @@ export class Kernel { await this.#hooks.runner('executing').run(this.#mainCommand!, true) await this.#executor.run(this.#mainCommand!, this) await this.#hooks.runner('executed').run(this.#mainCommand!, true) - this.exitCode = this.exitCode ?? this.#mainCommand!.exitCode ?? 0 + this.exitCode = this.exitCode ?? this.#mainCommand!.exitCode this.#state = 'completed' } catch (error) { this.exitCode = 1 @@ -361,7 +361,7 @@ export class Kernel { * Incase multiple loaders returns a single command, the command from the * most recent loader will be used. */ - addLoader(loader: LoadersContract): this { + addLoader(loader: LoadersContract | (() => Promise>)): this { if (this.#state !== 'idle') { throw new RuntimeException(`Cannot add loader in "${this.#state}" state`) } @@ -587,10 +587,22 @@ export class Kernel { * Load metadata for all commands using the loaders */ for (let loader of this.#loaders) { - const commands = await loader.getMetaData() + let loaderInstance: LoadersContract + + /** + * A loader can be a function that lazily imports and instantiates + * a loader + */ + if (typeof loader === 'function') { + loaderInstance = await loader() + } else { + loaderInstance = loader + } + + const commands = await loaderInstance.getMetaData() commands.forEach((command) => { - this.#commands.set(command.commandName, { metaData: command, loader }) + this.#commands.set(command.commandName, { metaData: command, loader: loaderInstance }) command.aliases.forEach((alias) => this.addAlias(alias, command.commandName)) command.namespace && namespaces.add(command.namespace) }) diff --git a/src/loaders/fs_loader.ts b/src/loaders/fs_loader.ts index 22d730d..f8719a5 100644 --- a/src/loaders/fs_loader.ts +++ b/src/loaders/fs_loader.ts @@ -9,7 +9,7 @@ import { fileURLToPath } from 'node:url' import { extname, relative } from 'node:path' -import { fsReadAll, RuntimeException } from '@poppinss/utils' +import { fsReadAll, importDefault } from '@poppinss/utils' import { validateCommand } from '../helpers.js' import type { AbstractBaseCommand, CommandMetaData, LoadersContract } from '../types.js' @@ -27,13 +27,19 @@ export class FsLoader implements LoadersCon */ #comandsDirectory: string + /** + * Paths to ignore + */ + #ignorePaths: string[] + /** * An array of loaded commands */ #commands: { command: Command; filePath: string }[] = [] - constructor(comandsDirectory: string) { + constructor(comandsDirectory: string, ignorePaths?: string[]) { this.#comandsDirectory = comandsDirectory + this.#ignorePaths = ignorePaths || [] } /** @@ -48,6 +54,7 @@ export class FsLoader implements LoadersCon */ const commandFiles = await fsReadAll(this.#comandsDirectory, { pathType: 'url', + ignoreMissingRoot: true, filter: (filePath: string) => { const ext = extname(filePath) if (JS_MODULES.includes(ext)) { @@ -67,15 +74,18 @@ export class FsLoader implements LoadersCon * export */ for (let file of commandFiles) { - const fileRelativeName = relative(this.#comandsDirectory, fileURLToPath(file)) - const commandFileExports = await import(file) - if (!commandFileExports.default) { - throw new RuntimeException( - `Invalid command exported from "${fileRelativeName}" file. Missing export default` - ) + /** + * Remapping .ts files to .js, otherwise the file cannot imported + */ + if (file.endsWith('.ts')) { + file = file.replace(/\.ts$/, '.js') } - commands[fileRelativeName] = commandFileExports.default + const relativeFileName = relative(this.#comandsDirectory, fileURLToPath(file)) + + if (!this.#ignorePaths?.includes(relativeFileName)) { + commands[relativeFileName] = await importDefault(() => import(file), relativeFileName) + } } return commands @@ -84,7 +94,7 @@ export class FsLoader implements LoadersCon /** * Returns the metadata of commands */ - async getMetaData(): Promise<(CommandMetaData & { filePath: string })[]> { + async getMetaData(): Promise { const commandsCollection = await this.#loadCommands() Object.keys(commandsCollection).forEach((key) => { @@ -104,8 +114,9 @@ export class FsLoader implements LoadersCon */ async getCommand(metaData: CommandMetaData): Promise { return ( - this.#commands.find(({ command }) => command.commandName === metaData.commandName)?.command || - null + this.#commands.find(({ command }) => { + return command.commandName === metaData.commandName + })?.command || null ) } } diff --git a/src/loaders/modules_loader.ts b/src/loaders/modules_loader.ts deleted file mode 100644 index dc67d79..0000000 --- a/src/loaders/modules_loader.ts +++ /dev/null @@ -1,160 +0,0 @@ -/* - * @adonisjs/ace - * - * (c) AdonisJS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { RuntimeException } from '@poppinss/utils' - -import { validateCommand, validCommandMetaData } from '../helpers.js' -import type { AbstractBaseCommand, CommandMetaData, LoadersContract } from '../types.js' - -/** - * Module based command loader must implement the following methods. - */ -interface CommandsModuleLoader { - list: () => Promise - load: (command: CommandMetaData) => Promise -} - -/** - * Modules loader exposes the API to lazy load commands from - * ES modules. - * - * The modules have to implement the [[CommandsModuleLoader]] interface - */ -export class ModulesLoader - implements LoadersContract -{ - /** - * The import root is the base path to use when resolving - * modules mentioned in the command source. - */ - #importRoot: string | URL - - /** - * An array of modules to import for loading commands - */ - #commandSources: string[] - - /** - * A collection of commands with their loaders. The key is the - * command name and the value is the loader that loaded the - * command. - */ - #commands?: Map - - constructor(importRoot: string | URL, commandSources: string[]) { - this.#importRoot = importRoot - this.#commandSources = commandSources - } - - /** - * Imports the loader from the loader source path. - */ - async #importLoader(sourcePath: string): Promise { - const importPath = await import.meta.resolve!(sourcePath, this.#importRoot) - const loader = await import(importPath) - - /** - * Ensure the loader has list method - */ - if (typeof loader.list !== 'function') { - throw new RuntimeException( - `Invalid command loader "${sourcePath}". Missing "list" method export` - ) - } - - /** - * Ensure the loader has load method - */ - if (typeof loader.load !== 'function') { - throw new RuntimeException( - `Invalid command loader "${sourcePath}". Missing "load" method export` - ) - } - - return loader - } - - /** - * Returns an array of commands returns by the loader list method - */ - async #getCommandsList(loader: CommandsModuleLoader, importPath: string): Promise { - const list = await loader.list() - - if (!Array.isArray(list)) { - throw new RuntimeException( - `Invalid commands list. The "${importPath}.list" method must return an array of commands` - ) - } - - return list - } - - /** - * Loads commands from the registered sources - */ - async #loadCommands() { - this.#commands = new Map() - const commands: CommandMetaData[] = [] - - for (let sourcePath of this.#commandSources) { - const loader = await this.#importLoader(sourcePath) - const list = await this.#getCommandsList(loader, sourcePath) - - list.forEach((metaData) => { - validCommandMetaData(metaData, `"${sourcePath}.list" method`) - commands.push(metaData) - this.#commands!.set(metaData.commandName, { loader, sourcePath }) - }) - } - - return commands - } - - /** - * Returns the loader for a given command - */ - async #getCommandLoader(commandName: string) { - if (!this.#commands) { - await this.#loadCommands() - } - - const loader = this.#commands!.get(commandName) - if (!loader) { - return null - } - - return loader - } - - /** - * Returns an array of command's metadata - */ - async getMetaData(): Promise { - return this.#loadCommands() - } - - /** - * Returns the command class constructor for a given command. Null - * is returned when unable to lookup the command - */ - async getCommand(metaData: CommandMetaData): Promise { - const loader = await this.#getCommandLoader(metaData.commandName) - if (!loader) { - return null - } - - const command = await loader.loader.load(metaData) - if (command === null || command === undefined) { - return null - } - - validateCommand(command, `"${loader.sourcePath}.load" method`) - return command - } -} diff --git a/src/types.ts b/src/types.ts index 9597978..d47651a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -289,7 +289,7 @@ export type CommandMetaData = { * Command configuration options */ options: CommandOptions -} +} & Record /** * Static set of command options diff --git a/stubs/commands_loader.stub b/stubs/commands_loader.stub new file mode 100644 index 0000000..217af0c --- /dev/null +++ b/stubs/commands_loader.stub @@ -0,0 +1,36 @@ +import { readFile } from 'node:fs/promises' + +/** + * In-memory cache of commands after they have been loaded + */ +let commandsMetaData + +/** + * Reads the commands from the "./commands.json" file. Since, the commands.json + * file is generated automatically, we do not have to validate its contents + */ +export async function getMetaData() { + if (commandsMetaData) { + return commandsMetaData + } + + const commandsIndex = await readFile(new URL('./commands.json', import.meta.url), 'utf-8') + commandsMetaData = JSON.parse(commandsIndex).commands + + return commandsMetaData +} + +/** + * Imports the command by lookingup its path from the commands + * metadata + */ +export async function getCommand(metaData) { + const commands = await getMetaData() + const command = commands.find(({ commandName }) => metaData.commandName === commandName) + if (!command) { + return null + } + + const { default: commandConstructor } = await import(new URL(command.filePath, import.meta.url).href) + return commandConstructor +} diff --git a/toolkit/main.ts b/stubs/main.ts similarity index 64% rename from toolkit/main.ts rename to stubs/main.ts index f629116..64738b6 100644 --- a/toolkit/main.ts +++ b/stubs/main.ts @@ -7,7 +7,6 @@ * file that was distributed with this source code. */ -import { Kernel } from '../index.js' +import { getDirname } from '@poppinss/utils' -const kernel = Kernel.create() -kernel.handle(process.argv) +export const stubsRoot = getDirname(import.meta.url) diff --git a/tests/helpers.spec.ts b/tests/helpers.spec.ts index 4570de9..dabb13e 100644 --- a/tests/helpers.spec.ts +++ b/tests/helpers.spec.ts @@ -9,7 +9,12 @@ import { test } from '@japa/runner' import { cliui } from '@poppinss/cliui' -import { renderErrorWithSuggestions, sortAlphabetically } from '../src/helpers.js' +import { + validateCommand, + sortAlphabetically, + validateCommandMetaData, + renderErrorWithSuggestions, +} from '../src/helpers.js' test.group('Helpers | Sort', () => { test('sort values alphabetically', ({ assert }) => { @@ -43,3 +48,88 @@ test.group('Helpers | renderErrorWithSuggestions', () => { ]) }) }) + +test.group('Helpers | validateCommandMetaData', () => { + test('raise error when command metadata is not an object', ({ assert }) => { + assert.throws( + () => validateCommandMetaData('foo', '"./foo.js" file'), + 'Invalid command metadata exported from "./foo.js" file' + ) + }) + + test('raise error when command metadata is incomplete', ({ assert }) => { + assert.throws( + () => validateCommandMetaData({}, '"./foo.js" file'), + 'Invalid command exported from "./foo.js" file. requires property "aliases"' + ) + }) + + test('work fine when command metadata is complete', ({ assert }) => { + assert.doesNotThrows(() => + validateCommandMetaData( + { + commandName: 'serve', + description: '', + aliases: [], + namespace: null, + args: [], + flags: [], + options: {}, + }, + '"./foo.js" file' + ) + ) + }) +}) + +test.group('Helpers | validateCommand', () => { + test('raise error when command is not a constructor', ({ assert }) => { + assert.throws( + () => validateCommand('foo', '"./foo.js" file'), + 'Invalid command exported from "./foo.js" file. Expected command to be a class' + ) + }) + + test('raise error when command class does not have a serialize method', ({ assert }) => { + class MakeController {} + + assert.throws( + () => validateCommand(MakeController, '"./foo.js" file'), + 'Invalid command exported from "./foo.js" file. Expected command to extend the "BaseCommand"' + ) + }) + + test('raise error when command metadata is invalid', ({ assert }) => { + class MakeController { + static serialize() { + return {} + } + } + + assert.throws( + () => validateCommand(MakeController, '"./foo.js" file'), + 'Invalid command exported from "./foo.js" file. requires property "aliases"' + ) + }) + + test('work fine when metadata is valid', ({ assert }) => { + class MakeController { + static serialize() { + return { + commandName: 'serve', + description: '', + aliases: [], + namespace: null, + args: [], + flags: [], + options: {}, + } + } + } + + assert.doesNotThrows( + () => validateCommand(MakeController, '"./foo.js" file'), + 'Invalid command exported from "./foo.js" file. requires property "aliases"' + ) + }) +}) diff --git a/tests/index_generator.spec.ts b/tests/index_generator.spec.ts new file mode 100644 index 0000000..c3acd5c --- /dev/null +++ b/tests/index_generator.spec.ts @@ -0,0 +1,79 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import fs from 'fs-extra' +import { join } from 'node:path' +import { test } from '@japa/runner' +import { fileURLToPath } from 'node:url' +import { IndexGenerator } from '../index.js' +import { validateCommand, validateCommandMetaData } from '../src/helpers.js' + +const BASE_URL = new URL('./tmp/', import.meta.url) +const BASE_PATH = fileURLToPath(BASE_URL) + +test.group('Index generator', (group) => { + group.each.setup(() => { + return () => fs.remove(BASE_PATH) + }) + + test('generate loader and commands index by scanning commands directory', async ({ assert }) => { + await fs.outputFile( + join(BASE_PATH, 'commands', 'make_controller_v_2.ts'), + ` + export default class MakeController { + static commandName = 'make:controller' + static args = [] + static flags = [] + static aliases = [] + static options = {} + static description = '' + static namespace = 'make' + + static serialize() { + return { + commandName: this.commandName, + description: this.description, + namespace: this.namespace, + args: this.args, + flags: this.flags, + options: this.options, + aliases: this.aliases, + } + } + } + ` + ) + + const generator = new IndexGenerator(join(BASE_PATH, 'commands')) + await generator.generate() + + /** + * Validate index + */ + const indexJSON = await fs.readFile(join(BASE_PATH, 'commands', 'commands.json'), 'utf-8') + const commandsIndex = JSON.parse(indexJSON) + + assert.properties(commandsIndex, ['commands', 'version']) + assert.equal(commandsIndex.version, 1) + assert.isArray(commandsIndex.commands) + commandsIndex.commands.forEach((command: any) => + validateCommandMetaData(command, './commands.json') + ) + + /** + * Validate loader + */ + const loader = await import(new URL('./commands/main.js', BASE_URL).href) + const metaData = await loader.getMetaData() + metaData.forEach((command: any) => validateCommandMetaData(command, './commands.json')) + + const command = await loader.getCommand(metaData[0]) + validateCommand(command, './main.ts') + }) +}) diff --git a/tests/kernel/loaders.spec.ts b/tests/kernel/loaders.spec.ts index 266919e..4c3ab04 100644 --- a/tests/kernel/loaders.spec.ts +++ b/tests/kernel/loaders.spec.ts @@ -81,4 +81,25 @@ test.group('Kernel | loaders', () => { 'Cannot add loader in "booted" state' ) }) + + test('register loader as a function', async ({ assert }) => { + const kernel = Kernel.create() + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + } + + class MakeModel extends BaseCommand { + static commandName = 'make:model' + } + + kernel.addLoader(async () => new ListLoader([MakeController, MakeModel])) + await kernel.boot() + + assert.deepEqual(kernel.getCommands(), [ + kernel.getDefaultCommand().serialize(), + MakeController.serialize(), + MakeModel.serialize(), + ]) + }) }) diff --git a/tests/loaders/fs_loader.spec.ts b/tests/loaders/fs_loader.spec.ts index 25e8af4..b56ee44 100644 --- a/tests/loaders/fs_loader.spec.ts +++ b/tests/loaders/fs_loader.spec.ts @@ -8,10 +8,11 @@ */ import fs from 'fs-extra' +import { join } from 'node:path' import { test } from '@japa/runner' import { fileURLToPath } from 'node:url' + import { FsLoader } from '../../src/loaders/fs_loader.js' -import { join } from 'node:path' const BASE_URL = new URL('./tmp/', import.meta.url) const BASE_PATH = fileURLToPath(BASE_URL) @@ -37,7 +38,7 @@ test.group('Loaders | fs', (group) => { const loader = new FsLoader(join(BASE_PATH, './commands')) await assert.rejects( () => loader.getMetaData(), - 'Invalid command exported from "make_controller_v_1.ts" file. Missing export default' + 'Missing "export default" in module "make_controller_v_1.js"' ) }) @@ -73,7 +74,7 @@ test.group('Loaders | fs', (group) => { const commands = await loader.getMetaData() assert.deepEqual(commands, [ { - filePath: 'make_controller_v_2.ts', + filePath: 'make_controller_v_2.js', commandName: 'make:controller', description: '', namespace: 'make', @@ -164,7 +165,7 @@ test.group('Loaders | fs', (group) => { assert.deepEqual(commands, [ { commandName: 'make:controller', - filePath: 'make_controller_v_3.ts', + filePath: 'make_controller_v_3.js', description: '', namespace: 'make', args: [], @@ -250,7 +251,78 @@ test.group('Loaders | fs', (group) => { assert.deepEqual(commands, [ { commandName: 'make:controller', - filePath: 'make/controller.ts', + filePath: 'make/controller.js', + description: '', + namespace: 'make', + args: [], + flags: [], + options: {}, + aliases: [], + }, + ]) + }) + + test('ignore commands by filename', async ({ assert }) => { + await fs.outputFile( + join(BASE_PATH, 'commands', 'make_controller_v_2.ts'), + ` + export default class MakeController { + static commandName = 'make:controller' + static args = [] + static flags = [] + static aliases = [] + static options = {} + static description = '' + static namespace = 'make' + + static serialize() { + return { + commandName: this.commandName, + description: this.description, + namespace: this.namespace, + args: this.args, + flags: this.flags, + options: this.options, + aliases: this.aliases, + } + } + } + ` + ) + + await fs.outputFile( + join(BASE_PATH, 'commands', 'make', 'controller.ts'), + ` + export default class MakeController { + static commandName = 'make:controller' + static args = [] + static flags = [] + static aliases = [] + static options = {} + static description = '' + static namespace = 'make' + + static serialize() { + return { + commandName: this.commandName, + description: this.description, + namespace: this.namespace, + args: this.args, + flags: this.flags, + options: this.options, + aliases: this.aliases, + } + } + } + ` + ) + + const loader = new FsLoader(join(BASE_PATH, './commands'), ['make_controller_v_2.js']) + const commands = await loader.getMetaData() + assert.deepEqual(commands, [ + { + commandName: 'make:controller', + filePath: 'make/controller.js', description: '', namespace: 'make', args: [], diff --git a/tests/loaders/modules_loader.spec.ts b/tests/loaders/modules_loader.spec.ts deleted file mode 100644 index 5d07852..0000000 --- a/tests/loaders/modules_loader.spec.ts +++ /dev/null @@ -1,360 +0,0 @@ -/* - * @adonisjs/ace - * - * (c) AdonisJS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import fs from 'fs-extra' -import { join } from 'node:path' -import { test } from '@japa/runner' -import { fileURLToPath } from 'node:url' -import { ModulesLoader } from '../../src/loaders/modules_loader.js' - -const BASE_URL = new URL('./tmp/', import.meta.url) -const BASE_PATH = fileURLToPath(BASE_URL) - -test.group('Loaders | modules', (group) => { - group.each.setup(() => { - return () => fs.remove(BASE_PATH) - }) - - test('raise error when unable to import loaders', async ({ assert }) => { - const loader = new ModulesLoader(BASE_URL, ['./loader_one.js']) - await assert.rejects(() => loader.getMetaData(), /Cannot find module/) - }) - - test('raise error when loader does not implement the list method', async ({ assert }) => { - await fs.outputFile( - join(BASE_PATH, './loader_one.js'), - ` - ` - ) - - const loader = new ModulesLoader(BASE_URL, ['./loader_one.js']) - await assert.rejects( - () => loader.getMetaData(), - 'Invalid command loader "./loader_one.js". Missing "list" method export' - ) - }) - - test('raise error when loader does not implement the load method', async ({ assert }) => { - await fs.outputFile( - join(BASE_PATH, './loader_one.js'), - ` - export async function list() {} - ` - ) - - const loader = new ModulesLoader(BASE_URL, ['./loader_one.js?v=1']) - await assert.rejects( - () => loader.getMetaData(), - 'Invalid command loader "./loader_one.js?v=1". Missing "load" method export' - ) - }) - - test('raise error when list method does not return an array', async ({ assert }) => { - await fs.outputFile( - join(BASE_PATH, './loader_one.js'), - ` - export async function list() {} - export async function load() {} - ` - ) - - const loader = new ModulesLoader(BASE_URL, ['./loader_one.js?v=2']) - await assert.rejects( - () => loader.getMetaData(), - 'Invalid commands list. The "./loader_one.js?v=2.list" method must return an array of commands' - ) - }) - - test('raise error when list array does not have objects', async ({ assert }) => { - await fs.outputFile( - join(BASE_PATH, './loader_one.js'), - ` - export async function list() { - return ['foo'] - } - export async function load() {} - ` - ) - - const loader = new ModulesLoader(BASE_URL, ['./loader_one.js?v=3']) - await assert.rejects( - () => loader.getMetaData(), - `Invalid command exported from "./loader_one.js?v=3.list" method. Expected object, received "'foo'"` - ) - }) - - test('raise error when list array items are not valid metadata objects', async ({ assert }) => { - await fs.outputFile( - join(BASE_PATH, './loader_one.js'), - ` - export async function list() { - return [{}] - } - export async function load() {} - ` - ) - - const loader = new ModulesLoader(BASE_URL, ['./loader_one.js?v=4']) - await assert.rejects( - () => loader.getMetaData(), - `Invalid command exported from "./loader_one.js?v=4.list" method. requires property "commandName"` - ) - }) - - test('load commands from multiple module loaders', async ({ assert }) => { - await fs.outputFile( - join(BASE_PATH, './loader_two.js'), - ` - export async function list() { - return [ - { - commandName: 'make:controller', - description: '', - namespace: 'make', - args: [], - flags: [], - aliases: [], - options: {}, - } - ] - } - export async function load() {} - ` - ) - - await fs.outputFile( - join(BASE_PATH, './loader_three.js'), - ` - export async function list() { - return [ - { - commandName: 'make:model', - description: '', - namespace: 'make', - args: [], - flags: [], - aliases: [], - options: {}, - } - ] - } - export async function load() {} - ` - ) - - const loader = new ModulesLoader(BASE_URL, ['./loader_two.js', './loader_three.js']) - const commands = await loader.getMetaData() - - assert.deepEqual(commands, [ - { - commandName: 'make:controller', - description: '', - namespace: 'make', - args: [], - flags: [], - aliases: [], - options: {}, - }, - { - commandName: 'make:model', - description: '', - namespace: 'make', - args: [], - flags: [], - aliases: [], - options: {}, - }, - ]) - }) - - test('return null when load method returns undefined', async ({ assert }) => { - await fs.outputFile( - join(BASE_PATH, './loader_two.js'), - ` - export async function list() { - return [ - { - commandName: 'make:controller', - description: '', - namespace: 'make', - args: [], - flags: [], - aliases: [], - options: {}, - } - ] - } - export async function load() {} - ` - ) - - const loader = new ModulesLoader(BASE_URL, ['./loader_two.js?v=1']) - const command = await loader.getCommand({ commandName: 'make:controller' } as any) - assert.isNull(command) - }) - - test('return null when command is unknown', async ({ assert }) => { - const loader = new ModulesLoader(BASE_URL, []) - const command = await loader.getCommand({ commandName: 'make:controller' } as any) - assert.isNull(command) - }) - - test('return null when load method returns null', async ({ assert }) => { - await fs.outputFile( - join(BASE_PATH, './loader_two.js'), - ` - export async function list() { - return [ - { - commandName: 'make:controller', - description: '', - namespace: null, - args: [], - flags: [], - aliases: [], - options: {}, - } - ] - } - export async function load() { - return null - } - ` - ) - - const loader = new ModulesLoader(BASE_URL, ['./loader_two.js?v=2']) - const command = await loader.getCommand({ commandName: 'make:controller' } as any) - assert.isNull(command) - }) - - test('raise error when load method does not return command class', async ({ assert }) => { - await fs.outputFile( - join(BASE_PATH, './loader_two.js'), - ` - export async function list() { - return [ - { - commandName: 'make:controller', - description: '', - namespace: 'make', - args: [], - flags: [], - aliases: [], - options: {}, - } - ] - } - - export async function load() { - return { - commandName: 'make:controller', - description: '', - namespace: 'make', - args: [], - flags: [], - aliases: [], - options: {}, - } - } - ` - ) - - const loader = new ModulesLoader(BASE_URL, ['./loader_two.js?v=3']) - await assert.rejects( - () => loader.getCommand({ commandName: 'make:controller' } as any), - 'Invalid command exported from "./loader_two.js?v=3.load" method. Expected command to be a class' - ) - }) - - test('raise error when command class does not have serialize method', async ({ assert }) => { - await fs.outputFile( - join(BASE_PATH, './loader_two.js'), - ` - export async function list() { - return [ - { - commandName: 'make:controller', - description: '', - namespace: 'make', - args: [], - flags: [], - aliases: [], - options: {}, - } - ] - } - export async function load() { - return class Command { - static commandName = 'make:controller' - static args = [] - static flags = [] - static aliases = [] - static options = {} - static description = '' - static namespace = 'make' - } - } - ` - ) - - const loader = new ModulesLoader(BASE_URL, ['./loader_two.js?v=4']) - await assert.rejects( - () => loader.getCommand({ commandName: 'make:controller' } as any), - 'Invalid command exported from "./loader_two.js?v=4.load" method. Expected command to extend the "BaseCommand"' - ) - }) - - test('get command constructor returned by the load method', async ({ assert }) => { - await fs.outputFile( - join(BASE_PATH, './loader_two.js'), - ` - export async function list() { - return [ - { - commandName: 'make:controller', - description: '', - namespace: 'make', - args: [], - flags: [], - aliases: [], - options: {}, - } - ] - } - export async function load() { - return class Command { - static commandName = 'make:controller' - static args = [] - static flags = [] - static aliases = [] - static options = {} - static description = '' - static namespace = 'make' - - static serialize() { - return { - commandName: this.commandName, - args: this.args, - flags: this.flags, - aliases: this.aliases, - options: this.options, - description: this.description, - namespace: this.namespace, - } - } - } - } - ` - ) - - const loader = new ModulesLoader(BASE_URL, ['./loader_two.js?v=5']) - const command = await loader.getCommand({ commandName: 'make:controller' } as any) - assert.isFunction(command) - }) -}) diff --git a/toolkit/commands/index_command/main.ts b/toolkit/commands/index_command/main.ts deleted file mode 100644 index 4e8cdba..0000000 --- a/toolkit/commands/index_command/main.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * @adonisjs/ace - * - * (c) AdonisJS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { join } from 'node:path' -import { args, BaseCommand, FsLoader } from '../../../index.js' - -export class IndexCommand extends BaseCommand { - static commandName: string = 'index' - static description: string = - 'Generate JSON index and module loader for commands from a given directory' - - @args.string({ description: 'Path to the commands directory. Should be relative from "cwd"' }) - commandsDir!: string - - async run(): Promise { - const loader = new FsLoader(join(process.cwd(), this.commandsDir)) - const commandsMetaData = await loader.getMetaData() - JSON.stringify(commandsMetaData) - } -} diff --git a/toolkit/commands/index_command/stubs/module_loader.stub b/toolkit/commands/index_command/stubs/module_loader.stub deleted file mode 100644 index 22a5c22..0000000 --- a/toolkit/commands/index_command/stubs/module_loader.stub +++ /dev/null @@ -1,18 +0,0 @@ -import { readFile } from 'node:fs/promise' - -export async funnction list() { - const commandsIndex = await readFile('./commands_index.json', 'utf-8') - return JSON.parse(commandsIndex).commands -} - -export async funnction load(metaData) { - const commands = await this.list() - const command = commands.find(({ commandName }) => metaData.commandName) - - if (!command) { - return null - } - - const { default: commandConstructor } = await import(command.importPath) - return commandConstructor -} From dfb40913fa1211e969c9e4496e75bf93ddfaeeb8 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 27 Jan 2023 21:34:06 +0530 Subject: [PATCH 018/112] test: fix breaking tests --- tests/loaders/fs_loader.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/loaders/fs_loader.spec.ts b/tests/loaders/fs_loader.spec.ts index b56ee44..6d0902b 100644 --- a/tests/loaders/fs_loader.spec.ts +++ b/tests/loaders/fs_loader.spec.ts @@ -22,9 +22,9 @@ test.group('Loaders | fs', (group) => { return () => fs.remove(BASE_PATH) }) - test('raise error when commands directory does not exists', async ({ assert }) => { + test('do not raise error when commands directory does not exists', async ({ assert }) => { const loader = new FsLoader(join(BASE_PATH, './commands')) - await assert.rejects(() => loader.getMetaData(), /ENOENT: no such file or directory/) + await assert.doesNotRejects(() => loader.getMetaData()) }) test('raise error when there is no default export in command file', async ({ assert }) => { From 406c14ac9c0cff13ac186369427cbdfd06b8f960 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 27 Jan 2023 21:38:19 +0530 Subject: [PATCH 019/112] fix: normalize file relative paths to have unix slash --- src/loaders/fs_loader.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/loaders/fs_loader.ts b/src/loaders/fs_loader.ts index f8719a5..0e183f8 100644 --- a/src/loaders/fs_loader.ts +++ b/src/loaders/fs_loader.ts @@ -9,7 +9,7 @@ import { fileURLToPath } from 'node:url' import { extname, relative } from 'node:path' -import { fsReadAll, importDefault } from '@poppinss/utils' +import { fsReadAll, importDefault, slash } from '@poppinss/utils' import { validateCommand } from '../helpers.js' import type { AbstractBaseCommand, CommandMetaData, LoadersContract } from '../types.js' @@ -81,7 +81,7 @@ export class FsLoader implements LoadersCon file = file.replace(/\.ts$/, '.js') } - const relativeFileName = relative(this.#comandsDirectory, fileURLToPath(file)) + const relativeFileName = slash(relative(this.#comandsDirectory, fileURLToPath(file))) if (!this.#ignorePaths?.includes(relativeFileName)) { commands[relativeFileName] = await importDefault(() => import(file), relativeFileName) From e35bc4e93c2901c851e9ee71797a127a695eeb1b Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Sat, 28 Jan 2023 14:45:39 +0530 Subject: [PATCH 020/112] feat: add exception handler --- index.ts | 1 + package.json | 4 +- src/commands/base.ts | 8 +- src/exception_handler.ts | 101 ++++++++++++++++++++++++++ src/kernel.ts | 33 ++------- tests/base_command/exec.spec.ts | 4 +- tests/exception_handler.spec.ts | 125 ++++++++++++++++++++++++++++++++ 7 files changed, 240 insertions(+), 36 deletions(-) create mode 100644 src/exception_handler.ts create mode 100644 tests/exception_handler.spec.ts diff --git a/index.ts b/index.ts index c6cfd90..9b780ca 100644 --- a/index.ts +++ b/index.ts @@ -17,4 +17,5 @@ export { HelpCommand } from './src/commands/help.js' export { ListCommand } from './src/commands/list.js' export { FsLoader } from './src/loaders/fs_loader.js' export { ListLoader } from './src/loaders/list_loader.js' +export { ExceptionHandler } from './src/exception_handler.js' export { IndexGenerator } from './src/generators/index_generator.js' diff --git a/package.json b/package.json index 4b58d20..757e775 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,9 @@ "jsonschema": "^1.4.1", "string-similarity": "^4.0.4", "string-width": "^5.1.2", - "yargs-parser": "^21.1.1" + "yargs-parser": "^21.1.1", + "youch": "^3.2.2", + "youch-terminal": "^2.1.5" }, "devDependencies": { "@commitlint/cli": "^17.4.2", diff --git a/src/commands/base.ts b/src/commands/base.ts index fc42fcd..9bb8682 100644 --- a/src/commands/base.ts +++ b/src/commands/base.ts @@ -460,15 +460,11 @@ export class BaseCommand extends Macroable { try { this.result = await this.run() this.exitCode = this.exitCode ?? 0 + return this.result } catch (error) { this.error = error this.exitCode = this.exitCode ?? 1 + throw error } - - if (this.error) { - this.logger.fatal(this.error) - } - - return this.result } } diff --git a/src/exception_handler.ts b/src/exception_handler.ts new file mode 100644 index 0000000..c9ccaa2 --- /dev/null +++ b/src/exception_handler.ts @@ -0,0 +1,101 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { errors, Kernel } from '../index.js' +import { renderErrorWithSuggestions } from './helpers.js' + +/** + * The base exception handler that is used by default to exception + * ace exceptions. + * + * You can extend this class to custom the exception rendering + * behavior. + */ +export class ExceptionHandler { + debug: boolean = true + + /** + * Known error codes. For these error, only the error message is + * reported using the logger + */ + protected knownErrorCodes: string[] = [] + + /** + * Internal set of known error codes. + */ + protected internalKnownErrorCode = Object.keys(errors) + + /** + * Logs error to stderr using logger + */ + protected logError(error: { message: any } & unknown, kernel: Kernel) { + kernel.ui.logger.logError(`${kernel.ui.colors.bgRed().white(' ERROR ')} ${error.message}`) + } + + /** + * Pretty prints uncaught error in debug mode + */ + protected async prettyPrintError(error: object) { + // @ts-expect-error + const { default: youchTerminal } = await import('youch-terminal') + const { default: Youch } = await import('youch') + + const youch = new Youch(error, {}) + console.log(youchTerminal(await youch.toJSON(), { displayShortPath: true })) + } + + /** + * Renders an exception for the console + */ + async render(error: unknown, kernel: Kernel) { + /** + * Render non object errors or errors without message property + * as a string using the logger + */ + if (typeof error !== 'object' || error === null || !('message' in error)) { + this.logError({ message: String(error) }, kernel) + return + } + + /** + * Report command not found error with command suggestions + */ + if (error instanceof errors.E_COMMAND_NOT_FOUND) { + renderErrorWithSuggestions( + kernel.ui, + error.message, + kernel.getCommandSuggestions(error.commandName) + ) + return + } + + /** + * Known errors should always be reported with a message + */ + if ( + 'code' in error && + typeof error.code === 'string' && + (this.internalKnownErrorCode.includes(error.code) || + this.knownErrorCodes.includes(error.code)) + ) { + this.logError({ message: error.message }, kernel) + return + } + + /** + * Log error message only when not in debug mode + */ + if (!this.debug) { + this.logError({ message: error.message }, kernel) + return + } + + return this.prettyPrintError(error) + } +} diff --git a/src/kernel.ts b/src/kernel.ts index 6028b22..c045a43 100644 --- a/src/kernel.ts +++ b/src/kernel.ts @@ -18,8 +18,9 @@ import { Parser } from './parser.js' import * as errors from './errors.js' import { ListCommand } from './commands/list.js' import { BaseCommand } from './commands/base.js' +import { sortAlphabetically } from './helpers.js' import { ListLoader } from './loaders/list_loader.js' -import { sortAlphabetically, renderErrorWithSuggestions } from './helpers.js' +import { ExceptionHandler } from './exception_handler.js' import type { Flag, @@ -51,6 +52,10 @@ const knowErrorCodes = Object.keys(errors) * is tailored for a standard CLI environment. */ export class Kernel { + #errorHandler: { + render(error: unknown, kernel: Kernel): Promise + } = new ExceptionHandler() + /** * The default executor for creating command's instance * and running them @@ -299,31 +304,7 @@ export class Kernel { } catch (error) { this.exitCode = 1 this.#state = 'completed' - await this.#handleError(error) - } - } - - /** - * Handles the error raised during the main command execution. - * - * @note: Do not use this error handler for anything other than - * handling errors of the main command - */ - async #handleError(error: any) { - /** - * Reporting errors with the best UI possible based upon the error - * type - */ - if (error instanceof errors.E_COMMAND_NOT_FOUND) { - renderErrorWithSuggestions( - this.ui, - error.message, - this.getCommandSuggestions(error.commandName) - ) - } else if (knowErrorCodes.includes(error.code)) { - this.ui.logger.logError(`${this.ui.colors.bgRed().white(' ERROR ')} ${error.message}`) - } else { - console.log(error.stack) + await this.#errorHandler.render(error, this) } } diff --git a/tests/base_command/exec.spec.ts b/tests/base_command/exec.spec.ts index 1f6e8d8..b1122e1 100644 --- a/tests/base_command/exec.spec.ts +++ b/tests/base_command/exec.spec.ts @@ -110,13 +110,11 @@ test.group('Base command | execute | run fails', () => { MakeModel.defineFlag('connection', { type: 'string' }) const kernel = Kernel.create() - kernel.ui = cliui({ mode: 'raw' }) const model = await kernel.create(MakeModel, ['user', '--connection=sqlite']) - await model.exec() + await assert.rejects(() => model.exec()) assert.isUndefined(model.result) assert.equal(model.error?.message, 'Something went wrong') - assert.lengthOf(model.ui.logger.getRenderer().getLogs(), 1) assert.equal(model.exitCode, 1) }) }) diff --git a/tests/exception_handler.spec.ts b/tests/exception_handler.spec.ts new file mode 100644 index 0000000..980467d --- /dev/null +++ b/tests/exception_handler.spec.ts @@ -0,0 +1,125 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { Exception } from '@poppinss/utils' +import { errors, Kernel } from '../index.js' +import { ExceptionHandler } from '../src/exception_handler.js' + +test.group('Exception handler', () => { + test('render non object exceptions using the logger', async ({ assert }) => { + const kernel = Kernel.create() + kernel.ui.switchMode('raw') + + class CustomExceptionHandler extends ExceptionHandler { + debug: boolean = true + protected knownErrorCodes: string[] = ['E_CUSTOM_CODE'] + } + + await new CustomExceptionHandler().render('foo', kernel) + + const logs = kernel.ui.logger.getLogs() + assert.lengthOf(logs, 1) + assert.equal(logs[0].stream, 'stderr') + assert.equal(logs[0].message, 'bgRed(white( ERROR )) foo') + }) + + test('render null values using the logger', async ({ assert }) => { + const kernel = Kernel.create() + kernel.ui.switchMode('raw') + + class CustomExceptionHandler extends ExceptionHandler { + debug: boolean = true + protected knownErrorCodes: string[] = ['E_CUSTOM_CODE'] + } + + await new CustomExceptionHandler().render(null, kernel) + + const logs = kernel.ui.logger.getLogs() + assert.lengthOf(logs, 1) + assert.equal(logs[0].stream, 'stderr') + assert.equal(logs[0].message, 'bgRed(white( ERROR )) null') + }) + + test('report internal known exceptions using the logger', async ({ assert }) => { + const kernel = Kernel.create() + kernel.ui.switchMode('raw') + + class CustomExceptionHandler extends ExceptionHandler { + debug: boolean = true + protected knownErrorCodes: string[] = ['E_CUSTOM_CODE'] + } + + await new CustomExceptionHandler().render(new errors.E_MISSING_ARG(['name']), kernel) + + const logs = kernel.ui.logger.getLogs() + assert.lengthOf(logs, 1) + assert.equal(logs[0].stream, 'stderr') + assert.equal(logs[0].message, 'bgRed(white( ERROR )) Missing required argument "name"') + }) + + test('report known exceptions using the logger', async ({ assert }) => { + const kernel = Kernel.create() + kernel.ui.switchMode('raw') + + class CustomExceptionHandler extends ExceptionHandler { + protected knownErrorCodes: string[] = ['E_CUSTOM_CODE'] + } + + const error = new Exception('Custom error', { code: 'E_CUSTOM_CODE' }) + await new CustomExceptionHandler().render(error, kernel) + + const logs = kernel.ui.logger.getLogs() + assert.lengthOf(logs, 1) + assert.equal(logs[0].stream, 'stderr') + assert.equal(logs[0].message, 'bgRed(white( ERROR )) Custom error') + }) + + test('pretty print uncaught exceptions using youch', async ({ assert }) => { + const kernel = Kernel.create() + kernel.ui.switchMode('raw') + + class CustomExceptionHandler extends ExceptionHandler { + debug: boolean = true + protected knownErrorCodes: string[] = ['E_CUSTOM_CODE'] + + protected async prettyPrintError(error: any) { + kernel.ui.logger.logError(`Pretty printing: ${error.message}`) + } + } + + await new CustomExceptionHandler().render(new Error('Something went wrong'), kernel) + + const logs = kernel.ui.logger.getLogs() + assert.lengthOf(logs, 1) + assert.equal(logs[0].stream, 'stderr') + assert.equal(logs[0].message, 'Pretty printing: Something went wrong') + }) + + test('do not pretty print when not in debug mode', async ({ assert }) => { + const kernel = Kernel.create() + kernel.ui.switchMode('raw') + + class CustomExceptionHandler extends ExceptionHandler { + debug: boolean = false + protected knownErrorCodes: string[] = ['E_CUSTOM_CODE'] + + protected async prettyPrintError(error: any) { + kernel.ui.logger.logError(`Pretty printing: ${error.message}`) + } + } + + await new CustomExceptionHandler().render(new Error('Something went wrong'), kernel) + + const logs = kernel.ui.logger.getLogs() + assert.lengthOf(logs, 1) + assert.equal(logs[0].stream, 'stderr') + assert.equal(logs[0].message, 'bgRed(white( ERROR )) Something went wrong') + }) +}) From ade6a2a07bb961af13217ca3c57a44c8cecb5186 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Sat, 28 Jan 2023 15:37:06 +0530 Subject: [PATCH 021/112] feat: add isMain property to base command --- src/commands/base.ts | 8 ++++++++ src/kernel.ts | 2 -- tests/kernel/handle.spec.ts | 19 +++++++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/commands/base.ts b/src/commands/base.ts index 9bb8682..995d928 100644 --- a/src/commands/base.ts +++ b/src/commands/base.ts @@ -406,6 +406,14 @@ export class BaseCommand extends Macroable { return this.ui.colors } + /** + * Is the current command the main command executed from the + * CLI + */ + get isMain(): boolean { + return this.kernel.getMainCommand() === this + } + constructor( protected kernel: Kernel, protected parsed: ParsedOutput, diff --git a/src/kernel.ts b/src/kernel.ts index c045a43..c4ad520 100644 --- a/src/kernel.ts +++ b/src/kernel.ts @@ -43,8 +43,6 @@ import type { ExecutingHookHandler, } from './types.js' -const knowErrorCodes = Object.keys(errors) - /** * The Ace kernel manages the registration and execution of commands. * diff --git a/tests/kernel/handle.spec.ts b/tests/kernel/handle.spec.ts index 17137fe..e37f1ae 100644 --- a/tests/kernel/handle.spec.ts +++ b/tests/kernel/handle.spec.ts @@ -219,4 +219,23 @@ test.group('Kernel | handle', (group) => { assert.equal(kernel.exitCode, 0) assert.equal(kernel.getState(), 'completed') }) + + test('test if a command is a main command or not', async ({ assert }) => { + const kernel = Kernel.create() + class MakeController extends BaseCommand { + static commandName = 'make:controller' + async run() { + assert.equal(this.kernel.getState(), 'running') + assert.strictEqual(this.kernel.getMainCommand(), this) + assert.isTrue(this.isMain) + return 'executed' + } + } + + kernel.addLoader(new ListLoader([MakeController])) + await kernel.handle(['make:controller']) + + assert.equal(kernel.exitCode, 0) + assert.equal(kernel.getState(), 'completed') + }) }) From ce8868b63f10b87b0772ad84ba736d3284a4a4ff Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Sat, 28 Jan 2023 15:39:35 +0530 Subject: [PATCH 022/112] chore(release): 12.0.0-0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 757e775..71dc4c3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/ace", - "version": "11.3.1", + "version": "12.0.0-0", "description": "A CLI framework for Node.js", "main": "build/index.js", "type": "module", From 8c8be079733b638575513c1f5c0b0ed951b9273e Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 2 Feb 2023 11:44:25 +0530 Subject: [PATCH 023/112] feat: add toJSON method to serialize command to JSON --- src/commands/base.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/commands/base.ts b/src/commands/base.ts index 995d928..13d5ef0 100644 --- a/src/commands/base.ts +++ b/src/commands/base.ts @@ -475,4 +475,19 @@ export class BaseCommand extends Macroable { throw error } } + + /** + * JSON representation of the command + */ + toJSON() { + return { + commandName: (this.constructor as typeof BaseCommand).commandName, + options: (this.constructor as typeof BaseCommand).options, + args: this.parsed.args, + flags: this.parsed.flags, + error: this.error, + result: this.result, + exitCode: this.exitCode, + } + } } From 67737171966f86e7b21f0cbcd3be7eb458c293b0 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 2 Feb 2023 12:03:50 +0530 Subject: [PATCH 024/112] feat: add support for alias expansion --- src/kernel.ts | 37 ++++++++++++++++++++++++- tests/kernel/exec.spec.ts | 53 ++++++++++++++++++++++++++++++++++++ tests/kernel/find.spec.ts | 18 +++++++++++++ tests/kernel/handle.spec.ts | 54 ++++++++++++++++++++++++++++++++++++- tests/kernel/main.spec.ts | 16 +++++++++++ 5 files changed, 176 insertions(+), 2 deletions(-) diff --git a/src/kernel.ts b/src/kernel.ts index c4ad520..36fb2e8 100644 --- a/src/kernel.ts +++ b/src/kernel.ts @@ -151,6 +151,13 @@ export class Kernel { */ #aliases: Map = new Map() + /** + * An collection of expansion arguments and flags set on + * an alias. The key is the alias name and the value is + * everything after it. + */ + #aliasExpansions: Map = new Map() + /** * A collection of commands by the command name. This allows us to keep only * the unique commands and also keep the loader reference to know which @@ -224,6 +231,16 @@ export class Kernel { */ async #exec(commandName: string, argv: string[]): Promise> { const Command = await this.find(commandName) + + /** + * Expand aliases + */ + const aliasExpansions = this.#aliasExpansions.get(commandName) + if (aliasExpansions) { + argv = aliasExpansions.concat(argv) + debug('expanding alias %O, cli args %O', commandName, argv) + } + const commandInstance = await this.#create(Command, argv) /** @@ -244,6 +261,15 @@ export class Kernel { try { const Command = await this.find(commandName) + /** + * Expand aliases + */ + const aliasExpansions = this.#aliasExpansions.get(commandName) + if (aliasExpansions) { + argv = aliasExpansions.concat(argv) + debug('expanding alias %O, cli args %O', commandName, argv) + } + /** * Parse CLI argv and also merge global flags parser options. */ @@ -352,8 +378,17 @@ export class Kernel { /** * Register alias for a comamnd name. */ - addAlias(alias: string, commandName: string): this { + addAlias(alias: string, command: string): this { + const [commandName, ...expansions] = command.split(' ') this.#aliases.set(alias, commandName) + + if (expansions.length) { + debug('registering alias %O for command %O with options %O', alias, commandName, expansions) + this.#aliasExpansions.set(alias, expansions) + } else { + debug('registering alias %O for command %O', alias, commandName) + } + return this } diff --git a/tests/kernel/exec.spec.ts b/tests/kernel/exec.spec.ts index b56aaeb..7d0ecb3 100644 --- a/tests/kernel/exec.spec.ts +++ b/tests/kernel/exec.spec.ts @@ -9,6 +9,7 @@ import { test } from '@japa/runner' import { Kernel } from '../../src/kernel.js' +import { args, flags } from '../../index.js' import { BaseCommand } from '../../src/commands/base.js' import { ListLoader } from '../../src/loaders/list_loader.js' @@ -216,4 +217,56 @@ test.group('Kernel | exec', () => { 'Unknown flag "--help". The mentioned flag is not accepted by the command' ) }) + + test('execute command using alias', async ({ assert }) => { + const kernel = Kernel.create() + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + async run() { + return 'executed' + } + } + + kernel.addLoader(new ListLoader([MakeController])) + kernel.addAlias('mc', 'make:controller') + const command = await kernel.exec('mc', []) + + assert.equal(command.result, 'executed') + assert.equal(command.result, 'executed') + assert.equal(command.exitCode, 0) + + assert.isUndefined(kernel.exitCode) + assert.equal(kernel.getState(), 'booted') + }) + + test('expand alias before executing command', async ({ assert }) => { + const kernel = Kernel.create() + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + + @args.string() + declare name: string + + @flags.boolean() + declare resource: boolean + + @flags.boolean() + declare singular: boolean + + async run() { + assert.equal(this.name, 'user') + assert.isTrue(this.resource) + assert.isTrue(this.singular) + } + } + + kernel.addLoader(new ListLoader([MakeController])) + kernel.addAlias('mc', 'make:controller --resource') + const command = await kernel.exec('mc', ['user', '--singular']) + + assert.equal(command.exitCode, 0) + assert.isUndefined(kernel.exitCode) + }) }) diff --git a/tests/kernel/find.spec.ts b/tests/kernel/find.spec.ts index 34ca8ad..dfc1b4d 100644 --- a/tests/kernel/find.spec.ts +++ b/tests/kernel/find.spec.ts @@ -53,6 +53,24 @@ test.group('Kernel | find', () => { assert.strictEqual(await kernel.find('controller'), MakeController) }) + test('find command using the command alias with flags', async ({ assert }) => { + const kernel = Kernel.create() + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + } + + class MakeModel extends BaseCommand { + static commandName = 'make:model' + } + + kernel.addLoader(new ListLoader([MakeController, MakeModel])) + kernel.addAlias('controller', 'make:controller --resource') + await kernel.boot() + + assert.strictEqual(await kernel.find('controller'), MakeController) + }) + test('raise error when unable to find command', async ({ assert }) => { const kernel = Kernel.create() diff --git a/tests/kernel/handle.spec.ts b/tests/kernel/handle.spec.ts index e37f1ae..3bdef65 100644 --- a/tests/kernel/handle.spec.ts +++ b/tests/kernel/handle.spec.ts @@ -13,6 +13,7 @@ import { Kernel } from '../../src/kernel.js' import { CommandOptions } from '../../src/types.js' import { BaseCommand } from '../../src/commands/base.js' import { ListLoader } from '../../src/loaders/list_loader.js' +import { args, flags } from '../../index.js' test.group('Kernel | handle', (group) => { group.each.teardown(() => { @@ -220,7 +221,7 @@ test.group('Kernel | handle', (group) => { assert.equal(kernel.getState(), 'completed') }) - test('test if a command is a main command or not', async ({ assert }) => { + test('test if a command is a main command', async ({ assert }) => { const kernel = Kernel.create() class MakeController extends BaseCommand { static commandName = 'make:controller' @@ -238,4 +239,55 @@ test.group('Kernel | handle', (group) => { assert.equal(kernel.exitCode, 0) assert.equal(kernel.getState(), 'completed') }) + + test('execute command using alias', async ({ assert }) => { + const kernel = Kernel.create() + class MakeController extends BaseCommand { + static commandName = 'make:controller' + async run() { + assert.equal(this.kernel.getState(), 'running') + assert.strictEqual(this.kernel.getMainCommand(), this) + return 'executed' + } + } + + kernel.addLoader(new ListLoader([MakeController])) + kernel.addAlias('mc', 'make:controller') + await kernel.handle(['mc']) + + assert.equal(kernel.exitCode, 0) + assert.equal(kernel.getState(), 'completed') + }) + + test('expand alias before executing command', async ({ assert }) => { + const kernel = Kernel.create() + class MakeController extends BaseCommand { + static commandName = 'make:controller' + + @args.string() + declare name: string + + @flags.boolean() + declare resource: boolean + + @flags.boolean() + declare singular: boolean + + async run() { + assert.equal(this.kernel.getState(), 'running') + assert.strictEqual(this.kernel.getMainCommand(), this) + assert.equal(this.name, 'user') + assert.isTrue(this.resource) + assert.isTrue(this.singular) + return 'executed' + } + } + + kernel.addLoader(new ListLoader([MakeController])) + kernel.addAlias('mc', 'make:controller --resource') + await kernel.handle(['mc', 'user', '--singular']) + + assert.equal(kernel.exitCode, 0) + assert.equal(kernel.getState(), 'completed') + }) }) diff --git a/tests/kernel/main.spec.ts b/tests/kernel/main.spec.ts index 6acebf8..bae24e7 100644 --- a/tests/kernel/main.spec.ts +++ b/tests/kernel/main.spec.ts @@ -307,4 +307,20 @@ test.group('Kernel', () => { const kernel = Kernel.create() assert.isTrue(kernel.shortcircuit()) }) + + test('define aliases with flags', async ({ assert }) => { + const kernel = Kernel.create() + + class MakeController extends BaseCommand { + static commandName = 'make:controller' + } + + kernel.addLoader(new ListLoader([MakeController])) + kernel.addAlias('resource', 'make:controller --resource') + await kernel.boot() + + assert.deepEqual(kernel.getCommandAliases('make:controller'), ['resource']) + assert.deepEqual(kernel.getAliases(), ['resource']) + assert.deepEqual(kernel.getAliasCommand('resource')?.commandName, 'make:controller') + }) }) From 440784edc40a407ac47c1995385376cc5ff7c727 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 2 Feb 2023 12:09:11 +0530 Subject: [PATCH 025/112] refactor: hydrate commands outside of constructor Hydrating commands inside constructor does not work during inheritence, since the child command will re-declare the properties set by the parent base command during hydration --- src/commands/base.ts | 6 +++--- src/kernel.ts | 3 +++ src/types.ts | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/commands/base.ts b/src/commands/base.ts index 13d5ef0..3f460ed 100644 --- a/src/commands/base.ts +++ b/src/commands/base.ts @@ -421,13 +421,13 @@ export class BaseCommand extends Macroable { public prompt: Prompt ) { super() - this.#consumeParsedOutput() } /** - * Consume the parsed output and set property values on the command + * Hydrate command by setting class properties from + * the parsed output */ - #consumeParsedOutput() { + hydrate() { const CommandConstructor = this.constructor as typeof BaseCommand /** diff --git a/src/kernel.ts b/src/kernel.ts index 36fb2e8..a15d522 100644 --- a/src/kernel.ts +++ b/src/kernel.ts @@ -222,6 +222,8 @@ export class Kernel { * Construct command instance using the executor */ const commandInstance = await this.#executor.create(Command, parsed, this) + commandInstance.hydrate() + return commandInstance as InstanceType } @@ -316,6 +318,7 @@ export class Kernel { * Keep a note of the main command */ this.#mainCommand = await this.#executor.create(Command, parsed, this) + this.#mainCommand.hydrate() /** * Execute the command using the executor diff --git a/src/types.ts b/src/types.ts index d47651a..e8a6a28 100644 --- a/src/types.ts +++ b/src/types.ts @@ -387,5 +387,5 @@ export type AbstractBaseCommand = { flagsParserOptions: Required argumentsParserOptions: ArgumentsParserOptions[] } - new (...args: any[]): any + new (...args: any[]): { hydrate(): void; exitCode?: number } } From b33f0415af87f2b4cbec32ac3ecc8b9743827f73 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 2 Feb 2023 12:11:54 +0530 Subject: [PATCH 026/112] test: add test for command.toJSON method --- tests/base_command/exec.spec.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/base_command/exec.spec.ts b/tests/base_command/exec.spec.ts index b1122e1..2a801d0 100644 --- a/tests/base_command/exec.spec.ts +++ b/tests/base_command/exec.spec.ts @@ -34,6 +34,21 @@ test.group('Base command | execute', () => { await model.exec() assert.deepEqual(model.stack, ['run']) + + assert.deepEqual(model.toJSON(), { + args: ['user'], + commandName: '', + error: undefined, + exitCode: 0, + flags: { + connection: 'sqlite', + }, + options: { + allowUnknownFlags: false, + staysAlive: false, + }, + result: undefined, + }) }) test('store run method return value in the result property', async ({ assert }) => { From 07380ecf80a388c6bdba01bf4b4c0abf42feb687 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 2 Feb 2023 12:18:58 +0530 Subject: [PATCH 027/112] chore(release): 12.1.0-0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 71dc4c3..52b5e7c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/ace", - "version": "12.0.0-0", + "version": "12.1.0-0", "description": "A CLI framework for Node.js", "main": "build/index.js", "type": "module", From fc4d850d6902d4189f9872846bcfffb28e8ca4d8 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 2 Feb 2023 13:11:02 +0530 Subject: [PATCH 028/112] feat(index_generator): add support for ignore commands starting with _ --- src/generators/index_generator.ts | 11 +++- src/loaders/fs_loader.ts | 14 +++-- tests/index_generator.spec.ts | 100 ++++++++++++++++++++++++++++++ tests/loaders/fs_loader.spec.ts | 5 +- 4 files changed, 122 insertions(+), 8 deletions(-) diff --git a/src/generators/index_generator.ts b/src/generators/index_generator.ts index 9ff5fba..7805b3e 100644 --- a/src/generators/index_generator.ts +++ b/src/generators/index_generator.ts @@ -7,7 +7,7 @@ * file that was distributed with this source code. */ -import { join } from 'node:path' +import { basename, join } from 'node:path' import { copyFile, mkdir, writeFile } from 'node:fs/promises' import { stubsRoot } from '../../stubs/main.js' @@ -33,7 +33,14 @@ export class IndexGenerator { * Generate index */ async generate(): Promise { - const commandsMetaData = await new FsLoader(this.#commandsDir, ['main.js']).getMetaData() + const commandsMetaData = await new FsLoader(this.#commandsDir, (filePath: string) => { + if (filePath.startsWith('_') || basename(filePath).startsWith('_')) { + return false + } + + return true + }).getMetaData() + const indexJSON = JSON.stringify({ commands: commandsMetaData, version: 1 }) const indexFile = join(this.#commandsDir, 'commands.json') diff --git a/src/loaders/fs_loader.ts b/src/loaders/fs_loader.ts index 0e183f8..e17f239 100644 --- a/src/loaders/fs_loader.ts +++ b/src/loaders/fs_loader.ts @@ -28,18 +28,18 @@ export class FsLoader implements LoadersCon #comandsDirectory: string /** - * Paths to ignore + * File to ignore files */ - #ignorePaths: string[] + #filter?: (filePath: string) => boolean /** * An array of loaded commands */ #commands: { command: Command; filePath: string }[] = [] - constructor(comandsDirectory: string, ignorePaths?: string[]) { + constructor(comandsDirectory: string, filter?: (filePath: string) => boolean) { this.#comandsDirectory = comandsDirectory - this.#ignorePaths = ignorePaths || [] + this.#filter = filter } /** @@ -83,7 +83,11 @@ export class FsLoader implements LoadersCon const relativeFileName = slash(relative(this.#comandsDirectory, fileURLToPath(file))) - if (!this.#ignorePaths?.includes(relativeFileName)) { + /** + * Import file if no filters are defined or the filter + * allows the file + */ + if (!this.#filter || this.#filter(relativeFileName)) { commands[relativeFileName] = await importDefault(() => import(file), relativeFileName) } } diff --git a/tests/index_generator.spec.ts b/tests/index_generator.spec.ts index c3acd5c..3a1e2fb 100644 --- a/tests/index_generator.spec.ts +++ b/tests/index_generator.spec.ts @@ -76,4 +76,104 @@ test.group('Index generator', (group) => { const command = await loader.getCommand(metaData[0]) validateCommand(command, './main.ts') }) + + test('ignore directories starting with _', async ({ assert }) => { + await fs.outputFile( + join(BASE_PATH, 'commands', '_make', 'make_controller_v_2.ts'), + ` + export default class MakeController { + static commandName = 'make:controller' + static args = [] + static flags = [] + static aliases = [] + static options = {} + static description = '' + static namespace = 'make' + + static serialize() { + return { + commandName: this.commandName, + description: this.description, + namespace: this.namespace, + args: this.args, + flags: this.flags, + options: this.options, + aliases: this.aliases, + } + } + } + ` + ) + + const generator = new IndexGenerator(join(BASE_PATH, 'commands')) + await generator.generate() + + /** + * Validate index + */ + const indexJSON = await fs.readFile(join(BASE_PATH, 'commands', 'commands.json'), 'utf-8') + const commandsIndex = JSON.parse(indexJSON) + + assert.properties(commandsIndex, ['commands', 'version']) + assert.equal(commandsIndex.version, 1) + assert.isArray(commandsIndex.commands) + assert.lengthOf(commandsIndex.commands, 0) + + /** + * Validate loader + */ + const loader = await import(new URL('./commands/main.js?v=1', BASE_URL).href) + const metaData = await loader.getMetaData() + assert.lengthOf(metaData, 0) + }) + + test('ignore files starting with _', async ({ assert }) => { + await fs.outputFile( + join(BASE_PATH, 'commands', 'make', '_make_controller_v_2.ts'), + ` + export default class MakeController { + static commandName = 'make:controller' + static args = [] + static flags = [] + static aliases = [] + static options = {} + static description = '' + static namespace = 'make' + + static serialize() { + return { + commandName: this.commandName, + description: this.description, + namespace: this.namespace, + args: this.args, + flags: this.flags, + options: this.options, + aliases: this.aliases, + } + } + } + ` + ) + + const generator = new IndexGenerator(join(BASE_PATH, 'commands')) + await generator.generate() + + /** + * Validate index + */ + const indexJSON = await fs.readFile(join(BASE_PATH, 'commands', 'commands.json'), 'utf-8') + const commandsIndex = JSON.parse(indexJSON) + + assert.properties(commandsIndex, ['commands', 'version']) + assert.equal(commandsIndex.version, 1) + assert.isArray(commandsIndex.commands) + assert.lengthOf(commandsIndex.commands, 0) + + /** + * Validate loader + */ + const loader = await import(new URL('./commands/main.js?v=2', BASE_URL).href) + const metaData = await loader.getMetaData() + assert.lengthOf(metaData, 0) + }) }) diff --git a/tests/loaders/fs_loader.spec.ts b/tests/loaders/fs_loader.spec.ts index 6d0902b..3fb825f 100644 --- a/tests/loaders/fs_loader.spec.ts +++ b/tests/loaders/fs_loader.spec.ts @@ -317,7 +317,10 @@ test.group('Loaders | fs', (group) => { ` ) - const loader = new FsLoader(join(BASE_PATH, './commands'), ['make_controller_v_2.js']) + const loader = new FsLoader( + join(BASE_PATH, './commands'), + (filePath: string) => filePath !== 'make_controller_v_2.js' + ) const commands = await loader.getMetaData() assert.deepEqual(commands, [ { From 6becbb42dd3984a2d5f051cbb90b539be37f955e Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 3 Feb 2023 09:58:16 +0530 Subject: [PATCH 029/112] refactor: hydrate command before executing it --- src/commands/base.ts | 13 +++++++++++++ src/commands/help.ts | 4 ++++ 2 files changed, 17 insertions(+) diff --git a/src/commands/base.ts b/src/commands/base.ts index 3f460ed..be32477 100644 --- a/src/commands/base.ts +++ b/src/commands/base.ts @@ -375,6 +375,11 @@ export class BaseCommand extends Macroable { }) } + /** + * Check if a command has been hypdrated + */ + protected hydrated: boolean = false + /** * The exit code for the command */ @@ -428,6 +433,10 @@ export class BaseCommand extends Macroable { * the parsed output */ hydrate() { + if (this.hydrated) { + return + } + const CommandConstructor = this.constructor as typeof BaseCommand /** @@ -453,6 +462,8 @@ export class BaseCommand extends Macroable { configurable: true, }) }) + + this.hydrated = true } /** @@ -465,6 +476,8 @@ export class BaseCommand extends Macroable { * Executes the commands by running the command's run method. */ async exec() { + this.hydrate() + try { this.result = await this.run() this.exitCode = this.exitCode ?? 0 diff --git a/src/commands/help.ts b/src/commands/help.ts index 3dbdda8..392279f 100644 --- a/src/commands/help.ts +++ b/src/commands/help.ts @@ -106,6 +106,10 @@ export class HelpCommand extends BaseCommand { endColumn: TERMINAL_SIZE, }).join('\n') + if (!description) { + return + } + this.logger.log('') this.logger.log(this.colors.yellow('Description:')) this.logger.log(description) From d10c36570d41d3b6538b20154d3fbd56a039e6bb Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 3 Feb 2023 15:12:47 +0530 Subject: [PATCH 030/112] chore(release): 12.1.1-0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 52b5e7c..ac1e995 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/ace", - "version": "12.1.0-0", + "version": "12.1.1-0", "description": "A CLI framework for Node.js", "main": "build/index.js", "type": "module", From 8237d6da99df8709fd58a431021640652922bfca Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 13 Feb 2023 14:14:20 +0530 Subject: [PATCH 031/112] chore: update dependencies --- package.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index ac1e995..bc7b04c 100644 --- a/package.json +++ b/package.json @@ -55,28 +55,28 @@ "string-similarity": "^4.0.4", "string-width": "^5.1.2", "yargs-parser": "^21.1.1", - "youch": "^3.2.2", - "youch-terminal": "^2.1.5" + "youch": "^3.2.3", + "youch-terminal": "^2.2.0" }, "devDependencies": { "@commitlint/cli": "^17.4.2", "@commitlint/config-conventional": "^17.4.2", - "@japa/assert": "^1.3.5", - "@japa/expect-type": "^1.0.2", - "@japa/run-failed-tests": "^1.0.8", - "@japa/runner": "^2.1.1", - "@japa/spec-reporter": "^1.2.0", + "@japa/assert": "^1.4.1", + "@japa/expect-type": "^1.0.3", + "@japa/run-failed-tests": "^1.1.1", + "@japa/runner": "^2.3.0", + "@japa/spec-reporter": "^1.3.3", "@poppinss/dev-utils": "^2.0.3", - "@swc/core": "^1.3.29", + "@swc/core": "^1.3.35", "@types/fs-extra": "^11.0.1", - "@types/node": "^18.7.15", + "@types/node": "^18.13.0", "@types/sinon": "^10.0.13", "@types/string-similarity": "^4.0.0", "c8": "^7.12.0", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", "del-cli": "^5.0.0", - "eslint": "^8.32.0", + "eslint": "^8.34.0", "eslint-config-prettier": "^8.6.0", "eslint-plugin-adonis": "^3.0.3", "eslint-plugin-prettier": "^4.2.1", @@ -84,7 +84,7 @@ "github-label-sync": "^2.2.0", "husky": "^8.0.3", "np": "^7.6.3", - "prettier": "^2.8.3", + "prettier": "^2.8.4", "reflect-metadata": "^0.1.13", "sinon": "^15.0.1", "ts-json-schema-generator": "^1.2.0", From 1ccf921a2c4932953affff885ba7bde0e8b66995 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 13 Feb 2023 14:26:05 +0530 Subject: [PATCH 032/112] test: use filesystem plugin --- bin/test.ts | 3 +- package.json | 3 +- tests/formatters/command.spec.ts | 2 +- tests/index_generator.spec.ts | 39 ++++++++-------- tests/kernel/find.spec.ts | 2 +- tests/kernel/handle.spec.ts | 4 +- tests/kernel/main.spec.ts | 2 +- tests/loaders/fs_loader.spec.ts | 79 ++++++++++++++++---------------- 8 files changed, 68 insertions(+), 66 deletions(-) diff --git a/bin/test.ts b/bin/test.ts index ee6d2e9..3c1c21d 100644 --- a/bin/test.ts +++ b/bin/test.ts @@ -1,5 +1,6 @@ import { assert } from '@japa/assert' import { pathToFileURL } from 'node:url' +import { fileSystem } from '@japa/file-system' import { expectTypeOf } from '@japa/expect-type' import { specReporter } from '@japa/spec-reporter' import { runFailedTests } from '@japa/run-failed-tests' @@ -22,7 +23,7 @@ configure({ ...processCliArgs(process.argv.slice(2)), ...{ files: ['tests/**/*.spec.ts'], - plugins: [assert(), runFailedTests(), expectTypeOf()], + plugins: [assert(), runFailedTests(), expectTypeOf(), fileSystem()], reporters: [specReporter()], importer: (filePath: string) => import(pathToFileURL(filePath).href), }, diff --git a/package.json b/package.json index bc7b04c..e5ab142 100644 --- a/package.json +++ b/package.json @@ -63,12 +63,12 @@ "@commitlint/config-conventional": "^17.4.2", "@japa/assert": "^1.4.1", "@japa/expect-type": "^1.0.3", + "@japa/file-system": "^1.0.1", "@japa/run-failed-tests": "^1.1.1", "@japa/runner": "^2.3.0", "@japa/spec-reporter": "^1.3.3", "@poppinss/dev-utils": "^2.0.3", "@swc/core": "^1.3.35", - "@types/fs-extra": "^11.0.1", "@types/node": "^18.13.0", "@types/sinon": "^10.0.13", "@types/string-similarity": "^4.0.0", @@ -80,7 +80,6 @@ "eslint-config-prettier": "^8.6.0", "eslint-plugin-adonis": "^3.0.3", "eslint-plugin-prettier": "^4.2.1", - "fs-extra": "^11.1.0", "github-label-sync": "^2.2.0", "husky": "^8.0.3", "np": "^7.6.3", diff --git a/tests/formatters/command.spec.ts b/tests/formatters/command.spec.ts index 0a8f830..cea5ecd 100644 --- a/tests/formatters/command.spec.ts +++ b/tests/formatters/command.spec.ts @@ -9,9 +9,9 @@ import { test } from '@japa/runner' import { colors } from '@poppinss/cliui' -import { BaseCommand } from '../../src/commands/base.js' import { args } from '../../src/decorators/args.js' import { flags } from '../../src/decorators/flags.js' +import { BaseCommand } from '../../src/commands/base.js' import { CommandFormatter } from '../../src/formatters/command.js' test.group('Formatters | command', () => { diff --git a/tests/index_generator.spec.ts b/tests/index_generator.spec.ts index 3a1e2fb..e366055 100644 --- a/tests/index_generator.spec.ts +++ b/tests/index_generator.spec.ts @@ -7,7 +7,6 @@ * file that was distributed with this source code. */ -import fs from 'fs-extra' import { join } from 'node:path' import { test } from '@japa/runner' import { fileURLToPath } from 'node:url' @@ -18,13 +17,17 @@ const BASE_URL = new URL('./tmp/', import.meta.url) const BASE_PATH = fileURLToPath(BASE_URL) test.group('Index generator', (group) => { - group.each.setup(() => { - return () => fs.remove(BASE_PATH) + group.each.setup(({ context }) => { + context.fs.baseUrl = BASE_URL + context.fs.basePath = BASE_PATH }) - test('generate loader and commands index by scanning commands directory', async ({ assert }) => { - await fs.outputFile( - join(BASE_PATH, 'commands', 'make_controller_v_2.ts'), + test('generate loader and commands index by scanning commands directory', async ({ + assert, + fs, + }) => { + await fs.create( + 'commands/make_controller_v_2.ts', ` export default class MakeController { static commandName = 'make:controller' @@ -50,13 +53,13 @@ test.group('Index generator', (group) => { ` ) - const generator = new IndexGenerator(join(BASE_PATH, 'commands')) + const generator = new IndexGenerator(join(fs.basePath, 'commands')) await generator.generate() /** * Validate index */ - const indexJSON = await fs.readFile(join(BASE_PATH, 'commands', 'commands.json'), 'utf-8') + const indexJSON = await fs.contents('commands/commands.json') const commandsIndex = JSON.parse(indexJSON) assert.properties(commandsIndex, ['commands', 'version']) @@ -77,9 +80,9 @@ test.group('Index generator', (group) => { validateCommand(command, './main.ts') }) - test('ignore directories starting with _', async ({ assert }) => { - await fs.outputFile( - join(BASE_PATH, 'commands', '_make', 'make_controller_v_2.ts'), + test('ignore directories starting with _', async ({ assert, fs }) => { + await fs.create( + 'commands/_make/make_controller_v_2.ts', ` export default class MakeController { static commandName = 'make:controller' @@ -105,13 +108,13 @@ test.group('Index generator', (group) => { ` ) - const generator = new IndexGenerator(join(BASE_PATH, 'commands')) + const generator = new IndexGenerator(join(fs.basePath, 'commands')) await generator.generate() /** * Validate index */ - const indexJSON = await fs.readFile(join(BASE_PATH, 'commands', 'commands.json'), 'utf-8') + const indexJSON = await fs.contents('commands/commands.json') const commandsIndex = JSON.parse(indexJSON) assert.properties(commandsIndex, ['commands', 'version']) @@ -127,9 +130,9 @@ test.group('Index generator', (group) => { assert.lengthOf(metaData, 0) }) - test('ignore files starting with _', async ({ assert }) => { - await fs.outputFile( - join(BASE_PATH, 'commands', 'make', '_make_controller_v_2.ts'), + test('ignore files starting with _', async ({ assert, fs }) => { + await fs.create( + 'commands/make/_make_controller_v_2.ts', ` export default class MakeController { static commandName = 'make:controller' @@ -155,13 +158,13 @@ test.group('Index generator', (group) => { ` ) - const generator = new IndexGenerator(join(BASE_PATH, 'commands')) + const generator = new IndexGenerator(join(fs.basePath, 'commands')) await generator.generate() /** * Validate index */ - const indexJSON = await fs.readFile(join(BASE_PATH, 'commands', 'commands.json'), 'utf-8') + const indexJSON = await fs.contents('commands/commands.json') const commandsIndex = JSON.parse(indexJSON) assert.properties(commandsIndex, ['commands', 'version']) diff --git a/tests/kernel/find.spec.ts b/tests/kernel/find.spec.ts index dfc1b4d..78cb92b 100644 --- a/tests/kernel/find.spec.ts +++ b/tests/kernel/find.spec.ts @@ -11,8 +11,8 @@ import { test } from '@japa/runner' import { Kernel } from '../../src/kernel.js' import { BaseCommand } from '../../src/commands/base.js' +import type { CommandMetaData } from '../../src/types.js' import { ListLoader } from '../../src/loaders/list_loader.js' -import { CommandMetaData } from '../../src/types.js' test.group('Kernel | find', () => { test('find commands registered using a loader', async ({ assert }) => { diff --git a/tests/kernel/handle.spec.ts b/tests/kernel/handle.spec.ts index 3bdef65..a5efb3d 100644 --- a/tests/kernel/handle.spec.ts +++ b/tests/kernel/handle.spec.ts @@ -10,10 +10,10 @@ import { test } from '@japa/runner' import { cliui } from '@poppinss/cliui' import { Kernel } from '../../src/kernel.js' -import { CommandOptions } from '../../src/types.js' +import { args, flags } from '../../index.js' import { BaseCommand } from '../../src/commands/base.js' +import type { CommandOptions } from '../../src/types.js' import { ListLoader } from '../../src/loaders/list_loader.js' -import { args, flags } from '../../index.js' test.group('Kernel | handle', (group) => { group.each.teardown(() => { diff --git a/tests/kernel/main.spec.ts b/tests/kernel/main.spec.ts index bae24e7..ee117c6 100644 --- a/tests/kernel/main.spec.ts +++ b/tests/kernel/main.spec.ts @@ -10,9 +10,9 @@ import { test } from '@japa/runner' import { Kernel } from '../../src/kernel.js' +import { ListCommand } from '../../src/commands/list.js' import { BaseCommand } from '../../src/commands/base.js' import { ListLoader } from '../../src/loaders/list_loader.js' -import { ListCommand } from '../../src/commands/list.js' test.group('Kernel', () => { test('get alphabetically sorted list of commands', async ({ assert }) => { diff --git a/tests/loaders/fs_loader.spec.ts b/tests/loaders/fs_loader.spec.ts index 3fb825f..716fc83 100644 --- a/tests/loaders/fs_loader.spec.ts +++ b/tests/loaders/fs_loader.spec.ts @@ -7,44 +7,43 @@ * file that was distributed with this source code. */ -import fs from 'fs-extra' import { join } from 'node:path' import { test } from '@japa/runner' import { fileURLToPath } from 'node:url' - import { FsLoader } from '../../src/loaders/fs_loader.js' const BASE_URL = new URL('./tmp/', import.meta.url) const BASE_PATH = fileURLToPath(BASE_URL) test.group('Loaders | fs', (group) => { - group.each.setup(() => { - return () => fs.remove(BASE_PATH) + group.each.setup(({ context }) => { + context.fs.baseUrl = BASE_URL + context.fs.basePath = BASE_PATH }) - test('do not raise error when commands directory does not exists', async ({ assert }) => { - const loader = new FsLoader(join(BASE_PATH, './commands')) + test('do not raise error when commands directory does not exists', async ({ assert, fs }) => { + const loader = new FsLoader(join(fs.basePath, './commands')) await assert.doesNotRejects(() => loader.getMetaData()) }) - test('raise error when there is no default export in command file', async ({ assert }) => { - await fs.outputFile( - join(BASE_PATH, 'commands', 'make_controller_v_1.ts'), + test('raise error when there is no default export in command file', async ({ assert, fs }) => { + await fs.create( + 'commands/make_controller_v_1.ts', ` export class MakeController {} ` ) - const loader = new FsLoader(join(BASE_PATH, './commands')) + const loader = new FsLoader(join(fs.basePath, './commands')) await assert.rejects( () => loader.getMetaData(), 'Missing "export default" in module "make_controller_v_1.js"' ) }) - test('return commands metadata', async ({ assert }) => { - await fs.outputFile( - join(BASE_PATH, 'commands', 'make_controller_v_2.ts'), + test('return commands metadata', async ({ assert, fs }) => { + await fs.create( + 'commands/make_controller_v_2.ts', ` export default class MakeController { static commandName = 'make:controller' @@ -70,7 +69,7 @@ test.group('Loaders | fs', (group) => { ` ) - const loader = new FsLoader(join(BASE_PATH, './commands')) + const loader = new FsLoader(join(fs.basePath, './commands')) const commands = await loader.getMetaData() assert.deepEqual(commands, [ { @@ -86,9 +85,9 @@ test.group('Loaders | fs', (group) => { ]) }) - test('load command from .js files', async ({ assert }) => { - await fs.outputFile( - join(BASE_PATH, 'commands', 'make_controller.js'), + test('load command from .js files', async ({ assert, fs }) => { + await fs.create( + 'commands/make_controller.js', ` export default class MakeController { static commandName = 'make:controller' @@ -114,7 +113,7 @@ test.group('Loaders | fs', (group) => { ` ) - const loader = new FsLoader(join(BASE_PATH, './commands')) + const loader = new FsLoader(join(fs.basePath, './commands')) const commands = await loader.getMetaData() assert.deepEqual(commands, [ { @@ -130,9 +129,9 @@ test.group('Loaders | fs', (group) => { ]) }) - test('ignore .json files', async ({ assert }) => { - await fs.outputFile( - join(BASE_PATH, 'commands', 'make_controller_v_3.ts'), + test('ignore .json files', async ({ assert, fs }) => { + await fs.create( + 'commands/make_controller_v_3.ts', ` export default class MakeController { static commandName = 'make:controller' @@ -158,9 +157,9 @@ test.group('Loaders | fs', (group) => { ` ) - await fs.outputFile(join(BASE_PATH, 'commands', 'foo.json'), `{}`) + await fs.create('commands/foo.json', `{}`) - const loader = new FsLoader(join(BASE_PATH, './commands')) + const loader = new FsLoader(join(fs.basePath, './commands')) const commands = await loader.getMetaData() assert.deepEqual(commands, [ { @@ -176,9 +175,9 @@ test.group('Loaders | fs', (group) => { ]) }) - test('get command constructor for a given command', async ({ assert }) => { - await fs.outputFile( - join(BASE_PATH, 'commands', 'make_controller_v_5.ts'), + test('get command constructor for a given command', async ({ assert, fs }) => { + await fs.create( + 'commands/make_controller_v_5.ts', ` export default class MakeController { static commandName = 'make:controller' @@ -204,23 +203,23 @@ test.group('Loaders | fs', (group) => { ` ) - await fs.outputFile(join(BASE_PATH, 'commands', 'foo.json'), `{}`) + await fs.create('commands/foo.json', `{}`) - const loader = new FsLoader(join(BASE_PATH, './commands')) + const loader = new FsLoader(join(fs.basePath, './commands')) const commands = await loader.getMetaData() const command = await loader.getCommand(commands[0]) assert.isFunction(command) }) - test('return null when unable to lookup command', async ({ assert }) => { - const loader = new FsLoader(join(BASE_PATH, './commands')) + test('return null when unable to lookup command', async ({ assert, fs }) => { + const loader = new FsLoader(join(fs.basePath, './commands')) const command = await loader.getCommand({ commandName: 'make:model' } as any) assert.isNull(command) }) - test('load commands from nested directories', async ({ assert }) => { - await fs.outputFile( - join(BASE_PATH, 'commands', 'make', 'controller.ts'), + test('load commands from nested directories', async ({ assert, fs }) => { + await fs.create( + 'commands/make/controller.ts', ` export default class MakeController { static commandName = 'make:controller' @@ -246,7 +245,7 @@ test.group('Loaders | fs', (group) => { ` ) - const loader = new FsLoader(join(BASE_PATH, './commands')) + const loader = new FsLoader(join(fs.basePath, './commands')) const commands = await loader.getMetaData() assert.deepEqual(commands, [ { @@ -262,9 +261,9 @@ test.group('Loaders | fs', (group) => { ]) }) - test('ignore commands by filename', async ({ assert }) => { - await fs.outputFile( - join(BASE_PATH, 'commands', 'make_controller_v_2.ts'), + test('ignore commands using filters', async ({ assert, fs }) => { + await fs.create( + 'commands/make_controller_v_2.ts', ` export default class MakeController { static commandName = 'make:controller' @@ -290,8 +289,8 @@ test.group('Loaders | fs', (group) => { ` ) - await fs.outputFile( - join(BASE_PATH, 'commands', 'make', 'controller.ts'), + await fs.create( + 'commands/make/controller.ts', ` export default class MakeController { static commandName = 'make:controller' @@ -318,7 +317,7 @@ test.group('Loaders | fs', (group) => { ) const loader = new FsLoader( - join(BASE_PATH, './commands'), + join(fs.basePath, './commands'), (filePath: string) => filePath !== 'make_controller_v_2.js' ) const commands = await loader.getMetaData() From 30a85a0b1764712469ac3e7022daea8ef2199b72 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 13 Feb 2023 14:32:34 +0530 Subject: [PATCH 033/112] refactor(FsLoader): ignore file names prefixed with _ --- src/generators/index_generator.ts | 10 ++-------- src/loaders/fs_loader.ts | 10 +++++++++- tests/index_generator.spec.ts | 13 +++++++++---- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/generators/index_generator.ts b/src/generators/index_generator.ts index 7805b3e..f37cd6d 100644 --- a/src/generators/index_generator.ts +++ b/src/generators/index_generator.ts @@ -7,7 +7,7 @@ * file that was distributed with this source code. */ -import { basename, join } from 'node:path' +import { join } from 'node:path' import { copyFile, mkdir, writeFile } from 'node:fs/promises' import { stubsRoot } from '../../stubs/main.js' @@ -33,13 +33,7 @@ export class IndexGenerator { * Generate index */ async generate(): Promise { - const commandsMetaData = await new FsLoader(this.#commandsDir, (filePath: string) => { - if (filePath.startsWith('_') || basename(filePath).startsWith('_')) { - return false - } - - return true - }).getMetaData() + const commandsMetaData = await new FsLoader(this.#commandsDir).getMetaData() const indexJSON = JSON.stringify({ commands: commandsMetaData, version: 1 }) const indexFile = join(this.#commandsDir, 'commands.json') diff --git a/src/loaders/fs_loader.ts b/src/loaders/fs_loader.ts index e17f239..b2b2d2b 100644 --- a/src/loaders/fs_loader.ts +++ b/src/loaders/fs_loader.ts @@ -8,7 +8,7 @@ */ import { fileURLToPath } from 'node:url' -import { extname, relative } from 'node:path' +import { basename, extname, relative } from 'node:path' import { fsReadAll, importDefault, slash } from '@poppinss/utils' import { validateCommand } from '../helpers.js' @@ -57,6 +57,14 @@ export class FsLoader implements LoadersCon ignoreMissingRoot: true, filter: (filePath: string) => { const ext = extname(filePath) + + /** + * Ignore files prefixed with _ + */ + if (basename(filePath).startsWith('_')) { + return false + } + if (JS_MODULES.includes(ext)) { return true } diff --git a/tests/index_generator.spec.ts b/tests/index_generator.spec.ts index e366055..435eea8 100644 --- a/tests/index_generator.spec.ts +++ b/tests/index_generator.spec.ts @@ -80,9 +80,9 @@ test.group('Index generator', (group) => { validateCommand(command, './main.ts') }) - test('ignore directories starting with _', async ({ assert, fs }) => { + test('index directories starting with _', async ({ assert, fs }) => { await fs.create( - 'commands/_make/make_controller_v_2.ts', + 'commands/_make/controller_v_3.ts', ` export default class MakeController { static commandName = 'make:controller' @@ -120,14 +120,19 @@ test.group('Index generator', (group) => { assert.properties(commandsIndex, ['commands', 'version']) assert.equal(commandsIndex.version, 1) assert.isArray(commandsIndex.commands) - assert.lengthOf(commandsIndex.commands, 0) + commandsIndex.commands.forEach((command: any) => + validateCommandMetaData(command, './commands.json') + ) /** * Validate loader */ const loader = await import(new URL('./commands/main.js?v=1', BASE_URL).href) const metaData = await loader.getMetaData() - assert.lengthOf(metaData, 0) + metaData.forEach((command: any) => validateCommandMetaData(command, './commands.json')) + + const command = await loader.getCommand(metaData[0]) + validateCommand(command, './controller_v_3.ts') }) test('ignore files starting with _', async ({ assert, fs }) => { From 829ffeef11d705a8fad018cb5089396af66c90fe Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 13 Feb 2023 14:36:36 +0530 Subject: [PATCH 034/112] refactor: do not display description label when no command description is set --- src/commands/help.ts | 14 ++++++++------ tests/commands/help.spec.ts | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/src/commands/help.ts b/src/commands/help.ts index 392279f..e028be3 100644 --- a/src/commands/help.ts +++ b/src/commands/help.ts @@ -100,11 +100,7 @@ export class HelpCommand extends BaseCommand { */ protected renderDescription(command: CommandMetaData) { const formatter = new CommandFormatter(command, this.colors) - const description = wrap([formatter.formatDescription()], { - startColumn: 2, - trimStart: false, - endColumn: TERMINAL_SIZE, - }).join('\n') + const description = formatter.formatDescription() if (!description) { return @@ -112,7 +108,13 @@ export class HelpCommand extends BaseCommand { this.logger.log('') this.logger.log(this.colors.yellow('Description:')) - this.logger.log(description) + this.logger.log( + wrap([description], { + startColumn: 2, + trimStart: false, + endColumn: TERMINAL_SIZE, + }).join('\n') + ) } /** diff --git a/tests/commands/help.spec.ts b/tests/commands/help.spec.ts index 442a61c..76fd6d9 100644 --- a/tests/commands/help.spec.ts +++ b/tests/commands/help.spec.ts @@ -388,6 +388,39 @@ test.group('Help command', () => { ]) }) + test('do not display description when not defined', async ({ assert }) => { + const kernel = Kernel.create() + kernel.ui.switchMode('raw') + + class MakeController extends BaseCommand { + static aliases: string[] = ['mc', 'controller'] + static commandName: string = 'make:controller' + } + + kernel.addLoader(new ListLoader([MakeController])) + kernel.info.set('binary', 'node ace') + const command = await kernel.create(HelpCommand, ['make:controller']) + await command.exec() + + assert.equal(command.exitCode, 0) + assert.deepEqual(kernel.ui.logger.getLogs(), [ + { + message: '', + stream: 'stdout', + }, + { + message: 'yellow(Usage:)', + stream: 'stdout', + }, + { + message: [' node ace make:controller ', ' node ace mc ', ' node ace controller '].join( + '\n' + ), + stream: 'stdout', + }, + ]) + }) + test('error when command not found', async ({ assert }) => { const kernel = Kernel.create() kernel.ui.switchMode('raw') From 56e2509269046b7d8b54ad5a29fb8ec42a6ae5c1 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 13 Feb 2023 14:44:44 +0530 Subject: [PATCH 035/112] chore(release): 12.2.0-0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e5ab142..4f524e6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/ace", - "version": "12.1.1-0", + "version": "12.2.0-0", "description": "A CLI framework for Node.js", "main": "build/index.js", "type": "module", From d961a43a1db9a0f1ac410bf8478616ea7182f525 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Sun, 26 Feb 2023 10:13:56 +0530 Subject: [PATCH 036/112] chore: update dependencies --- package.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 4f524e6..4f12fab 100644 --- a/package.json +++ b/package.json @@ -59,20 +59,20 @@ "youch-terminal": "^2.2.0" }, "devDependencies": { - "@commitlint/cli": "^17.4.2", - "@commitlint/config-conventional": "^17.4.2", + "@commitlint/cli": "^17.4.4", + "@commitlint/config-conventional": "^17.4.4", "@japa/assert": "^1.4.1", "@japa/expect-type": "^1.0.3", "@japa/file-system": "^1.0.1", "@japa/run-failed-tests": "^1.1.1", - "@japa/runner": "^2.3.0", + "@japa/runner": "^2.5.1", "@japa/spec-reporter": "^1.3.3", "@poppinss/dev-utils": "^2.0.3", - "@swc/core": "^1.3.35", - "@types/node": "^18.13.0", + "@swc/core": "^1.3.36", + "@types/node": "^18.14.1", "@types/sinon": "^10.0.13", "@types/string-similarity": "^4.0.0", - "c8": "^7.12.0", + "c8": "^7.13.0", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", "del-cli": "^5.0.0", From 0da348029952e78d3fa39f9a91d16046c631fa0b Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 27 Feb 2023 09:40:46 +0530 Subject: [PATCH 037/112] feat: add support for defining node.js args --- src/kernel.ts | 43 +++++++++++++++++++++++++++++++------ src/parser.ts | 1 + src/types.ts | 2 ++ tests/kernel/handle.spec.ts | 38 ++++++++++++++++++++++++++++++++ tests/parser.spec.ts | 19 ++++++++++++++++ 5 files changed, 96 insertions(+), 7 deletions(-) diff --git a/src/kernel.ts b/src/kernel.ts index a15d522..25eb2fa 100644 --- a/src/kernel.ts +++ b/src/kernel.ts @@ -202,6 +202,31 @@ export class Kernel { this.#executor = executor } + /** + * Process command line arguments. All flags before the command + * name are considered as Node.js argv and all flags after + * the command name are considered as command argv. + * + * The behavior is same as Node.js CLI, where all flags before the + * script file name are Node.js argv. + */ + #processArgv(argv: string[]) { + const commandNameIndex = argv.findIndex((value) => !value.startsWith('-')) + if (commandNameIndex === -1) { + return { + nodeArgv: [], + commandName: null, + commandArgv: argv, + } + } + + return { + nodeArgv: argv.slice(0, commandNameIndex), + commandName: argv[commandNameIndex], + commandArgv: argv.slice(commandNameIndex + 1), + } + } + /** * Creates an instance of a command by parsing and validating * the command line arguments. @@ -259,7 +284,7 @@ export class Kernel { * Executes the main command and handles the exceptions by * reporting them */ - async #execMain(commandName: string, argv: string[]) { + async #execMain(commandName: string, nodeArgv: string[], argv: string[]) { try { const Command = await this.find(commandName) @@ -279,6 +304,11 @@ export class Kernel { Command.getParserOptions(this.#globalCommand.getParserOptions().flagsParserOptions) ).parse(argv) + /** + * Defined only for the main command + */ + parsed.nodeArgs = nodeArgv + /** * Validate the flags against the global list as well */ @@ -728,22 +758,21 @@ export class Kernel { } this.#state = 'running' + const { commandName, nodeArgv, commandArgv } = this.#processArgv(argv) /** - * Run the default command when no argv are defined - * or if only flags are mentioned + * Run the default command */ - if (!argv.length || argv[0].startsWith('-')) { + if (!commandName) { debug('running default command "%s"', this.#defaultCommand.commandName) - return this.#execMain(this.#defaultCommand.commandName, argv) + return this.#execMain(this.#defaultCommand.commandName, nodeArgv, commandArgv) } /** * Run the mentioned command as the main command */ - const [commandName, ...args] = argv debug('running main command "%s"', commandName) - return this.#execMain(commandName, args) + return this.#execMain(commandName, nodeArgv, commandArgv) } /** diff --git a/src/parser.ts b/src/parser.ts index 03e7d54..d3c2c8e 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -126,6 +126,7 @@ export class Parser { return { args: output, + nodeArgs: [], _: args.slice(lastParsedIndex === -1 ? 0 : lastParsedIndex), unknownFlags: this.#scanUnknownFlags(rest), flags: rest, diff --git a/src/types.ts b/src/types.ts index e8a6a28..96b9f12 100644 --- a/src/types.ts +++ b/src/types.ts @@ -22,6 +22,8 @@ export type YargsOutput = Arguments * Parsed output from the parser */ export type ParsedOutput = YargsOutput & { + nodeArgs: string[] + /** * Parsed arguments */ diff --git a/tests/kernel/handle.spec.ts b/tests/kernel/handle.spec.ts index a5efb3d..8c0aefb 100644 --- a/tests/kernel/handle.spec.ts +++ b/tests/kernel/handle.spec.ts @@ -290,4 +290,42 @@ test.group('Kernel | handle', (group) => { assert.equal(kernel.exitCode, 0) assert.equal(kernel.getState(), 'completed') }) + + test('treat flags before the command name as nodeArgs', async ({ assert }) => { + const kernel = Kernel.create() + class MakeController extends BaseCommand { + static commandName = 'make:controller' + async run() { + assert.deepEqual(this.parsed.nodeArgs, ['--no-warnings']) + assert.equal(this.kernel.getState(), 'running') + assert.strictEqual(this.kernel.getMainCommand(), this) + return 'executed' + } + } + + kernel.addLoader(new ListLoader([MakeController])) + await kernel.handle(['--no-warnings', 'make:controller']) + + assert.equal(kernel.exitCode, 0) + assert.equal(kernel.getState(), 'completed') + }) + + test('treat shorthand flags before the command name as nodeArgs', async ({ assert }) => { + const kernel = Kernel.create() + class MakeController extends BaseCommand { + static commandName = 'make:controller' + async run() { + assert.deepEqual(this.parsed.nodeArgs, ['-w']) + assert.equal(this.kernel.getState(), 'running') + assert.strictEqual(this.kernel.getMainCommand(), this) + return 'executed' + } + } + + kernel.addLoader(new ListLoader([MakeController])) + await kernel.handle(['-w', 'make:controller']) + + assert.equal(kernel.exitCode, 0) + assert.equal(kernel.getState(), 'completed') + }) }) diff --git a/tests/parser.spec.ts b/tests/parser.spec.ts index cbb0776..9451520 100644 --- a/tests/parser.spec.ts +++ b/tests/parser.spec.ts @@ -25,6 +25,7 @@ test.group('Parser | flags', () => { ), { _: [], + nodeArgs: [], args: [], unknownFlags: [], flags: { @@ -38,6 +39,7 @@ test.group('Parser | flags', () => { assert.deepEqual(new Parser(MakeModel.getParserOptions()).parse('--files=a --files=b'), { _: [], + nodeArgs: [], args: [], unknownFlags: [], flags: { @@ -55,6 +57,7 @@ test.group('Parser | flags', () => { assert.deepEqual(new Parser(MakeModel.getParserOptions()).parse('-c=sqlite -d -b=1 -f=a,b'), { _: [], + nodeArgs: [], args: [], unknownFlags: [], flags: { @@ -67,6 +70,7 @@ test.group('Parser | flags', () => { assert.deepEqual(new Parser(MakeModel.getParserOptions()).parse('-f=a -f=b'), { _: [], + nodeArgs: [], args: [], unknownFlags: [], flags: { @@ -85,6 +89,7 @@ test.group('Parser | flags', () => { const output = new Parser(MakeModel.getParserOptions()).parse('') assert.deepEqual(output, { _: [], + nodeArgs: [], args: [], unknownFlags: [], flags: {}, @@ -101,6 +106,7 @@ test.group('Parser | flags', () => { const output = new Parser(MakeModel.getParserOptions()).parse('') assert.deepEqual(output, { _: [], + nodeArgs: [], args: [], unknownFlags: [], flags: { @@ -124,6 +130,7 @@ test.group('Parser | flags', () => { assert.deepEqual(output, { _: [], + nodeArgs: [], args: [], unknownFlags: [], flags: { @@ -172,6 +179,7 @@ test.group('Parser | flags', () => { assert.deepEqual(output, { _: [], + nodeArgs: [], args: [], unknownFlags: [], flags: { @@ -217,6 +225,7 @@ test.group('Parser | flags', () => { const output = new Parser(MakeModel.getParserOptions()).parse('--batch-size') assert.deepEqual(output, { _: [], + nodeArgs: [], args: [], unknownFlags: [], flags: { @@ -246,6 +255,7 @@ test.group('Parser | flags', () => { const output = new Parser(MakeModel.getParserOptions()).parse('') assert.deepEqual(output, { _: [], + nodeArgs: [], args: [], unknownFlags: [], flags: {}, @@ -262,6 +272,7 @@ test.group('Parser | flags', () => { assert.deepEqual(output, { _: [], + nodeArgs: [], args: [], unknownFlags: ['foo', 'bar'], flags: { @@ -282,6 +293,7 @@ test.group('Parser | arguments', () => { const output = new Parser(MakeModel.getParserOptions()).parse('user sqlite mysql pg') assert.deepEqual(output, { _: [], + nodeArgs: [], args: ['user', ['sqlite', 'mysql', 'pg']], unknownFlags: [], flags: {}, @@ -296,6 +308,7 @@ test.group('Parser | arguments', () => { const output = new Parser(MakeModel.getParserOptions()).parse('user') assert.deepEqual(output, { _: [], + nodeArgs: [], args: ['user', ['sqlite']], unknownFlags: [], flags: {}, @@ -310,6 +323,7 @@ test.group('Parser | arguments', () => { const output = new Parser(MakeModel.getParserOptions()).parse(['user', '']) assert.deepEqual(output, { _: [], + nodeArgs: [], args: ['user', ['']], unknownFlags: [], flags: {}, @@ -334,6 +348,7 @@ test.group('Parser | arguments', () => { const output = new Parser(MakeModel.getParserOptions()).parse(['user', 'sqlite', 'pg']) assert.deepEqual(output, { _: [], + nodeArgs: [], args: ['USER', ['SQLITE', 'PG']], unknownFlags: [], flags: {}, @@ -360,6 +375,7 @@ test.group('Parser | arguments', () => { const output = new Parser(MakeModel.getParserOptions()).parse([]) assert.deepEqual(output, { _: [], + nodeArgs: [], args: ['POST', ['SQLITE']], unknownFlags: [], flags: {}, @@ -384,6 +400,7 @@ test.group('Parser | arguments', () => { const output = new Parser(MakeModel.getParserOptions()).parse([]) assert.deepEqual(output, { _: [], + nodeArgs: [], args: [undefined, undefined], unknownFlags: [], flags: {}, @@ -403,6 +420,7 @@ test.group('Parser | arguments', () => { const output = new Parser(MakeModel.getParserOptions()).parse([]) assert.deepEqual(output, { _: [], + nodeArgs: [], args: [undefined, [1]], unknownFlags: [], flags: {}, @@ -423,6 +441,7 @@ test.group('Parser | arguments', () => { const output = new Parser(MakeModel.getParserOptions()).parse([]) assert.deepEqual(output, { _: [], + nodeArgs: [], args: [null, [1]], unknownFlags: [], flags: {}, From 74ad44a4532fa8f1364f5eaa0174da784b2b4c10 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 27 Feb 2023 09:41:58 +0530 Subject: [PATCH 038/112] chore: update dependencies --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 4f12fab..606da50 100644 --- a/package.json +++ b/package.json @@ -69,14 +69,14 @@ "@japa/spec-reporter": "^1.3.3", "@poppinss/dev-utils": "^2.0.3", "@swc/core": "^1.3.36", - "@types/node": "^18.14.1", + "@types/node": "^18.14.2", "@types/sinon": "^10.0.13", "@types/string-similarity": "^4.0.0", "c8": "^7.13.0", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", "del-cli": "^5.0.0", - "eslint": "^8.34.0", + "eslint": "^8.35.0", "eslint-config-prettier": "^8.6.0", "eslint-plugin-adonis": "^3.0.3", "eslint-plugin-prettier": "^4.2.1", From 3f87e8fbae7637cb7c09baed21d5a5c13bc5e39c Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 27 Feb 2023 09:49:21 +0530 Subject: [PATCH 039/112] refactor: fix build issues --- tests/base_command/main.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/base_command/main.spec.ts b/tests/base_command/main.spec.ts index b52cdd8..87ce927 100644 --- a/tests/base_command/main.spec.ts +++ b/tests/base_command/main.spec.ts @@ -25,7 +25,7 @@ test.group('Base command', () => { const kernel = Kernel.create() const model = new MakeModel( kernel, - { _: [], args: [], unknownFlags: [], flags: {} }, + { _: [], args: [], unknownFlags: [], flags: {}, nodeArgs: [] }, cliui(), kernel.prompt ) @@ -43,7 +43,7 @@ test.group('Base command', () => { const kernel = Kernel.create() const model = new MakeModel( kernel, - { _: [], args: [], unknownFlags: [], flags: {} }, + { _: [], args: [], unknownFlags: [], flags: {}, nodeArgs: [] }, cliui(), kernel.prompt ) From a4feb70459fdf7f815d9155ad092c4703bcd26d7 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 27 Feb 2023 09:51:54 +0530 Subject: [PATCH 040/112] chore(release): 12.3.0-0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 606da50..45741c9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/ace", - "version": "12.2.0-0", + "version": "12.3.0-0", "description": "A CLI framework for Node.js", "main": "build/index.js", "type": "module", From a5352490c668e1f014126102bad2ed73e112b326 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 3 Mar 2023 10:29:55 +0530 Subject: [PATCH 041/112] chore: update dependencies --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 45741c9..5016115 100644 --- a/package.json +++ b/package.json @@ -68,8 +68,8 @@ "@japa/runner": "^2.5.1", "@japa/spec-reporter": "^1.3.3", "@poppinss/dev-utils": "^2.0.3", - "@swc/core": "^1.3.36", - "@types/node": "^18.14.2", + "@swc/core": "^1.3.37", + "@types/node": "^18.14.4", "@types/sinon": "^10.0.13", "@types/string-similarity": "^4.0.0", "c8": "^7.13.0", From 6c8bdc04c6a90f4696bde13eae7bc809e992ddf6 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 3 Mar 2023 10:31:18 +0530 Subject: [PATCH 042/112] refactor: remove ace-toolkit Moving it to AdonisJS core --- commands/index_command.ts | 27 --------------------------- commands/main.ts | 17 ----------------- package.json | 4 ---- 3 files changed, 48 deletions(-) delete mode 100644 commands/index_command.ts delete mode 100644 commands/main.ts diff --git a/commands/index_command.ts b/commands/index_command.ts deleted file mode 100644 index c37512a..0000000 --- a/commands/index_command.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * @adonisjs/ace - * - * (c) AdonisJS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { join } from 'node:path' -import { args, BaseCommand, IndexGenerator } from '../index.js' - -/** - * Generates index of commands with a loader. Must be called against - * the TypeScript compiled output. - */ -export default class IndexCommand extends BaseCommand { - static commandName = 'index' - static description: string = 'Create an index of commands along with a lazy loader' - - @args.string({ description: 'Relative path from cwd to the commands directory' }) - declare commandsDir: string - - async run(): Promise { - await new IndexGenerator(join(process.cwd(), this.commandsDir)).generate() - } -} diff --git a/commands/main.ts b/commands/main.ts deleted file mode 100644 index ae0333d..0000000 --- a/commands/main.ts +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env node - -/* - * @adonisjs/ace - * - * (c) AdonisJS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { Kernel, ListLoader } from '../index.js' -import IndexCommand from './index_command.js' - -const kernel = Kernel.create() -kernel.addLoader(new ListLoader([IndexCommand])) -await kernel.handle(process.argv.splice(2)) diff --git a/package.json b/package.json index 5016115..3fb3c27 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,6 @@ "main": "build/index.js", "type": "module", "files": [ - "build/commands", "build/schemas", "build/src", "build/stubs", @@ -16,9 +15,6 @@ ".": "./build/index.js", "./types": "./build/src/types.js" }, - "bin": { - "ace-toolkit": "./build/commands/main.js" - }, "scripts": { "pretest": "npm run lint", "test": "cross-env NODE_DEBUG=adonisjs:ace c8 npm run vscode:test", From a803bd7d8f97ad4773148ff73afac0c813213230 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 3 Mar 2023 10:44:35 +0530 Subject: [PATCH 043/112] chore(release): 12.3.1-0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3fb3c27..00b61a1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/ace", - "version": "12.3.0-0", + "version": "12.3.1-0", "description": "A CLI framework for Node.js", "main": "build/index.js", "type": "module", From a08f3b4287a331a8783236b12645cd3bb1056c7f Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Sat, 4 Mar 2023 09:05:06 +0530 Subject: [PATCH 044/112] chore: update dependencies --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 00b61a1..e40d0c3 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "@japa/spec-reporter": "^1.3.3", "@poppinss/dev-utils": "^2.0.3", "@swc/core": "^1.3.37", - "@types/node": "^18.14.4", + "@types/node": "^18.14.6", "@types/sinon": "^10.0.13", "@types/string-similarity": "^4.0.0", "c8": "^7.13.0", From 93c7862f48cd8998b5a3eaa5ce0367f68fd77579 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 10 Mar 2023 09:20:34 +0530 Subject: [PATCH 045/112] chore: update dependencies --- package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index e40d0c3..e779609 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "@poppinss/cliui": "^6.1.1-0", "@poppinss/hooks": "^7.1.1-0", "@poppinss/macroable": "^1.0.0-2", - "@poppinss/prompts": "^3.1.0-0", + "@poppinss/prompts": "^3.1.0-1", "@poppinss/utils": "^6.5.0-0", "jsonschema": "^1.4.1", "string-similarity": "^4.0.4", @@ -64,8 +64,8 @@ "@japa/runner": "^2.5.1", "@japa/spec-reporter": "^1.3.3", "@poppinss/dev-utils": "^2.0.3", - "@swc/core": "^1.3.37", - "@types/node": "^18.14.6", + "@swc/core": "^1.3.38", + "@types/node": "^18.15.0", "@types/sinon": "^10.0.13", "@types/string-similarity": "^4.0.0", "c8": "^7.13.0", @@ -73,10 +73,10 @@ "cross-env": "^7.0.3", "del-cli": "^5.0.0", "eslint": "^8.35.0", - "eslint-config-prettier": "^8.6.0", + "eslint-config-prettier": "^8.7.0", "eslint-plugin-adonis": "^3.0.3", "eslint-plugin-prettier": "^4.2.1", - "github-label-sync": "^2.2.0", + "github-label-sync": "^2.3.1", "husky": "^8.0.3", "np": "^7.6.3", "prettier": "^2.8.4", From 15f93183fdd7cbaf944fe2257d704e5d7ad4ad12 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 10 Mar 2023 09:44:43 +0530 Subject: [PATCH 046/112] chore(release): 12.3.1-1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e779609..65387bf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/ace", - "version": "12.3.1-0", + "version": "12.3.1-1", "description": "A CLI framework for Node.js", "main": "build/index.js", "type": "module", From 4b132f263896411a671237f739baf79ef83677a1 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 10 Mar 2023 10:44:32 +0530 Subject: [PATCH 047/112] feat: expose command static properties from command instance --- src/commands/base.ts | 28 +++++++++ tests/base_command/main.spec.ts | 106 ++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+) diff --git a/src/commands/base.ts b/src/commands/base.ts index be32477..28ae494 100644 --- a/src/commands/base.ts +++ b/src/commands/base.ts @@ -419,6 +419,34 @@ export class BaseCommand extends Macroable { return this.kernel.getMainCommand() === this } + /** + * Reference to the command name + */ + get commandName() { + return (this.constructor as typeof BaseCommand).commandName + } + + /** + * Reference to the command options + */ + get options() { + return (this.constructor as typeof BaseCommand).options + } + + /** + * Reference to the command args + */ + get args() { + return (this.constructor as typeof BaseCommand).args + } + + /** + * Reference to the command flags + */ + get flags() { + return (this.constructor as typeof BaseCommand).flags + } + constructor( protected kernel: Kernel, protected parsed: ParsedOutput, diff --git a/tests/base_command/main.spec.ts b/tests/base_command/main.spec.ts index 87ce927..0ac41a6 100644 --- a/tests/base_command/main.spec.ts +++ b/tests/base_command/main.spec.ts @@ -12,8 +12,54 @@ import { cliui } from '@poppinss/cliui' import { Kernel } from '../../src/kernel.js' import { BaseCommand } from '../../src/commands/base.js' +import type { Argument, CommandOptions, Flag } from '../../src/types.js' test.group('Base command', () => { + test('access command name from command instance', ({ assert }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'make:model' + declare name: string + declare connection: string + } + + MakeModel.boot() + + const kernel = Kernel.create() + const model = new MakeModel( + kernel, + { _: [], args: [], unknownFlags: [], flags: {}, nodeArgs: [] }, + cliui(), + kernel.prompt + ) + + assert.equal(model.commandName, 'make:model') + }) + + test('access command options from command instance', ({ assert, expectTypeOf }) => { + class MakeModel extends BaseCommand { + static options: CommandOptions = { + allowUnknownFlags: true, + } + declare name: string + declare connection: string + } + + MakeModel.boot() + + const kernel = Kernel.create() + const model = new MakeModel( + kernel, + { _: [], args: [], unknownFlags: [], flags: {}, nodeArgs: [] }, + cliui(), + kernel.prompt + ) + + expectTypeOf(model.options).toEqualTypeOf() + assert.deepEqual(model.options, { + allowUnknownFlags: true, + }) + }) + test('access the ui logger from the logger property', ({ assert }) => { class MakeModel extends BaseCommand { declare name: string @@ -83,6 +129,36 @@ test.group('Base command | consume args', () => { assert.deepEqual(model.names, ['user', 'post']) assert.equal(model.connection, 'sqlite') }) + + test('access command args from command instance', ({ assert, expectTypeOf }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'make:model' + declare name: string + declare connection: string + } + + MakeModel.boot() + MakeModel.defineArgument('name', { type: 'string' }) + MakeModel.defineFlag('connection', { type: 'string' }) + + const kernel = Kernel.create() + const model = new MakeModel( + kernel, + { _: [], args: [], unknownFlags: [], flags: {}, nodeArgs: [] }, + cliui(), + kernel.prompt + ) + + expectTypeOf(model.args).toEqualTypeOf() + assert.deepEqual(model.args, [ + { + name: 'name', + argumentName: 'name', + required: true, + type: 'string', + }, + ]) + }) }) test.group('Base command | consume flags', () => { @@ -146,4 +222,34 @@ test.group('Base command | consume flags', () => { assert.deepEqual(model.connections, ['sqlite']) assert.isUndefined(model.dropAll) }) + + test('access command flags from command instance', ({ assert, expectTypeOf }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'make:model' + declare name: string + declare connection: string + } + + MakeModel.boot() + MakeModel.defineArgument('name', { type: 'string' }) + MakeModel.defineFlag('connection', { type: 'string' }) + + const kernel = Kernel.create() + const model = new MakeModel( + kernel, + { _: [], args: [], unknownFlags: [], flags: {}, nodeArgs: [] }, + cliui(), + kernel.prompt + ) + + expectTypeOf(model.flags).toEqualTypeOf() + assert.deepEqual(model.flags, [ + { + name: 'connection', + flagName: 'connection', + required: false, + type: 'string', + }, + ]) + }) }) From ab4758e6b072cf68088afcabd1396f2b47e631a3 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 10 Mar 2023 15:54:09 +0530 Subject: [PATCH 048/112] feat: add assertionMethods --- src/commands/base.ts | 126 +++++++++++++++++++ src/commands/help.ts | 10 +- tests/base_command/assertions.spec.ts | 168 ++++++++++++++++++++++++++ 3 files changed, 299 insertions(+), 5 deletions(-) create mode 100644 tests/base_command/assertions.spec.ts diff --git a/src/commands/base.ts b/src/commands/base.ts index 28ae494..052b9b6 100644 --- a/src/commands/base.ts +++ b/src/commands/base.ts @@ -7,9 +7,11 @@ * file that was distributed with this source code. */ +import { inspect } from 'node:util' import string from '@poppinss/utils/string' import Macroable from '@poppinss/macroable' import lodash from '@poppinss/utils/lodash' +import { AssertionError } from 'node:assert' import type { Prompt } from '@poppinss/prompts' import { defineStaticProperty, InvalidArgumentsException } from '@poppinss/utils' @@ -531,4 +533,128 @@ export class BaseCommand extends Macroable { exitCode: this.exitCode, } } + + /** + * Assert the command exists with a given exit code + */ + assertExitCode(code: number) { + if (this.exitCode !== code) { + const error = new AssertionError({ + message: `Expected '${this.commandName}' command to finish with exit code '${code}'`, + actual: this.exitCode, + expected: code, + operator: 'strictEqual', + stackStartFn: this.assertExitCode, + }) + Object.defineProperty(error, 'showDiff', { value: true }) + + throw error + } + } + + /** + * Assert the command exists with a given exit code + */ + assertNotExitCode(code: number) { + if (this.exitCode === code) { + throw new AssertionError({ + message: `Expected '${this.commandName}' command to finish without exit code '${this.exitCode}'`, + stackStartFn: this.assertNotExitCode, + }) + } + } + + /** + * Assert the command exists with zero exit code + */ + assertSucceeded() { + return this.assertExitCode(0) + } + + /** + * Assert the command exists with non-zero exit code + */ + assertFailed() { + return this.assertNotExitCode(0) + } + + /** + * Assert command to log the expected message + */ + assertLogMessage(message: string, stream?: 'stdout' | 'stderr') { + const logs = this.logger.getLogs() + const logMessages = logs.map((log) => log.message) + const matchingLog = logs.find((log) => log.message === message) + + /** + * No log found + */ + if (!matchingLog) { + const error = new AssertionError({ + message: `Expected log messages to include ${inspect(message)}`, + actual: logMessages, + expected: [message], + operator: 'strictEqual', + stackStartFn: this.assertLogMessage, + }) + Object.defineProperty(error, 'showDiff', { value: true }) + + throw error + } + + /** + * Log is on a different stream + */ + if (stream && matchingLog.stream !== stream) { + const error = new AssertionError({ + message: `Expected log message stream to be ${inspect(stream)}, instead received ${inspect( + matchingLog.stream + )}`, + actual: matchingLog.stream, + expected: stream, + operator: 'strictEqual', + stackStartFn: this.assertLogMessage, + }) + Object.defineProperty(error, 'showDiff', { value: true }) + + throw error + } + } + + /** + * Assert command to log the expected message + */ + assertLogMatches(matchingRegex: RegExp, stream?: 'stdout' | 'stderr') { + const logs = this.logger.getLogs() + const matchingLog = logs.find((log) => matchingRegex.test(log.message)) + + /** + * No log found + */ + if (!matchingLog) { + const error = new AssertionError({ + message: `Expected log messages to match ${inspect(matchingRegex)}`, + stackStartFn: this.assertLogMessage, + }) + throw error + } + + /** + * Log is on a different stream + */ + if (stream && matchingLog.stream !== stream) { + const error = new AssertionError({ + message: `Expected log message stream to be ${inspect(stream)}, instead received ${inspect( + matchingLog.stream + )}`, + actual: matchingLog.stream, + expected: stream, + operator: 'strictEqual', + stackStartFn: this.assertLogMessage, + }) + Object.defineProperty(error, 'showDiff', { value: true }) + + throw error + } + } } diff --git a/src/commands/help.ts b/src/commands/help.ts index e028be3..59994ff 100644 --- a/src/commands/help.ts +++ b/src/commands/help.ts @@ -32,7 +32,7 @@ export class HelpCommand extends BaseCommand { * The command name argument */ @args.string({ description: 'Command name', argumentName: 'command' }) - declare commandName: string + declare name: string /** * Returns the command arguments table @@ -82,12 +82,12 @@ export class HelpCommand extends BaseCommand { * Validates the command name to ensure it exists */ #validateCommandName(): boolean { - const command = this.kernel.getCommand(this.commandName) + const command = this.kernel.getCommand(this.name) if (!command) { renderErrorWithSuggestions( this.ui, - `Command "${this.commandName}" is not defined`, - this.kernel.getCommandSuggestions(this.commandName) + `Command "${this.name}" is not defined`, + this.kernel.getCommandSuggestions(this.name) ) return false } @@ -170,7 +170,7 @@ export class HelpCommand extends BaseCommand { return } - const command = this.kernel.getCommand(this.commandName)! + const command = this.kernel.getCommand(this.name)! this.renderDescription(command) this.renderUsage(command) this.renderList(command) diff --git a/tests/base_command/assertions.spec.ts b/tests/base_command/assertions.spec.ts new file mode 100644 index 0000000..76b6e5f --- /dev/null +++ b/tests/base_command/assertions.spec.ts @@ -0,0 +1,168 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { Kernel } from '../../src/kernel.js' +import { BaseCommand } from '../../src/commands/base.js' + +test.group('Base command | assertions', () => { + test('assert command has exitCode', async ({ assert }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'make:model' + + async run() { + return super.run() + } + } + + const kernel = Kernel.create() + const model = await kernel.create(MakeModel, []) + + await model.exec() + assert.doesNotThrows(() => model.assertExitCode(0)) + assert.throws( + () => model.assertExitCode(1), + `Expected 'make:model' command to finish with exit code '1'` + ) + }) + + test('assert command does not have exitCode', async ({ assert }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'make:model' + + async run() { + return super.run() + } + } + + const kernel = Kernel.create() + const model = await kernel.create(MakeModel, []) + + await model.exec() + assert.doesNotThrows(() => model.assertNotExitCode(1)) + assert.throws( + () => model.assertNotExitCode(0), + `Expected 'make:model' command to finish without exit code '0'` + ) + }) + + test('assert command finishes successfully', async ({ assert }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'make:model' + async run() { + return super.run() + } + } + + class MakeController extends BaseCommand { + static commandName: string = 'make:controller' + async run() { + this.exitCode = 1 + } + } + + const kernel = Kernel.create() + const model = await kernel.create(MakeModel, []) + await model.exec() + assert.doesNotThrows(() => model.assertSucceeded()) + + const controller = await kernel.create(MakeController, []) + await controller.exec() + assert.throws( + () => controller.assertSucceeded(), + `Expected 'make:controller' command to finish with exit code '0'` + ) + }) + + test('assert command fails', async ({ assert }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'make:model' + async run() { + return super.run() + } + } + + class MakeController extends BaseCommand { + static commandName: string = 'make:controller' + async run() { + this.exitCode = 1 + } + } + + const kernel = Kernel.create() + const model = await kernel.create(MakeModel, []) + await model.exec() + assert.throws( + () => model.assertFailed(), + `Expected 'make:model' command to finish without exit code '0` + ) + + const controller = await kernel.create(MakeController, []) + await controller.exec() + assert.doesNotThrows(() => controller.assertFailed()) + }) + + test('assert command logs a specific message', async ({ assert }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'make:model' + async run() { + this.logger.info('Running make:model command') + return super.run() + } + } + + const kernel = Kernel.create() + kernel.ui.switchMode('raw') + + const model = await kernel.create(MakeModel, []) + await model.exec() + + assert.doesNotThrows(() => model.assertLogMessage('[ blue(info) ] Running make:model command')) + assert.throws( + () => model.assertLogMessage('[ cyan(info) ] Running make:model command'), + `Expected log messages to include '[ cyan(info) ] Running make:model command'` + ) + + assert.doesNotThrows(() => + model.assertLogMessage('[ blue(info) ] Running make:model command', 'stdout') + ) + assert.throws( + () => model.assertLogMessage('[ blue(info) ] Running make:model command', 'stderr'), + `Expected log message stream to be 'stderr', instead received 'stdout'` + ) + }) + + test('assert command logs matches a given regex', async ({ assert }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'make:model' + async run() { + this.logger.info('Running make:model command') + return super.run() + } + } + + const kernel = Kernel.create() + kernel.ui.switchMode('raw') + + const model = await kernel.create(MakeModel, []) + await model.exec() + + assert.doesNotThrows(() => model.assertLogMatches(/Running make:model command/)) + assert.throws( + () => model.assertLogMatches(/Running command/), + `Expected log messages to match /Running command/` + ) + + assert.doesNotThrows(() => model.assertLogMatches(/Running make:model command/, 'stdout')) + assert.throws( + () => model.assertLogMatches(/Running make:model command/, 'stderr'), + `Expected log message stream to be 'stderr', instead received 'stdout'` + ) + }) +}) From a6e17e4fdc9941d5d88720e591b832cdbd779f7e Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Sat, 11 Mar 2023 13:11:37 +0530 Subject: [PATCH 049/112] chore: update dependencies --- package.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 65387bf..2549044 100644 --- a/package.json +++ b/package.json @@ -42,11 +42,11 @@ "author": "virk,adonisjs", "license": "MIT", "dependencies": { - "@poppinss/cliui": "^6.1.1-0", - "@poppinss/hooks": "^7.1.1-0", - "@poppinss/macroable": "^1.0.0-2", - "@poppinss/prompts": "^3.1.0-1", - "@poppinss/utils": "^6.5.0-0", + "@poppinss/cliui": "^6.1.1-1", + "@poppinss/hooks": "^7.1.1-1", + "@poppinss/macroable": "^1.0.0-3", + "@poppinss/prompts": "^3.1.0-2", + "@poppinss/utils": "^6.5.0-1", "jsonschema": "^1.4.1", "string-similarity": "^4.0.4", "string-width": "^5.1.2", @@ -64,7 +64,7 @@ "@japa/runner": "^2.5.1", "@japa/spec-reporter": "^1.3.3", "@poppinss/dev-utils": "^2.0.3", - "@swc/core": "^1.3.38", + "@swc/core": "^1.3.39", "@types/node": "^18.15.0", "@types/sinon": "^10.0.13", "@types/string-similarity": "^4.0.0", @@ -72,7 +72,7 @@ "copyfiles": "^2.4.1", "cross-env": "^7.0.3", "del-cli": "^5.0.0", - "eslint": "^8.35.0", + "eslint": "^8.36.0", "eslint-config-prettier": "^8.7.0", "eslint-plugin-adonis": "^3.0.3", "eslint-plugin-prettier": "^4.2.1", @@ -84,7 +84,7 @@ "sinon": "^15.0.1", "ts-json-schema-generator": "^1.2.0", "ts-node": "^10.9.1", - "typescript": "^4.8.2" + "typescript": "^4.9.5" }, "repository": { "type": "git", From 46eb31929432e8b07fe26a2ca7374f2af7a9b1c2 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Sat, 11 Mar 2023 13:15:21 +0530 Subject: [PATCH 050/112] refactor: rename assertLogMessage to assertLog --- src/commands/base.ts | 10 +++++----- tests/base_command/assertions.spec.ts | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/commands/base.ts b/src/commands/base.ts index 052b9b6..805c317 100644 --- a/src/commands/base.ts +++ b/src/commands/base.ts @@ -581,7 +581,7 @@ export class BaseCommand extends Macroable { /** * Assert command to log the expected message */ - assertLogMessage(message: string, stream?: 'stdout' | 'stderr') { + assertLog(message: string, stream?: 'stdout' | 'stderr') { const logs = this.logger.getLogs() const logMessages = logs.map((log) => log.message) const matchingLog = logs.find((log) => log.message === message) @@ -595,7 +595,7 @@ export class BaseCommand extends Macroable { actual: logMessages, expected: [message], operator: 'strictEqual', - stackStartFn: this.assertLogMessage, + stackStartFn: this.assertLog, }) Object.defineProperty(error, 'showDiff', { value: true }) @@ -613,7 +613,7 @@ export class BaseCommand extends Macroable { actual: matchingLog.stream, expected: stream, operator: 'strictEqual', - stackStartFn: this.assertLogMessage, + stackStartFn: this.assertLog, }) Object.defineProperty(error, 'showDiff', { value: true }) @@ -634,7 +634,7 @@ export class BaseCommand extends Macroable { if (!matchingLog) { const error = new AssertionError({ message: `Expected log messages to match ${inspect(matchingRegex)}`, - stackStartFn: this.assertLogMessage, + stackStartFn: this.assertLogMatches, }) throw error } @@ -650,7 +650,7 @@ export class BaseCommand extends Macroable { actual: matchingLog.stream, expected: stream, operator: 'strictEqual', - stackStartFn: this.assertLogMessage, + stackStartFn: this.assertLogMatches, }) Object.defineProperty(error, 'showDiff', { value: true }) diff --git a/tests/base_command/assertions.spec.ts b/tests/base_command/assertions.spec.ts index 76b6e5f..01aa3bf 100644 --- a/tests/base_command/assertions.spec.ts +++ b/tests/base_command/assertions.spec.ts @@ -123,17 +123,17 @@ test.group('Base command | assertions', () => { const model = await kernel.create(MakeModel, []) await model.exec() - assert.doesNotThrows(() => model.assertLogMessage('[ blue(info) ] Running make:model command')) + assert.doesNotThrows(() => model.assertLog('[ blue(info) ] Running make:model command')) assert.throws( - () => model.assertLogMessage('[ cyan(info) ] Running make:model command'), + () => model.assertLog('[ cyan(info) ] Running make:model command'), `Expected log messages to include '[ cyan(info) ] Running make:model command'` ) assert.doesNotThrows(() => - model.assertLogMessage('[ blue(info) ] Running make:model command', 'stdout') + model.assertLog('[ blue(info) ] Running make:model command', 'stdout') ) assert.throws( - () => model.assertLogMessage('[ blue(info) ] Running make:model command', 'stderr'), + () => model.assertLog('[ blue(info) ] Running make:model command', 'stderr'), `Expected log message stream to be 'stderr', instead received 'stdout'` ) }) From 0396494f90e0a4c380618e8268297fc7aa28ded0 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Sat, 11 Mar 2023 13:20:14 +0530 Subject: [PATCH 051/112] chore(release): 12.3.1-2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2549044..abe6e7a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/ace", - "version": "12.3.1-1", + "version": "12.3.1-2", "description": "A CLI framework for Node.js", "main": "build/index.js", "type": "module", From d91d1f806f20a9ccfadebf045fdad38a86a49857 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 14 Mar 2023 13:52:15 +0530 Subject: [PATCH 052/112] feat: export cliHelpers from poppinss/cliui --- index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/index.ts b/index.ts index 9b780ca..30eb385 100644 --- a/index.ts +++ b/index.ts @@ -12,6 +12,7 @@ export { Kernel } from './src/kernel.js' export * as errors from './src/errors.js' export { args } from './src/decorators/args.js' export { flags } from './src/decorators/flags.js' +export * as cliHelpers from '@poppinss/cliui/helpers' export { BaseCommand } from './src/commands/base.js' export { HelpCommand } from './src/commands/help.js' export { ListCommand } from './src/commands/list.js' From fad9ce2f25b2b4a0e49a03350ef3bdde6b16655c Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 14 Mar 2023 13:53:26 +0530 Subject: [PATCH 053/112] chore: update dependencies --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index abe6e7a..5b44cd2 100644 --- a/package.json +++ b/package.json @@ -64,8 +64,8 @@ "@japa/runner": "^2.5.1", "@japa/spec-reporter": "^1.3.3", "@poppinss/dev-utils": "^2.0.3", - "@swc/core": "^1.3.39", - "@types/node": "^18.15.0", + "@swc/core": "^1.3.40", + "@types/node": "^18.15.3", "@types/sinon": "^10.0.13", "@types/string-similarity": "^4.0.0", "c8": "^7.13.0", @@ -81,7 +81,7 @@ "np": "^7.6.3", "prettier": "^2.8.4", "reflect-metadata": "^0.1.13", - "sinon": "^15.0.1", + "sinon": "^15.0.2", "ts-json-schema-generator": "^1.2.0", "ts-node": "^10.9.1", "typescript": "^4.9.5" From ac6e1213d3156f8f3f0e564e2817e67baaefb7c2 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 14 Mar 2023 15:48:26 +0530 Subject: [PATCH 054/112] chore(release): 12.3.1-3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5b44cd2..248f55e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/ace", - "version": "12.3.1-2", + "version": "12.3.1-3", "description": "A CLI framework for Node.js", "main": "build/index.js", "type": "module", From 83ac84927c89bf2aa5255366c1797f1db79fb13d Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 15 Mar 2023 11:35:01 +0530 Subject: [PATCH 055/112] feat: add --json output for the list command --- src/commands/list.ts | 26 ++++++ tests/commands/list.spec.ts | 170 +++++++++++++++++++++++++++++++++++- 2 files changed, 195 insertions(+), 1 deletion(-) diff --git a/src/commands/list.ts b/src/commands/list.ts index 67f7c36..3f177b7 100644 --- a/src/commands/list.ts +++ b/src/commands/list.ts @@ -9,6 +9,7 @@ import { BaseCommand } from './base.js' import { args } from '../decorators/args.js' +import { flags } from '../decorators/flags.js' import { FlagFormatter } from '../formatters/flag.js' import { ListFormatter } from '../formatters/list.js' import { renderErrorWithSuggestions } from '../helpers.js' @@ -41,6 +42,9 @@ export class ListCommand extends BaseCommand { }) declare namespaces?: string[] + @flags.boolean({ description: 'Get list of commands as JSON' }) + declare json?: boolean + /** * Returns a table for an array of commands. */ @@ -148,6 +152,23 @@ export class ListCommand extends BaseCommand { }) } + protected renderToJSON() { + if (this.namespaces && this.namespaces.length) { + return this.namespaces + .map((namespace) => { + return this.kernel.getNamespaceCommands(namespace) + }) + .flat(1) + } + + return this.kernel.getNamespaceCommands().concat( + this.kernel + .getNamespaces() + .map((namespace) => this.kernel.getNamespaceCommands(namespace)) + .flat(1) + ) + } + /** * Executed by ace directly */ @@ -158,6 +179,11 @@ export class ListCommand extends BaseCommand { return } + if (this.json) { + this.logger.log(JSON.stringify(this.renderToJSON(), null, 2)) + return + } + this.renderList() } } diff --git a/tests/commands/list.spec.ts b/tests/commands/list.spec.ts index 3552eb0..1c1d2cd 100644 --- a/tests/commands/list.spec.ts +++ b/tests/commands/list.spec.ts @@ -73,7 +73,7 @@ test.group('List command', () => { ]) }) - test('show list of all the registered commands for a namespace', async ({ assert }) => { + test('show JSON list of all the registered commands for a namespace', async ({ assert }) => { const kernel = Kernel.create() kernel.ui.switchMode('raw') @@ -218,4 +218,172 @@ test.group('List command', () => { }, ]) }) + + test('show list of all the registered commands as JSON', async ({ assert }) => { + const kernel = Kernel.create() + kernel.ui.switchMode('raw') + + class Serve extends BaseCommand { + static commandName: string = 'serve' + static description: string = 'Start the AdonisJS HTTP server' + } + + class MakeController extends BaseCommand { + @args.string({ description: 'Name of the controller' }) + name!: string + + @flags.boolean({ description: 'Add resourceful methods', default: false }) + resource!: boolean + + static aliases: string[] = ['mc'] + static commandName: string = 'make:controller' + static description: string = 'Make a new HTTP controller' + } + + kernel.addLoader(new ListLoader([Serve, MakeController])) + const command = await kernel.create(ListCommand, ['--json']) + await command.exec() + + assert.equal(command.exitCode, 0) + assert.deepEqual(JSON.parse(kernel.ui.logger.getLogs()[0].message), [ + { + commandName: 'list', + description: 'View list of available commands', + help: [ + 'The list command displays a list of all the commands:', + ' {{ binaryName }}list', + '', + 'You can also display the commands for a specific namespace:', + ' {{ binaryName }}list ', + ], + namespace: null, + aliases: [], + flags: [ + { + name: 'json', + flagName: 'json', + required: false, + type: 'boolean', + description: 'Get list of commands as JSON', + }, + ], + args: [ + { + name: 'namespaces', + argumentName: 'namespaces', + required: false, + description: 'Filter list by namespace', + type: 'spread', + }, + ], + options: { + staysAlive: false, + allowUnknownFlags: false, + }, + }, + { + commandName: 'serve', + description: 'Start the AdonisJS HTTP server', + help: '', + namespace: null, + aliases: [], + flags: [], + args: [], + options: { + staysAlive: false, + allowUnknownFlags: false, + }, + }, + { + commandName: 'make:controller', + description: 'Make a new HTTP controller', + help: '', + namespace: 'make', + aliases: ['mc'], + flags: [ + { + name: 'resource', + flagName: 'resource', + required: false, + type: 'boolean', + description: 'Add resourceful methods', + default: false, + }, + ], + args: [ + { + name: 'name', + argumentName: 'name', + required: true, + description: 'Name of the controller', + type: 'string', + }, + ], + options: { + staysAlive: false, + allowUnknownFlags: false, + }, + }, + ]) + }) + + test('show JSON list of all the registered commands for a namespace', async ({ assert }) => { + const kernel = Kernel.create() + kernel.ui.switchMode('raw') + + class Serve extends BaseCommand { + static commandName: string = 'serve' + static description: string = 'Start the AdonisJS HTTP server' + } + + class MakeController extends BaseCommand { + @args.string({ description: 'Name of the controller' }) + name!: string + + @flags.boolean({ description: 'Add resourceful methods', default: false }) + resource!: boolean + + static aliases: string[] = ['mc'] + static commandName: string = 'make:controller' + static description: string = 'Make a new HTTP controller' + } + + kernel.addLoader(new ListLoader([Serve, MakeController])) + const command = await kernel.create(ListCommand, ['make', '--json']) + await command.exec() + + assert.equal(command.exitCode, 0) + assert.deepEqual(JSON.parse(kernel.ui.logger.getLogs()[0].message), [ + { + commandName: 'make:controller', + description: 'Make a new HTTP controller', + help: '', + namespace: 'make', + aliases: ['mc'], + flags: [ + { + name: 'resource', + flagName: 'resource', + required: false, + type: 'boolean', + description: 'Add resourceful methods', + default: false, + }, + ], + args: [ + { + name: 'name', + argumentName: 'name', + required: true, + description: 'Name of the controller', + type: 'string', + }, + ], + options: { + staysAlive: false, + allowUnknownFlags: false, + }, + }, + ]) + }) }) From 2a77c27d9ed29c8199dea1ffbb1ad494b0a2ee10 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 15 Mar 2023 11:39:59 +0530 Subject: [PATCH 056/112] chore(release): 12.3.1-4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 248f55e..55c0887 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/ace", - "version": "12.3.1-3", + "version": "12.3.1-4", "description": "A CLI framework for Node.js", "main": "build/index.js", "type": "module", From c10d9cf17fef7b18eda9023bc244af84c1322cec Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 27 Mar 2023 13:58:04 +0530 Subject: [PATCH 057/112] chore: update dependencies --- package.json | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 55c0887..b03ba2c 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "youch-terminal": "^2.2.0" }, "devDependencies": { - "@commitlint/cli": "^17.4.4", + "@commitlint/cli": "^17.5.0", "@commitlint/config-conventional": "^17.4.4", "@japa/assert": "^1.4.1", "@japa/expect-type": "^1.0.3", @@ -63,28 +63,25 @@ "@japa/run-failed-tests": "^1.1.1", "@japa/runner": "^2.5.1", "@japa/spec-reporter": "^1.3.3", - "@poppinss/dev-utils": "^2.0.3", - "@swc/core": "^1.3.40", - "@types/node": "^18.15.3", - "@types/sinon": "^10.0.13", + "@swc/core": "^1.3.42", + "@types/node": "^18.15.10", "@types/string-similarity": "^4.0.0", "c8": "^7.13.0", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", "del-cli": "^5.0.0", "eslint": "^8.36.0", - "eslint-config-prettier": "^8.7.0", + "eslint-config-prettier": "^8.8.0", "eslint-plugin-adonis": "^3.0.3", "eslint-plugin-prettier": "^4.2.1", "github-label-sync": "^2.3.1", "husky": "^8.0.3", - "np": "^7.6.3", - "prettier": "^2.8.4", + "np": "^7.6.4", + "prettier": "^2.8.7", "reflect-metadata": "^0.1.13", - "sinon": "^15.0.2", "ts-json-schema-generator": "^1.2.0", "ts-node": "^10.9.1", - "typescript": "^4.9.5" + "typescript": "^5.0.2" }, "repository": { "type": "git", From 8dc2ede78baf3bd75a2ebfd89c7491b235fd8f3b Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 27 Mar 2023 13:59:09 +0530 Subject: [PATCH 058/112] docs: update License file --- LICENSE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE.md b/LICENSE.md index 59a3cfa..381426b 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ # The MIT License -Copyright (c) 2023 AdonisJS Framework +Copyright (c) 2023 Harminder Virk 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: From f40898b1d3bd3156019842be36b16727683f8828 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 27 Mar 2023 14:36:38 +0530 Subject: [PATCH 059/112] chore: publish source and generate delcaration map --- package.json | 4 ++++ tsconfig.json | 1 + 2 files changed, 5 insertions(+) diff --git a/package.json b/package.json index b03ba2c..f0021d5 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,14 @@ "main": "build/index.js", "type": "module", "files": [ + "src", + "stubs", + "index.ts", "build/schemas", "build/src", "build/stubs", "build/index.d.ts", + "build/index.d.ts.map", "build/index.js" ], "exports": { diff --git a/tsconfig.json b/tsconfig.json index f2b7dda..f3bdb22 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,6 +23,7 @@ "strictBindCallApply": true, "strictFunctionTypes": true, "noImplicitThis": true, + "declarationMap": true, "skipLibCheck": true, "types": ["@types/node"] }, From a4f1a9d36661a45fcd215efa5a17218456e7fb64 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 27 Mar 2023 14:41:24 +0530 Subject: [PATCH 060/112] chore(release): 12.3.1-5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f0021d5..343aae0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/ace", - "version": "12.3.1-4", + "version": "12.3.1-5", "description": "A CLI framework for Node.js", "main": "build/index.js", "type": "module", From ce2b4578acf33e0539e5f5be0e4f8257365d93a4 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 17 Apr 2023 15:33:18 +0530 Subject: [PATCH 061/112] chore: update dependencies --- package.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 343aae0..689c9ed 100644 --- a/package.json +++ b/package.json @@ -46,11 +46,11 @@ "author": "virk,adonisjs", "license": "MIT", "dependencies": { - "@poppinss/cliui": "^6.1.1-1", - "@poppinss/hooks": "^7.1.1-1", - "@poppinss/macroable": "^1.0.0-3", + "@poppinss/cliui": "^6.1.1-2", + "@poppinss/hooks": "^7.1.1-2", + "@poppinss/macroable": "^1.0.0-6", "@poppinss/prompts": "^3.1.0-2", - "@poppinss/utils": "^6.5.0-1", + "@poppinss/utils": "^6.5.0-2", "jsonschema": "^1.4.1", "string-similarity": "^4.0.4", "string-width": "^5.1.2", @@ -59,33 +59,33 @@ "youch-terminal": "^2.2.0" }, "devDependencies": { - "@commitlint/cli": "^17.5.0", - "@commitlint/config-conventional": "^17.4.4", + "@commitlint/cli": "^17.6.1", + "@commitlint/config-conventional": "^17.6.1", "@japa/assert": "^1.4.1", "@japa/expect-type": "^1.0.3", "@japa/file-system": "^1.0.1", "@japa/run-failed-tests": "^1.1.1", "@japa/runner": "^2.5.1", "@japa/spec-reporter": "^1.3.3", - "@swc/core": "^1.3.42", - "@types/node": "^18.15.10", + "@swc/core": "^1.3.51", + "@types/node": "^18.15.11", "@types/string-similarity": "^4.0.0", "c8": "^7.13.0", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", "del-cli": "^5.0.0", - "eslint": "^8.36.0", + "eslint": "^8.38.0", "eslint-config-prettier": "^8.8.0", "eslint-plugin-adonis": "^3.0.3", "eslint-plugin-prettier": "^4.2.1", "github-label-sync": "^2.3.1", "husky": "^8.0.3", - "np": "^7.6.4", + "np": "^7.7.0", "prettier": "^2.8.7", "reflect-metadata": "^0.1.13", "ts-json-schema-generator": "^1.2.0", "ts-node": "^10.9.1", - "typescript": "^5.0.2" + "typescript": "^5.0.4" }, "repository": { "type": "git", From 0fc3f0ee8a12e3dfa47fcbcb0610b1e0dd179057 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 17 Apr 2023 15:37:16 +0530 Subject: [PATCH 062/112] fix: formatting when using binaryName during interpolation --- src/formatters/command.ts | 2 +- tests/commands/help.spec.ts | 2 +- tests/formatters/command.spec.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/formatters/command.ts b/src/formatters/command.ts index a2caeac..4aae514 100644 --- a/src/formatters/command.ts +++ b/src/formatters/command.ts @@ -46,7 +46,7 @@ export class CommandFormatter { * Returns multiline command help */ formatHelp(binaryName?: AllowedInfoValues, terminalWidth: number = TERMINAL_SIZE): string { - const binary = binaryName ? `${binaryName} ` : '' + const binary = binaryName ? `${binaryName}` : '' if (!this.#command.help) { return '' } diff --git a/tests/commands/help.spec.ts b/tests/commands/help.spec.ts index 76fd6d9..d420df3 100644 --- a/tests/commands/help.spec.ts +++ b/tests/commands/help.spec.ts @@ -337,7 +337,7 @@ test.group('Help command', () => { static aliases: string[] = ['mc', 'controller'] static commandName: string = 'make:controller' static description: string = 'Make a new HTTP controller' - static help?: string | string[] | undefined = '{{ binaryName }}make:controller' + static help?: string | string[] | undefined = '{{ binaryName }} make:controller' } kernel.addLoader(new ListLoader([MakeController])) diff --git a/tests/formatters/command.spec.ts b/tests/formatters/command.spec.ts index cea5ecd..ac24f63 100644 --- a/tests/formatters/command.spec.ts +++ b/tests/formatters/command.spec.ts @@ -185,7 +185,7 @@ test.group('Formatters | command', () => { test('subsitute binary name in help text', ({ assert }) => { class MakeController extends BaseCommand { static commandName: string = 'make:controller' - static help = 'Make a new HTTP controller {{binaryName}}make:controller ' + static help = 'Make a new HTTP controller {{binaryName}} make:controller ' @args.string() declare name: string From 2c40176541a908285ff429ff252d9d5d8a2949c9 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 17 Apr 2023 15:42:41 +0530 Subject: [PATCH 063/112] chore(release): 12.3.1-6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 689c9ed..a45ae06 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/ace", - "version": "12.3.1-5", + "version": "12.3.1-6", "description": "A CLI framework for Node.js", "main": "build/index.js", "type": "module", From 157f0bca8839e069db6950d2af8476c308db8d19 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 4 Jul 2023 14:55:18 +0530 Subject: [PATCH 064/112] chore: update dependencies --- package.json | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index a45ae06..0f1005a 100644 --- a/package.json +++ b/package.json @@ -46,46 +46,46 @@ "author": "virk,adonisjs", "license": "MIT", "dependencies": { - "@poppinss/cliui": "^6.1.1-2", - "@poppinss/hooks": "^7.1.1-2", - "@poppinss/macroable": "^1.0.0-6", - "@poppinss/prompts": "^3.1.0-2", - "@poppinss/utils": "^6.5.0-2", + "@poppinss/cliui": "^6.1.1-3", + "@poppinss/hooks": "^7.1.1-4", + "@poppinss/macroable": "^1.0.0-7", + "@poppinss/prompts": "^3.1.0-4", + "@poppinss/utils": "^6.5.0-3", "jsonschema": "^1.4.1", "string-similarity": "^4.0.4", - "string-width": "^5.1.2", + "string-width": "^6.1.0", "yargs-parser": "^21.1.1", "youch": "^3.2.3", - "youch-terminal": "^2.2.0" + "youch-terminal": "^2.2.1" }, "devDependencies": { - "@commitlint/cli": "^17.6.1", - "@commitlint/config-conventional": "^17.6.1", + "@commitlint/cli": "^17.6.6", + "@commitlint/config-conventional": "^17.6.6", "@japa/assert": "^1.4.1", "@japa/expect-type": "^1.0.3", - "@japa/file-system": "^1.0.1", + "@japa/file-system": "^1.1.0", "@japa/run-failed-tests": "^1.1.1", "@japa/runner": "^2.5.1", "@japa/spec-reporter": "^1.3.3", - "@swc/core": "^1.3.51", - "@types/node": "^18.15.11", + "@swc/core": "^1.3.67", + "@types/node": "^20.3.3", "@types/string-similarity": "^4.0.0", - "c8": "^7.13.0", + "c8": "^8.0.0", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", "del-cli": "^5.0.0", - "eslint": "^8.38.0", + "eslint": "^8.44.0", "eslint-config-prettier": "^8.8.0", "eslint-plugin-adonis": "^3.0.3", "eslint-plugin-prettier": "^4.2.1", "github-label-sync": "^2.3.1", "husky": "^8.0.3", - "np": "^7.7.0", - "prettier": "^2.8.7", + "np": "^8.0.4", + "prettier": "^2.8.8", "reflect-metadata": "^0.1.13", "ts-json-schema-generator": "^1.2.0", "ts-node": "^10.9.1", - "typescript": "^5.0.4" + "typescript": "^5.1.6" }, "repository": { "type": "git", From 44c799dd97ec754e6e41e841e4de25fdeb0df055 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 4 Jul 2023 14:58:07 +0530 Subject: [PATCH 065/112] chore: upgrade japa to v3 --- bin/japa_types.ts | 7 ------- bin/test.ts | 15 ++++----------- package.json | 10 ++++------ 3 files changed, 8 insertions(+), 24 deletions(-) delete mode 100644 bin/japa_types.ts diff --git a/bin/japa_types.ts b/bin/japa_types.ts deleted file mode 100644 index d42cac6..0000000 --- a/bin/japa_types.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Assert } from '@japa/assert' - -declare module '@japa/runner' { - interface TestContext { - assert: Assert - } -} diff --git a/bin/test.ts b/bin/test.ts index 3c1c21d..bc6801f 100644 --- a/bin/test.ts +++ b/bin/test.ts @@ -1,10 +1,7 @@ import { assert } from '@japa/assert' -import { pathToFileURL } from 'node:url' import { fileSystem } from '@japa/file-system' import { expectTypeOf } from '@japa/expect-type' -import { specReporter } from '@japa/spec-reporter' -import { runFailedTests } from '@japa/run-failed-tests' -import { processCliArgs, configure, run } from '@japa/runner' +import { processCLIArgs, configure, run } from '@japa/runner' /* |-------------------------------------------------------------------------- @@ -19,14 +16,10 @@ import { processCliArgs, configure, run } from '@japa/runner' | | Please consult japa.dev/runner-config for the config docs. */ +processCLIArgs(process.argv.slice(2)) configure({ - ...processCliArgs(process.argv.slice(2)), - ...{ - files: ['tests/**/*.spec.ts'], - plugins: [assert(), runFailedTests(), expectTypeOf(), fileSystem()], - reporters: [specReporter()], - importer: (filePath: string) => import(pathToFileURL(filePath).href), - }, + files: ['tests/**/*.spec.ts'], + plugins: [assert(), expectTypeOf(), fileSystem()], }) /* diff --git a/package.json b/package.json index 0f1005a..1ba5ed7 100644 --- a/package.json +++ b/package.json @@ -61,12 +61,10 @@ "devDependencies": { "@commitlint/cli": "^17.6.6", "@commitlint/config-conventional": "^17.6.6", - "@japa/assert": "^1.4.1", - "@japa/expect-type": "^1.0.3", - "@japa/file-system": "^1.1.0", - "@japa/run-failed-tests": "^1.1.1", - "@japa/runner": "^2.5.1", - "@japa/spec-reporter": "^1.3.3", + "@japa/assert": "^2.0.0-1", + "@japa/expect-type": "^2.0.0-0", + "@japa/file-system": "^2.0.0-1", + "@japa/runner": "^3.0.0-3", "@swc/core": "^1.3.67", "@types/node": "^20.3.3", "@types/string-similarity": "^4.0.0", From 9540d4eac014132c17d5b01f14a0faaaa4a4bbe7 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 4 Jul 2023 15:03:33 +0530 Subject: [PATCH 066/112] chore: use @adonisjs/tooling presets for tooling config --- .github/workflows/checks.yml | 14 +++++++++++ .github/workflows/test.yml | 7 ------ package.json | 48 +++++++++--------------------------- tsconfig.json | 34 +++---------------------- 4 files changed, 28 insertions(+), 75 deletions(-) create mode 100644 .github/workflows/checks.yml delete mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml new file mode 100644 index 0000000..c27fb04 --- /dev/null +++ b/.github/workflows/checks.yml @@ -0,0 +1,14 @@ +name: checks +on: + - push + - pull_request + +jobs: + test: + uses: adonisjs/.github/.github/workflows/test.yml@main + + lint: + uses: adonisjs/.github/.github/workflows/lint.yml@main + + typecheck: + uses: adonisjs/.github/.github/workflows/typecheck.yml@main diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 2d9bc9e..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,7 +0,0 @@ -name: test -on: - - push - - pull_request -jobs: - test: - uses: adonisjs/.github/.github/workflows/test.yml@main diff --git a/package.json b/package.json index 1ba5ed7..5eb1e96 100644 --- a/package.json +++ b/package.json @@ -21,11 +21,12 @@ }, "scripts": { "pretest": "npm run lint", - "test": "cross-env NODE_DEBUG=adonisjs:ace c8 npm run vscode:test", + "test": "cross-env NODE_DEBUG=adonisjs:ace c8 npm run quick:test", "clean": "del-cli build", "build:schema": "ts-json-schema-generator --path='src/types.ts' --type='CommandMetaData' --tsconfig='tsconfig.json' --out='schemas/command_metadata_schema.json'", "copy:files": "copyfiles schemas/* stubs/*.stub build", "precompile": "npm run lint && npm run clean", + "typecheck": "tsc --noEmit", "compile": "tsc", "postcompile": "npm run build:schema && npm run copy:files", "build": "npm run compile", @@ -35,7 +36,7 @@ "lint": "eslint . --ext=.ts", "format": "prettier --write .", "sync-labels": "github-label-sync --labels .github/labels.json adonisjs/ace", - "vscode:test": "node --loader=ts-node/esm bin/test.ts" + "quick:test": "node --loader=ts-node/esm bin/test.ts" }, "keywords": [ "adonisjs", @@ -59,6 +60,9 @@ "youch-terminal": "^2.2.1" }, "devDependencies": { + "@adonisjs/eslint-config": "^1.1.7", + "@adonisjs/prettier-config": "^1.1.7", + "@adonisjs/tsconfig": "^1.1.7", "@commitlint/cli": "^17.6.6", "@commitlint/config-conventional": "^17.6.6", "@japa/assert": "^2.0.0-1", @@ -73,9 +77,6 @@ "cross-env": "^7.0.3", "del-cli": "^5.0.0", "eslint": "^8.44.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-adonis": "^3.0.3", - "eslint-plugin-prettier": "^4.2.1", "github-label-sync": "^2.3.1", "husky": "^8.0.3", "np": "^8.0.4", @@ -92,37 +93,6 @@ "bugs": { "url": "https://github.com/adonisjs/ace/issues" }, - "eslintConfig": { - "extends": [ - "plugin:adonis/typescriptPackage", - "prettier" - ], - "plugins": [ - "prettier" - ], - "rules": { - "prettier/prettier": [ - "error", - { - "endOfLine": "auto" - } - ] - } - }, - "eslintIgnore": [ - "build", - "backup" - ], - "prettier": { - "trailingComma": "es5", - "semi": false, - "singleQuote": true, - "useTabs": false, - "quoteProps": "consistent", - "bracketSpacing": true, - "arrowParens": "always", - "printWidth": 100 - }, "commitlint": { "extends": [ "@commitlint/config-conventional" @@ -148,5 +118,9 @@ "build/**", "examples/**" ] - } + }, + "eslintConfig": { + "extends": "@adonisjs/eslint-config/package" + }, + "prettier": "@adonisjs/prettier-config" } diff --git a/tsconfig.json b/tsconfig.json index f3bdb22..2039043 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,35 +1,7 @@ { + "extends": "@adonisjs/tsconfig/tsconfig.package.json", "compilerOptions": { - "target": "ESNext", - "module": "NodeNext", - "lib": ["ESNext"], - "useDefineForClassFields": true, - "resolveJsonModule": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "isolatedModules": true, - "removeComments": true, - "declaration": true, "rootDir": "./", - "outDir": "./build", - "esModuleInterop": true, - "strictNullChecks": true, - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "allowSyntheticDefaultImports": true, - "forceConsistentCasingInFileNames": true, - "strictPropertyInitialization": true, - "noImplicitAny": true, - "strictBindCallApply": true, - "strictFunctionTypes": true, - "noImplicitThis": true, - "declarationMap": true, - "skipLibCheck": true, - "types": ["@types/node"] - }, - "include": ["./**/*"], - "exclude": ["./node_modules", "./build", "./backup"], - "ts-node": { - "swc": true + "outDir": "./build" } -} +} \ No newline at end of file From e5eb6e80d022c5773ff0570f61250c3d235d72c7 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 4 Jul 2023 15:03:52 +0530 Subject: [PATCH 067/112] chore: do not publish source files --- package.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/package.json b/package.json index 5eb1e96..8d3bc1b 100644 --- a/package.json +++ b/package.json @@ -5,14 +5,10 @@ "main": "build/index.js", "type": "module", "files": [ - "src", - "stubs", - "index.ts", "build/schemas", "build/src", "build/stubs", "build/index.d.ts", - "build/index.d.ts.map", "build/index.js" ], "exports": { From ba97fff4c2436d2b3105ec39453b52c77d0a5823 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 4 Jul 2023 15:04:06 +0530 Subject: [PATCH 068/112] chore: add engines to package.json file --- package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package.json b/package.json index 8d3bc1b..b7a70ce 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,9 @@ ".": "./build/index.js", "./types": "./build/src/types.js" }, + "engines": { + "node": ">=18.16.0" + }, "scripts": { "pretest": "npm run lint", "test": "cross-env NODE_DEBUG=adonisjs:ace c8 npm run quick:test", From e2a9b35fcd4bc57edcce59ea728f5e60ea746268 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 4 Jul 2023 15:09:38 +0530 Subject: [PATCH 069/112] fix(tsconfig): resolve JSON modules --- tsconfig.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index 2039043..441ca84 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,7 @@ "extends": "@adonisjs/tsconfig/tsconfig.package.json", "compilerOptions": { "rootDir": "./", - "outDir": "./build" + "outDir": "./build", + "resolveJsonModule": true } -} \ No newline at end of file +} From 9bafd0b8de06c675edcd577082087cc1c292e45f Mon Sep 17 00:00:00 2001 From: Julien Ripouteau Date: Tue, 4 Jul 2023 11:44:32 +0200 Subject: [PATCH 070/112] feat: add absoluteFilePath to CommandMetadata (#153) --- package.json | 1 + src/loaders/fs_loader.ts | 15 ++++++++++----- tests/loaders/fs_loader.spec.ts | 6 ++++++ 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index b7a70ce..7438889 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "@swc/core": "^1.3.67", "@types/node": "^20.3.3", "@types/string-similarity": "^4.0.0", + "@types/yargs-parser": "^21.0.0", "c8": "^8.0.0", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", diff --git a/src/loaders/fs_loader.ts b/src/loaders/fs_loader.ts index b2b2d2b..9204b98 100644 --- a/src/loaders/fs_loader.ts +++ b/src/loaders/fs_loader.ts @@ -8,7 +8,7 @@ */ import { fileURLToPath } from 'node:url' -import { basename, extname, relative } from 'node:path' +import { basename, extname, join, relative } from 'node:path' import { fsReadAll, importDefault, slash } from '@poppinss/utils' import { validateCommand } from '../helpers.js' @@ -35,7 +35,7 @@ export class FsLoader implements LoadersCon /** * An array of loaded commands */ - #commands: { command: Command; filePath: string }[] = [] + #commands: { command: Command; filePath: string; absoluteFilePath: string }[] = [] constructor(comandsDirectory: string, filter?: (filePath: string) => boolean) { this.#comandsDirectory = comandsDirectory @@ -112,11 +112,16 @@ export class FsLoader implements LoadersCon Object.keys(commandsCollection).forEach((key) => { const command = commandsCollection[key] validateCommand(command, `"${key}" file`) - this.#commands.push({ command, filePath: key }) + + this.#commands.push({ + command, + filePath: key, + absoluteFilePath: slash(join(this.#comandsDirectory, key)), + }) }) - return this.#commands.map(({ command, filePath }) => { - return Object.assign({}, command.serialize(), { filePath }) + return this.#commands.map(({ command, filePath, absoluteFilePath }) => { + return Object.assign({}, command.serialize(), { filePath, absoluteFilePath }) }) } diff --git a/tests/loaders/fs_loader.spec.ts b/tests/loaders/fs_loader.spec.ts index 716fc83..54cdf44 100644 --- a/tests/loaders/fs_loader.spec.ts +++ b/tests/loaders/fs_loader.spec.ts @@ -11,6 +11,7 @@ import { join } from 'node:path' import { test } from '@japa/runner' import { fileURLToPath } from 'node:url' import { FsLoader } from '../../src/loaders/fs_loader.js' +import { slash } from '@poppinss/utils' const BASE_URL = new URL('./tmp/', import.meta.url) const BASE_PATH = fileURLToPath(BASE_URL) @@ -74,6 +75,7 @@ test.group('Loaders | fs', (group) => { assert.deepEqual(commands, [ { filePath: 'make_controller_v_2.js', + absoluteFilePath: slash(join(fs.basePath, './commands/make_controller_v_2.js')), commandName: 'make:controller', description: '', namespace: 'make', @@ -119,6 +121,7 @@ test.group('Loaders | fs', (group) => { { commandName: 'make:controller', filePath: 'make_controller.js', + absoluteFilePath: slash(join(fs.basePath, './commands/make_controller.js')), description: '', namespace: 'make', args: [], @@ -165,6 +168,7 @@ test.group('Loaders | fs', (group) => { { commandName: 'make:controller', filePath: 'make_controller_v_3.js', + absoluteFilePath: slash(join(fs.basePath, './commands/make_controller_v_3.js')), description: '', namespace: 'make', args: [], @@ -251,6 +255,7 @@ test.group('Loaders | fs', (group) => { { commandName: 'make:controller', filePath: 'make/controller.js', + absoluteFilePath: slash(join(fs.basePath, './commands/make/controller.js')), description: '', namespace: 'make', args: [], @@ -323,6 +328,7 @@ test.group('Loaders | fs', (group) => { const commands = await loader.getMetaData() assert.deepEqual(commands, [ { + absoluteFilePath: slash(join(fs.basePath, 'commands/make/controller.js')), commandName: 'make:controller', filePath: 'make/controller.js', description: '', From aa07f83307191ce75bc1d6a428d855c8bb913a1e Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 4 Jul 2023 15:17:11 +0530 Subject: [PATCH 071/112] chore(release): 12.3.1-7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7438889..9a72848 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/ace", - "version": "12.3.1-6", + "version": "12.3.1-7", "description": "A CLI framework for Node.js", "main": "build/index.js", "type": "module", From a2f47b4a215cf3bdcc1078455454c59c397b77c2 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 5 Jul 2023 11:35:05 +0530 Subject: [PATCH 072/112] refactor: remove string-similarity package in favor of fastest-levenshtein string-similarity package has been deprecated. Also, fastest-levenshtein results are not as accurate as string-similarity because of different underlying algorithms in use --- package.json | 3 +-- src/kernel.ts | 34 +++++++++++++++++++++++++--------- tests/kernel/main.spec.ts | 4 ++-- 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 9a72848..6790275 100644 --- a/package.json +++ b/package.json @@ -51,8 +51,8 @@ "@poppinss/macroable": "^1.0.0-7", "@poppinss/prompts": "^3.1.0-4", "@poppinss/utils": "^6.5.0-3", + "fastest-levenshtein": "^1.0.16", "jsonschema": "^1.4.1", - "string-similarity": "^4.0.4", "string-width": "^6.1.0", "yargs-parser": "^21.1.1", "youch": "^3.2.3", @@ -70,7 +70,6 @@ "@japa/runner": "^3.0.0-3", "@swc/core": "^1.3.67", "@types/node": "^20.3.3", - "@types/string-similarity": "^4.0.0", "@types/yargs-parser": "^21.0.0", "c8": "^8.0.0", "copyfiles": "^2.4.1", diff --git a/src/kernel.ts b/src/kernel.ts index 25eb2fa..f7926ba 100644 --- a/src/kernel.ts +++ b/src/kernel.ts @@ -10,7 +10,7 @@ import Hooks from '@poppinss/hooks' import { cliui } from '@poppinss/cliui' import { Prompt } from '@poppinss/prompts' -import { findBestMatch } from 'string-similarity' +import { distance } from 'fastest-levenshtein' import { RuntimeException } from '@poppinss/utils' import debug from './debug.js' @@ -543,20 +543,36 @@ export class Kernel { const commandsAndAliases = [...this.#commands.keys()].concat([...this.#aliases.keys()]) - return findBestMatch(keyword, commandsAndAliases) - .ratings.sort((current, next) => next.rating - current.rating) - .filter((rating) => rating.rating > 0.4) - .map((rating) => rating.target) + return commandsAndAliases + .map((value) => { + return { + value, + distance: distance(keyword, value), + } + }) + .sort((current, next) => next.distance - current.distance) + .filter((rating) => { + return rating.distance <= 3 + }) + .map((rating) => rating.value) } /** * Returns an array of namespaces suggestions for a given keyword. */ getNamespaceSuggestions(keyword: string): string[] { - return findBestMatch(keyword, this.#namespaces) - .ratings.sort((current, next) => next.rating - current.rating) - .filter((rating) => rating.rating > 0.4) - .map((rating) => rating.target) + return this.#namespaces + .map((value) => { + return { + value, + distance: distance(keyword, value), + } + }) + .sort((current, next) => next.distance - current.distance) + .filter((rating) => { + return rating.distance <= 3 + }) + .map((rating) => rating.value) } /** diff --git a/tests/kernel/main.spec.ts b/tests/kernel/main.spec.ts index ee117c6..b4ef70e 100644 --- a/tests/kernel/main.spec.ts +++ b/tests/kernel/main.spec.ts @@ -246,8 +246,8 @@ test.group('Kernel', () => { kernel.addAlias('mm', 'unrecognized:command') await kernel.boot() - assert.deepEqual(kernel.getCommandSuggestions('controller'), ['make:controller']) - assert.deepEqual(kernel.getCommandSuggestions('migrate'), ['migration:run']) + assert.deepEqual(kernel.getCommandSuggestions('controller'), []) + assert.deepEqual(kernel.getCommandSuggestions('migrate'), []) }) test('get commands suggestions for a namespace', async ({ assert }) => { From b6233637fd5d421a57867868b43e32254be58044 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 5 Jul 2023 12:24:12 +0530 Subject: [PATCH 073/112] chore(release): 12.3.1-8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6790275..6224855 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/ace", - "version": "12.3.1-7", + "version": "12.3.1-8", "description": "A CLI framework for Node.js", "main": "build/index.js", "type": "module", From 94492ec790001cd066f2a13f089a9cfa4952b1b2 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 5 Jul 2023 12:43:38 +0530 Subject: [PATCH 074/112] feat: add assertTableRows method --- src/commands/base.ts | 22 ++++++++++++ tests/base_command/assertions.spec.ts | 50 +++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/src/commands/base.ts b/src/commands/base.ts index 805c317..ccf59f1 100644 --- a/src/commands/base.ts +++ b/src/commands/base.ts @@ -657,4 +657,26 @@ export class BaseCommand extends Macroable { throw error } } + + /** + * Assert the command prints a table to stdout + */ + assertTableRows(rows: string[][]) { + const logs = this.logger.getLogs() + const hasAllMatchingRows = rows.every((row) => { + const columnsContent = row.join('|') + return !!logs.find((log) => log.message === columnsContent) + }) + + if (!hasAllMatchingRows) { + const error = new AssertionError({ + message: `Expected log messages to include a table with the expected rows`, + operator: 'strictEqual', + stackStartFn: this.assertTableRows, + }) + Object.defineProperty(error, 'showDiff', { value: true }) + + throw error + } + } } diff --git a/tests/base_command/assertions.spec.ts b/tests/base_command/assertions.spec.ts index 01aa3bf..e598760 100644 --- a/tests/base_command/assertions.spec.ts +++ b/tests/base_command/assertions.spec.ts @@ -165,4 +165,54 @@ test.group('Base command | assertions', () => { `Expected log message stream to be 'stderr', instead received 'stdout'` ) }) + + test('assert command logs a table', async ({ assert }) => { + class MakeModel extends BaseCommand { + static commandName: string = 'make:model' + async run() { + const table = this.ui.table() + table.head(['Name', 'Email']) + + table.row(['Harminder Virk', 'virk@adonisjs.com']) + table.row(['Romain Lanz', 'romain@adonisjs.com']) + table.row(['Julien-R44', 'julien@adonisjs.com']) + table.row(['Michaël Zasso', 'targos@adonisjs.com']) + + table.render() + + return super.run() + } + } + + const kernel = Kernel.create() + kernel.ui.switchMode('raw') + + const model = await kernel.create(MakeModel, []) + await model.exec() + + assert.doesNotThrows(() => + model.assertTableRows([ + ['Harminder Virk', 'virk@adonisjs.com'], + ['Romain Lanz', 'romain@adonisjs.com'], + ['Julien-R44', 'julien@adonisjs.com'], + ]) + ) + assert.doesNotThrows(() => + model.assertTableRows([ + ['Harminder Virk', 'virk@adonisjs.com'], + ['Romain Lanz', 'romain@adonisjs.com'], + ['Julien-R44', 'julien@adonisjs.com'], + ['Michaël Zasso', 'targos@adonisjs.com'], + ]) + ) + + assert.throws( + () => + model.assertTableRows([ + ['Harminder Virk', 'virk@adonisjs.com'], + ['Romain', 'romain@adonisjs.com'], + ]), + `Expected log messages to include a table with the expected rows` + ) + }) }) From 62c2034bff5bdf1b7aec02e748041705cde14e98 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 5 Jul 2023 12:44:54 +0530 Subject: [PATCH 075/112] refactor: do not assign showDiff property to assertion errors --- src/commands/base.ts | 28 ++++++---------------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/src/commands/base.ts b/src/commands/base.ts index ccf59f1..cd70772 100644 --- a/src/commands/base.ts +++ b/src/commands/base.ts @@ -539,16 +539,13 @@ export class BaseCommand extends Macroable { */ assertExitCode(code: number) { if (this.exitCode !== code) { - const error = new AssertionError({ + throw new AssertionError({ message: `Expected '${this.commandName}' command to finish with exit code '${code}'`, actual: this.exitCode, expected: code, operator: 'strictEqual', stackStartFn: this.assertExitCode, }) - Object.defineProperty(error, 'showDiff', { value: true }) - - throw error } } @@ -590,23 +587,20 @@ export class BaseCommand extends Macroable { * No log found */ if (!matchingLog) { - const error = new AssertionError({ + throw new AssertionError({ message: `Expected log messages to include ${inspect(message)}`, actual: logMessages, expected: [message], operator: 'strictEqual', stackStartFn: this.assertLog, }) - Object.defineProperty(error, 'showDiff', { value: true }) - - throw error } /** * Log is on a different stream */ if (stream && matchingLog.stream !== stream) { - const error = new AssertionError({ + throw new AssertionError({ message: `Expected log message stream to be ${inspect(stream)}, instead received ${inspect( matchingLog.stream )}`, @@ -615,9 +609,6 @@ export class BaseCommand extends Macroable { operator: 'strictEqual', stackStartFn: this.assertLog, }) - Object.defineProperty(error, 'showDiff', { value: true }) - - throw error } } @@ -632,18 +623,17 @@ export class BaseCommand extends Macroable { * No log found */ if (!matchingLog) { - const error = new AssertionError({ + throw new AssertionError({ message: `Expected log messages to match ${inspect(matchingRegex)}`, stackStartFn: this.assertLogMatches, }) - throw error } /** * Log is on a different stream */ if (stream && matchingLog.stream !== stream) { - const error = new AssertionError({ + throw new AssertionError({ message: `Expected log message stream to be ${inspect(stream)}, instead received ${inspect( matchingLog.stream )}`, @@ -652,9 +642,6 @@ export class BaseCommand extends Macroable { operator: 'strictEqual', stackStartFn: this.assertLogMatches, }) - Object.defineProperty(error, 'showDiff', { value: true }) - - throw error } } @@ -669,14 +656,11 @@ export class BaseCommand extends Macroable { }) if (!hasAllMatchingRows) { - const error = new AssertionError({ + throw new AssertionError({ message: `Expected log messages to include a table with the expected rows`, operator: 'strictEqual', stackStartFn: this.assertTableRows, }) - Object.defineProperty(error, 'showDiff', { value: true }) - - throw error } } } From 6da0d9c4556465ce4bf22fb71957eb99eefbd302 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 26 Jul 2023 13:44:41 +0530 Subject: [PATCH 076/112] chore: update dependencies --- package.json | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 6224855..79b2a28 100644 --- a/package.json +++ b/package.json @@ -59,27 +59,27 @@ "youch-terminal": "^2.2.1" }, "devDependencies": { - "@adonisjs/eslint-config": "^1.1.7", - "@adonisjs/prettier-config": "^1.1.7", - "@adonisjs/tsconfig": "^1.1.7", - "@commitlint/cli": "^17.6.6", - "@commitlint/config-conventional": "^17.6.6", + "@adonisjs/eslint-config": "^1.1.8", + "@adonisjs/prettier-config": "^1.1.8", + "@adonisjs/tsconfig": "^1.1.8", + "@commitlint/cli": "^17.6.7", + "@commitlint/config-conventional": "^17.6.7", "@japa/assert": "^2.0.0-1", "@japa/expect-type": "^2.0.0-0", "@japa/file-system": "^2.0.0-1", "@japa/runner": "^3.0.0-3", - "@swc/core": "^1.3.67", - "@types/node": "^20.3.3", + "@swc/core": "^1.3.71", + "@types/node": "^20.4.5", "@types/yargs-parser": "^21.0.0", - "c8": "^8.0.0", + "c8": "^8.0.1", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", "del-cli": "^5.0.0", - "eslint": "^8.44.0", + "eslint": "^8.45.0", "github-label-sync": "^2.3.1", "husky": "^8.0.3", "np": "^8.0.4", - "prettier": "^2.8.8", + "prettier": "^3.0.0", "reflect-metadata": "^0.1.13", "ts-json-schema-generator": "^1.2.0", "ts-node": "^10.9.1", From 01240d07b8dc5c05f3058b511792f66ae4601d7f Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 26 Jul 2023 13:52:20 +0530 Subject: [PATCH 077/112] chore(release): 12.3.1-9 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 79b2a28..385d866 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/ace", - "version": "12.3.1-8", + "version": "12.3.1-9", "description": "A CLI framework for Node.js", "main": "build/index.js", "type": "module", From 9967a10f65cce6c5fa8b28dcc97e53375ec15f00 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 21 Aug 2023 11:48:16 +0530 Subject: [PATCH 078/112] chore: update dependencies --- package.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 385d866..757aed6 100644 --- a/package.json +++ b/package.json @@ -62,26 +62,26 @@ "@adonisjs/eslint-config": "^1.1.8", "@adonisjs/prettier-config": "^1.1.8", "@adonisjs/tsconfig": "^1.1.8", - "@commitlint/cli": "^17.6.7", - "@commitlint/config-conventional": "^17.6.7", + "@commitlint/cli": "^17.7.1", + "@commitlint/config-conventional": "^17.7.0", "@japa/assert": "^2.0.0-1", "@japa/expect-type": "^2.0.0-0", "@japa/file-system": "^2.0.0-1", "@japa/runner": "^3.0.0-3", - "@swc/core": "^1.3.71", - "@types/node": "^20.4.5", + "@swc/core": "^1.3.78", + "@types/node": "^20.5.1", "@types/yargs-parser": "^21.0.0", "c8": "^8.0.1", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", "del-cli": "^5.0.0", - "eslint": "^8.45.0", + "eslint": "^8.47.0", "github-label-sync": "^2.3.1", "husky": "^8.0.3", "np": "^8.0.4", - "prettier": "^3.0.0", + "prettier": "^3.0.2", "reflect-metadata": "^0.1.13", - "ts-json-schema-generator": "^1.2.0", + "ts-json-schema-generator": "^1.3.0", "ts-node": "^10.9.1", "typescript": "^5.1.6" }, From 04aba7cb50132e08b24fa19f28859d921cb6840b Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 21 Aug 2023 12:13:55 +0530 Subject: [PATCH 079/112] feat: generate commands index types --- src/generators/index_generator.ts | 6 ++++++ stubs/commands_loader_types.stub | 4 ++++ tests/index_generator.spec.ts | 2 ++ 3 files changed, 12 insertions(+) create mode 100644 stubs/commands_loader_types.stub diff --git a/src/generators/index_generator.ts b/src/generators/index_generator.ts index f37cd6d..79bbe10 100644 --- a/src/generators/index_generator.ts +++ b/src/generators/index_generator.ts @@ -41,6 +41,9 @@ export class IndexGenerator { const loaderFile = join(this.#commandsDir, 'main.js') const loaderStub = join(stubsRoot, 'commands_loader.stub') + const loaderTypes = join(this.#commandsDir, 'main.d.ts') + const loaderTypesStub = join(stubsRoot, 'commands_loader_types.stub') + await mkdir(this.#commandsDir, { recursive: true }) console.log(`artifacts directory: ${this.#commandsDir}`) @@ -49,5 +52,8 @@ export class IndexGenerator { await copyFile(loaderStub, loaderFile) console.log('create main.js') + + await copyFile(loaderTypesStub, loaderTypes) + console.log('create main.d.ts') } } diff --git a/stubs/commands_loader_types.stub b/stubs/commands_loader_types.stub new file mode 100644 index 0000000..693355b --- /dev/null +++ b/stubs/commands_loader_types.stub @@ -0,0 +1,4 @@ +import { CommandMetaData, Command } from '@adonisjs/ace/types'; + +export function getMetaData(): Promise +export function getCommand(metaData: CommandMetaData): Promise diff --git a/tests/index_generator.spec.ts b/tests/index_generator.spec.ts index 435eea8..f1f06bf 100644 --- a/tests/index_generator.spec.ts +++ b/tests/index_generator.spec.ts @@ -56,6 +56,8 @@ test.group('Index generator', (group) => { const generator = new IndexGenerator(join(fs.basePath, 'commands')) await generator.generate() + await assert.fileExists('commands/main.d.ts') + /** * Validate index */ From dbb5f231bd043614d25704b86e09f7ac046fae62 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 21 Aug 2023 12:26:29 +0530 Subject: [PATCH 080/112] chore(release): 12.3.1-10 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 757aed6..57d55b5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/ace", - "version": "12.3.1-9", + "version": "12.3.1-10", "description": "A CLI framework for Node.js", "main": "build/index.js", "type": "module", From bd56441347367193dcebc2a72c192dfe81cae590 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 23 Aug 2023 13:00:14 +0530 Subject: [PATCH 081/112] chore: update dependencies --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 57d55b5..f0f8ed8 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "@poppinss/hooks": "^7.1.1-4", "@poppinss/macroable": "^1.0.0-7", "@poppinss/prompts": "^3.1.0-4", - "@poppinss/utils": "^6.5.0-3", + "@poppinss/utils": "^6.5.0-5", "fastest-levenshtein": "^1.0.16", "jsonschema": "^1.4.1", "string-width": "^6.1.0", @@ -69,7 +69,7 @@ "@japa/file-system": "^2.0.0-1", "@japa/runner": "^3.0.0-3", "@swc/core": "^1.3.78", - "@types/node": "^20.5.1", + "@types/node": "^20.5.3", "@types/yargs-parser": "^21.0.0", "c8": "^8.0.1", "copyfiles": "^2.4.1", From 9d54b6d3667757206dec85e082b6af6a3d661412 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 23 Aug 2023 13:03:34 +0530 Subject: [PATCH 082/112] chore: bundle output using tsup --- package.json | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index f0f8ed8..c4b9689 100644 --- a/package.json +++ b/package.json @@ -5,11 +5,7 @@ "main": "build/index.js", "type": "module", "files": [ - "build/schemas", - "build/src", - "build/stubs", - "build/index.d.ts", - "build/index.js" + "build" ], "exports": { ".": "./build/index.js", @@ -26,7 +22,7 @@ "copy:files": "copyfiles schemas/* stubs/*.stub build", "precompile": "npm run lint && npm run clean", "typecheck": "tsc --noEmit", - "compile": "tsc", + "compile": "tsup-node", "postcompile": "npm run build:schema && npm run copy:files", "build": "npm run compile", "release": "np", @@ -83,6 +79,7 @@ "reflect-metadata": "^0.1.13", "ts-json-schema-generator": "^1.3.0", "ts-node": "^10.9.1", + "tsup": "^7.2.0", "typescript": "^5.1.6" }, "repository": { @@ -121,5 +118,16 @@ "eslintConfig": { "extends": "@adonisjs/eslint-config/package" }, - "prettier": "@adonisjs/prettier-config" + "prettier": "@adonisjs/prettier-config", + "tsup": { + "entry": [ + "./index.ts", + "./src/types.ts" + ], + "outDir": "./build", + "clean": true, + "format": "esm", + "dts": true, + "target": "esnext" + } } From 584eec04c0f0f55d7bc21ae973536c533df44319 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 23 Aug 2023 13:05:34 +0530 Subject: [PATCH 083/112] docs: update badges url --- README.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 0a51c8e..7abb2ee 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@
-[![gh-workflow-image]][gh-workflow-url] [![npm-image]][npm-url] ![][typescript-image] [![license-image]][license-url] [![synk-image]][synk-url] +[![gh-workflow-image]][gh-workflow-url] [![npm-image]][npm-url] ![][typescript-image] [![license-image]][license-url] ## Introduction Ace is the command-line framework for Node.js. It is built with **testing in mind**, is **light weight** in comparison to other CLI frameworks, and offers a clean API for creating CLI commands. @@ -21,8 +21,8 @@ In order to ensure that the AdonisJS community is welcoming to all, please revie ## License AdonisJS Ace is open-sourced software licensed under the [MIT license](LICENSE.md). -[gh-workflow-image]: https://img.shields.io/github/actions/workflow/status/adonisjs/ace/test.yml?style=for-the-badge -[gh-workflow-url]: https://github.com/adonisjs/ace/actions/workflows/test.yml "Github action" +[gh-workflow-image]: https://img.shields.io/github/actions/workflow/status/adonisjs/ace/checks.yml?style=for-the-badge +[gh-workflow-url]: https://github.com/adonisjs/ace/actions/workflows/checks.yml "Github action" [npm-image]: https://img.shields.io/npm/v/@adonisjs/ace/latest.svg?style=for-the-badge&logo=npm [npm-url]: https://npmjs.org/package/@adonisjs/ace/v/latest "npm" @@ -31,6 +31,3 @@ AdonisJS Ace is open-sourced software licensed under the [MIT license](LICENSE.m [license-url]: LICENSE.md [license-image]: https://img.shields.io/github/license/adonisjs/ace?style=for-the-badge - -[synk-image]: https://img.shields.io/snyk/vulnerabilities/github/adonisjs/ace?label=Synk%20Vulnerabilities&style=for-the-badge -[synk-url]: https://snyk.io/test/github/adonisjs/ace?targetFile=package.json "synk" From e517d1fd3b3071abf7056568cb62c23c5ea2793d Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 23 Aug 2023 13:09:40 +0530 Subject: [PATCH 084/112] chore(release): 12.3.1-11 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c4b9689..abe20b4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/ace", - "version": "12.3.1-10", + "version": "12.3.1-11", "description": "A CLI framework for Node.js", "main": "build/index.js", "type": "module", From 82584c87292771f35b91062d35e69e80ff9e48e9 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 23 Aug 2023 13:35:37 +0530 Subject: [PATCH 085/112] fix: build path to stubs after bundling --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index abe20b4..414baa6 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "test": "cross-env NODE_DEBUG=adonisjs:ace c8 npm run quick:test", "clean": "del-cli build", "build:schema": "ts-json-schema-generator --path='src/types.ts' --type='CommandMetaData' --tsconfig='tsconfig.json' --out='schemas/command_metadata_schema.json'", - "copy:files": "copyfiles schemas/* stubs/*.stub build", + "copy:files": "copyfiles schemas/* build && copyfiles --up=1 stubs/*.stub build", "precompile": "npm run lint && npm run clean", "typecheck": "tsc --noEmit", "compile": "tsup-node", From 34801cc2ead9b7f159c2a56a87cdecc053c1751f Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 23 Aug 2023 13:40:05 +0530 Subject: [PATCH 086/112] chore(release): 12.3.1-12 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 414baa6..e667de9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/ace", - "version": "12.3.1-11", + "version": "12.3.1-12", "description": "A CLI framework for Node.js", "main": "build/index.js", "type": "module", From feab411151fcf370f2f02af1a83de0953343ba0a Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 25 Sep 2023 17:24:09 +0530 Subject: [PATCH 087/112] chore: update dependencies --- package.json | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index e667de9..35ade78 100644 --- a/package.json +++ b/package.json @@ -42,17 +42,17 @@ "author": "virk,adonisjs", "license": "MIT", "dependencies": { - "@poppinss/cliui": "^6.1.1-3", - "@poppinss/hooks": "^7.1.1-4", - "@poppinss/macroable": "^1.0.0-7", - "@poppinss/prompts": "^3.1.0-4", - "@poppinss/utils": "^6.5.0-5", + "@poppinss/cliui": "^6.1.1-4", + "@poppinss/hooks": "^7.1.1-5", + "@poppinss/macroable": "^1.0.0-8", + "@poppinss/prompts": "^3.1.0-5", + "@poppinss/utils": "^6.5.0-7", "fastest-levenshtein": "^1.0.16", "jsonschema": "^1.4.1", "string-width": "^6.1.0", "yargs-parser": "^21.1.1", - "youch": "^3.2.3", - "youch-terminal": "^2.2.1" + "youch": "^3.3.2", + "youch-terminal": "^2.2.3" }, "devDependencies": { "@adonisjs/eslint-config": "^1.1.8", @@ -60,27 +60,27 @@ "@adonisjs/tsconfig": "^1.1.8", "@commitlint/cli": "^17.7.1", "@commitlint/config-conventional": "^17.7.0", - "@japa/assert": "^2.0.0-1", - "@japa/expect-type": "^2.0.0-0", - "@japa/file-system": "^2.0.0-1", - "@japa/runner": "^3.0.0-3", - "@swc/core": "^1.3.78", - "@types/node": "^20.5.3", - "@types/yargs-parser": "^21.0.0", + "@japa/assert": "^2.0.0-2", + "@japa/expect-type": "^2.0.0-1", + "@japa/file-system": "^2.0.0-2", + "@japa/runner": "^3.0.0-9", + "@swc/core": "1.3.82", + "@types/node": "^20.6.5", + "@types/yargs-parser": "^21.0.1", "c8": "^8.0.1", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", - "del-cli": "^5.0.0", - "eslint": "^8.47.0", + "del-cli": "^5.1.0", + "eslint": "^8.50.0", "github-label-sync": "^2.3.1", "husky": "^8.0.3", "np": "^8.0.4", - "prettier": "^3.0.2", + "prettier": "^3.0.3", "reflect-metadata": "^0.1.13", "ts-json-schema-generator": "^1.3.0", "ts-node": "^10.9.1", "tsup": "^7.2.0", - "typescript": "^5.1.6" + "typescript": "^5.2.2" }, "repository": { "type": "git", From d185f5246b52b6221ac3eed2af5804290ef04628 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 25 Sep 2023 17:30:31 +0530 Subject: [PATCH 088/112] feat: export E_PROMPT_CANCELLED exception --- src/errors.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/errors.ts b/src/errors.ts index 232ec21..f7e34d9 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -7,8 +7,11 @@ * file that was distributed with this source code. */ +import { errors } from '@poppinss/prompts' import { createError, Exception } from '@poppinss/utils' +export const E_PROMPT_CANCELLED = errors.E_PROMPT_CANCELLED + /** * Command is missing the static property command name */ From 261b839edaacbdb99b2b73057a1cba68b86f78eb Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 26 Sep 2023 10:38:32 +0530 Subject: [PATCH 089/112] refactor: do not import assertions as they are not stable --- src/helpers.ts | 6 +++++- tsconfig.json | 3 +-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/helpers.ts b/src/helpers.ts index a887731..d34c82b 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -8,10 +8,14 @@ */ import { Validator } from 'jsonschema' +import { readFile } from 'node:fs/promises' import { RuntimeException } from '@poppinss/utils' -import schema from '../schemas/command_metadata_schema.json' assert { type: 'json' } import type { AbstractBaseCommand, CommandMetaData, UIPrimitives } from './types.js' +const schema = JSON.parse( + await readFile(new URL('./schemas/command_metadata_schema.json', import.meta.url), 'utf8') +) + /** * Helper to sort array of strings alphabetically. */ diff --git a/tsconfig.json b/tsconfig.json index 441ca84..ad0cc44 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,6 @@ "extends": "@adonisjs/tsconfig/tsconfig.package.json", "compilerOptions": { "rootDir": "./", - "outDir": "./build", - "resolveJsonModule": true + "outDir": "./build" } } From 884bceb18152cc02d40257a32c7f15eff2ee7e38 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 26 Sep 2023 10:44:43 +0530 Subject: [PATCH 090/112] chore: generate correct path to schema file post build --- package.json | 2 +- schemas/main.ts | 10 ++++++++++ src/commands/base.ts | 3 ++- src/helpers.ts | 4 +++- 4 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 schemas/main.ts diff --git a/package.json b/package.json index 35ade78..73ee553 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "test": "cross-env NODE_DEBUG=adonisjs:ace c8 npm run quick:test", "clean": "del-cli build", "build:schema": "ts-json-schema-generator --path='src/types.ts' --type='CommandMetaData' --tsconfig='tsconfig.json' --out='schemas/command_metadata_schema.json'", - "copy:files": "copyfiles schemas/* build && copyfiles --up=1 stubs/*.stub build", + "copy:files": "copyfiles --up=1 schemas/* build && copyfiles --up=1 stubs/*.stub build", "precompile": "npm run lint && npm run clean", "typecheck": "tsc --noEmit", "compile": "tsup-node", diff --git a/schemas/main.ts b/schemas/main.ts new file mode 100644 index 0000000..fe7e759 --- /dev/null +++ b/schemas/main.ts @@ -0,0 +1,10 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export const schemaRoot = import.meta.url diff --git a/src/commands/base.ts b/src/commands/base.ts index cd70772..de7108f 100644 --- a/src/commands/base.ts +++ b/src/commands/base.ts @@ -13,6 +13,7 @@ import Macroable from '@poppinss/macroable' import lodash from '@poppinss/utils/lodash' import { AssertionError } from 'node:assert' import type { Prompt } from '@poppinss/prompts' +import type { Colors } from '@poppinss/cliui/types' import { defineStaticProperty, InvalidArgumentsException } from '@poppinss/utils' import debug from '../debug.js' @@ -409,7 +410,7 @@ export class BaseCommand extends Macroable { /** * Add colors to console messages */ - get colors() { + get colors(): Colors { return this.ui.colors } diff --git a/src/helpers.ts b/src/helpers.ts index d34c82b..83cfc17 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -10,10 +10,12 @@ import { Validator } from 'jsonschema' import { readFile } from 'node:fs/promises' import { RuntimeException } from '@poppinss/utils' + +import { schemaRoot } from '../schemas/main.js' import type { AbstractBaseCommand, CommandMetaData, UIPrimitives } from './types.js' const schema = JSON.parse( - await readFile(new URL('./schemas/command_metadata_schema.json', import.meta.url), 'utf8') + await readFile(new URL('./command_metadata_schema.json', schemaRoot), 'utf8') ) /** From 07f297e2114ee1fa22e4fc982f702079bafd6847 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 26 Sep 2023 10:54:52 +0530 Subject: [PATCH 091/112] chore(release): 12.3.1-13 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 73ee553..1c6995f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/ace", - "version": "12.3.1-12", + "version": "12.3.1-13", "description": "A CLI framework for Node.js", "main": "build/index.js", "type": "module", From f149393dd5dc66a7ab2750418fb774236d1fb6c3 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 17 Oct 2023 11:03:33 +0530 Subject: [PATCH 092/112] chore: update dependencies --- package.json | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 1c6995f..0f761fb 100644 --- a/package.json +++ b/package.json @@ -42,11 +42,11 @@ "author": "virk,adonisjs", "license": "MIT", "dependencies": { - "@poppinss/cliui": "^6.1.1-4", - "@poppinss/hooks": "^7.1.1-5", - "@poppinss/macroable": "^1.0.0-8", - "@poppinss/prompts": "^3.1.0-5", - "@poppinss/utils": "^6.5.0-7", + "@poppinss/cliui": "^6.2.0", + "@poppinss/hooks": "^7.2.0", + "@poppinss/macroable": "^1.0.0", + "@poppinss/prompts": "^3.1.0", + "@poppinss/utils": "^6.5.0", "fastest-levenshtein": "^1.0.16", "jsonschema": "^1.4.1", "string-width": "^6.1.0", @@ -58,26 +58,25 @@ "@adonisjs/eslint-config": "^1.1.8", "@adonisjs/prettier-config": "^1.1.8", "@adonisjs/tsconfig": "^1.1.8", - "@commitlint/cli": "^17.7.1", - "@commitlint/config-conventional": "^17.7.0", - "@japa/assert": "^2.0.0-2", - "@japa/expect-type": "^2.0.0-1", - "@japa/file-system": "^2.0.0-2", - "@japa/runner": "^3.0.0-9", + "@commitlint/cli": "^17.8.0", + "@commitlint/config-conventional": "^17.8.0", + "@japa/assert": "^2.0.0", + "@japa/expect-type": "^2.0.0", + "@japa/file-system": "^2.0.0", + "@japa/runner": "^3.0.2", "@swc/core": "1.3.82", - "@types/node": "^20.6.5", + "@types/node": "^20.8.6", "@types/yargs-parser": "^21.0.1", "c8": "^8.0.1", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", "del-cli": "^5.1.0", - "eslint": "^8.50.0", + "eslint": "^8.51.0", "github-label-sync": "^2.3.1", "husky": "^8.0.3", "np": "^8.0.4", "prettier": "^3.0.3", - "reflect-metadata": "^0.1.13", - "ts-json-schema-generator": "^1.3.0", + "ts-json-schema-generator": "^1.4.0", "ts-node": "^10.9.1", "tsup": "^7.2.0", "typescript": "^5.2.2" From ce8622038a47e93d1c134cb06389c3e60fdbd3ec Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 17 Oct 2023 11:09:22 +0530 Subject: [PATCH 093/112] chore(release): 12.3.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0f761fb..ce0ab75 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/ace", - "version": "12.3.1-13", + "version": "12.3.1", "description": "A CLI framework for Node.js", "main": "build/index.js", "type": "module", From 8146b319737bfa16353ab45bcd5e71e05ee67875 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 17 Oct 2023 11:10:58 +0530 Subject: [PATCH 094/112] chore(release): 12.3.2-0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ce0ab75..7f954cf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/ace", - "version": "12.3.1", + "version": "12.3.2-0", "description": "A CLI framework for Node.js", "main": "build/index.js", "type": "module", From 703d8dd868584f740150cedae63bf31b0f686463 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 23 Nov 2023 09:35:43 +0530 Subject: [PATCH 095/112] chore: update dependencies --- package.json | 42 +++++++++++++++++++++--------------------- src/parser.ts | 4 ++-- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/package.json b/package.json index 7f954cf..b8cfb40 100644 --- a/package.json +++ b/package.json @@ -42,44 +42,44 @@ "author": "virk,adonisjs", "license": "MIT", "dependencies": { - "@poppinss/cliui": "^6.2.0", - "@poppinss/hooks": "^7.2.0", + "@poppinss/cliui": "^6.2.1", + "@poppinss/hooks": "^7.2.1", "@poppinss/macroable": "^1.0.0", - "@poppinss/prompts": "^3.1.0", - "@poppinss/utils": "^6.5.0", + "@poppinss/prompts": "^3.1.1", + "@poppinss/utils": "^6.5.1", "fastest-levenshtein": "^1.0.16", "jsonschema": "^1.4.1", - "string-width": "^6.1.0", + "string-width": "^7.0.0", "yargs-parser": "^21.1.1", - "youch": "^3.3.2", + "youch": "^3.3.3", "youch-terminal": "^2.2.3" }, "devDependencies": { - "@adonisjs/eslint-config": "^1.1.8", - "@adonisjs/prettier-config": "^1.1.8", - "@adonisjs/tsconfig": "^1.1.8", - "@commitlint/cli": "^17.8.0", - "@commitlint/config-conventional": "^17.8.0", - "@japa/assert": "^2.0.0", + "@adonisjs/eslint-config": "^1.1.9", + "@adonisjs/prettier-config": "^1.1.9", + "@adonisjs/tsconfig": "^1.1.9", + "@commitlint/cli": "^18.4.3", + "@commitlint/config-conventional": "^18.4.3", + "@japa/assert": "^2.0.1", "@japa/expect-type": "^2.0.0", - "@japa/file-system": "^2.0.0", - "@japa/runner": "^3.0.2", - "@swc/core": "1.3.82", - "@types/node": "^20.8.6", - "@types/yargs-parser": "^21.0.1", + "@japa/file-system": "^2.0.1", + "@japa/runner": "^3.1.0", + "@swc/core": "^1.3.99", + "@types/node": "^20.9.4", + "@types/yargs-parser": "^21.0.3", "c8": "^8.0.1", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", "del-cli": "^5.1.0", - "eslint": "^8.51.0", + "eslint": "^8.54.0", "github-label-sync": "^2.3.1", "husky": "^8.0.3", "np": "^8.0.4", - "prettier": "^3.0.3", + "prettier": "^3.1.0", "ts-json-schema-generator": "^1.4.0", "ts-node": "^10.9.1", - "tsup": "^7.2.0", - "typescript": "^5.2.2" + "tsup": "^8.0.1", + "typescript": "5.2.2" }, "repository": { "type": "git", diff --git a/src/parser.ts b/src/parser.ts index d3c2c8e..34a930a 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -79,8 +79,8 @@ export class Parser { value = Array.isArray(option.default) ? option.default : option.default === undefined - ? undefined - : [option.default] + ? undefined + : [option.default] } /** From 65655647a0967720f1aeba201a578b973fedabd4 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 23 Nov 2023 09:41:21 +0530 Subject: [PATCH 096/112] chore: publish source maps and use tsc for generating types --- package.json | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index b8cfb40..4e31c68 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,10 @@ "main": "build/index.js", "type": "module", "files": [ - "build" + "build", + "!build/bin", + "!build/examples", + "!build/tests" ], "exports": { ".": "./build/index.js", @@ -19,10 +22,10 @@ "test": "cross-env NODE_DEBUG=adonisjs:ace c8 npm run quick:test", "clean": "del-cli build", "build:schema": "ts-json-schema-generator --path='src/types.ts' --type='CommandMetaData' --tsconfig='tsconfig.json' --out='schemas/command_metadata_schema.json'", - "copy:files": "copyfiles --up=1 schemas/* build && copyfiles --up=1 stubs/*.stub build", + "copy:files": "copyfiles --up=1 schemas/*.stub build && copyfiles --up=1 stubs/*.stub build", "precompile": "npm run lint && npm run clean", "typecheck": "tsc --noEmit", - "compile": "tsup-node", + "compile": "tsup-node && tsc --emitDeclarationOnly --declaration", "postcompile": "npm run build:schema && npm run copy:files", "build": "npm run compile", "release": "np", @@ -126,7 +129,8 @@ "outDir": "./build", "clean": true, "format": "esm", - "dts": true, + "dts": false, + "sourcemap": true, "target": "esnext" } } From a12359aed31398dfe78f2745358906d2d652c7d5 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 23 Nov 2023 10:02:43 +0530 Subject: [PATCH 097/112] chore(release): 12.3.2-1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4e31c68..00d8def 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/ace", - "version": "12.3.2-0", + "version": "12.3.2-1", "description": "A CLI framework for Node.js", "main": "build/index.js", "type": "module", From 8f734cdf038b681b1de69a6fce8fdc1e9cee7108 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 23 Nov 2023 10:35:39 +0530 Subject: [PATCH 098/112] fix: build process to copy schema.json file --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 00d8def..429ff8d 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "test": "cross-env NODE_DEBUG=adonisjs:ace c8 npm run quick:test", "clean": "del-cli build", "build:schema": "ts-json-schema-generator --path='src/types.ts' --type='CommandMetaData' --tsconfig='tsconfig.json' --out='schemas/command_metadata_schema.json'", - "copy:files": "copyfiles --up=1 schemas/*.stub build && copyfiles --up=1 stubs/*.stub build", + "copy:files": "copyfiles --up=1 schemas/*.stub schemas/*.json build && copyfiles --up=1 stubs/*.stub build", "precompile": "npm run lint && npm run clean", "typecheck": "tsc --noEmit", "compile": "tsup-node && tsc --emitDeclarationOnly --declaration", From de6a7cbb13a68d591ad4f48c029fd41b25779d25 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 23 Nov 2023 10:37:38 +0530 Subject: [PATCH 099/112] chore(release): 12.3.2-2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 429ff8d..96ca290 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/ace", - "version": "12.3.2-1", + "version": "12.3.2-2", "description": "A CLI framework for Node.js", "main": "build/index.js", "type": "module", From 6d91b18854bc38d98d994aac581e4f7a604a05e2 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 19 Dec 2023 11:52:46 +0530 Subject: [PATCH 100/112] chore: update dependencies --- package.json | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index 96ca290..04ece0e 100644 --- a/package.json +++ b/package.json @@ -45,11 +45,11 @@ "author": "virk,adonisjs", "license": "MIT", "dependencies": { - "@poppinss/cliui": "^6.2.1", - "@poppinss/hooks": "^7.2.1", - "@poppinss/macroable": "^1.0.0", - "@poppinss/prompts": "^3.1.1", - "@poppinss/utils": "^6.5.1", + "@poppinss/cliui": "^6.2.3", + "@poppinss/hooks": "^7.2.2", + "@poppinss/macroable": "^1.0.1", + "@poppinss/prompts": "^3.1.2", + "@poppinss/utils": "^6.7.0", "fastest-levenshtein": "^1.0.16", "jsonschema": "^1.4.1", "string-width": "^7.0.0", @@ -58,31 +58,31 @@ "youch-terminal": "^2.2.3" }, "devDependencies": { - "@adonisjs/eslint-config": "^1.1.9", - "@adonisjs/prettier-config": "^1.1.9", - "@adonisjs/tsconfig": "^1.1.9", + "@adonisjs/eslint-config": "^1.2.0", + "@adonisjs/prettier-config": "^1.2.0", + "@adonisjs/tsconfig": "^1.2.0", "@commitlint/cli": "^18.4.3", "@commitlint/config-conventional": "^18.4.3", - "@japa/assert": "^2.0.1", - "@japa/expect-type": "^2.0.0", - "@japa/file-system": "^2.0.1", - "@japa/runner": "^3.1.0", - "@swc/core": "^1.3.99", - "@types/node": "^20.9.4", + "@japa/assert": "^2.1.0", + "@japa/expect-type": "^2.0.1", + "@japa/file-system": "^2.1.0", + "@japa/runner": "^3.1.1", + "@swc/core": "^1.3.101", + "@types/node": "^20.10.5", "@types/yargs-parser": "^21.0.3", "c8": "^8.0.1", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", "del-cli": "^5.1.0", - "eslint": "^8.54.0", + "eslint": "^8.56.0", "github-label-sync": "^2.3.1", "husky": "^8.0.3", - "np": "^8.0.4", - "prettier": "^3.1.0", - "ts-json-schema-generator": "^1.4.0", - "ts-node": "^10.9.1", + "np": "^9.2.0", + "prettier": "^3.1.1", + "ts-json-schema-generator": "^1.5.0", + "ts-node": "^10.9.2", "tsup": "^8.0.1", - "typescript": "5.2.2" + "typescript": "^5.3.3" }, "repository": { "type": "git", From 7888c7e9209fc4455655ed3ffdef5124541cfcde Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 19 Dec 2023 12:00:24 +0530 Subject: [PATCH 101/112] test: add test for parsing big numbers Closes: #154 --- tests/parser.spec.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/parser.spec.ts b/tests/parser.spec.ts index 9451520..d77f6d7 100644 --- a/tests/parser.spec.ts +++ b/tests/parser.spec.ts @@ -282,6 +282,28 @@ test.group('Parser | flags', () => { }, }) }) + + test('parse big numbers', ({ assert }) => { + class MakeModel extends BaseCommand {} + MakeModel.defineFlag('connection', { type: 'string' }) + MakeModel.defineFlag('batchSize', { type: 'number' }) + + assert.deepEqual( + new Parser(MakeModel.getParserOptions()).parse( + '--connection=111111111111111111111111 --batch-size=111111111111111111111111' + ), + { + _: [], + nodeArgs: [], + args: [], + unknownFlags: [], + flags: { + 'batch-size': 1.1111111111111111e23, // converted to number + 'connection': '111111111111111111111111', // reatains string value + }, + } + ) + }) }) test.group('Parser | arguments', () => { From 6970243773af497f81e95ba50e7f1343e32568b2 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 19 Dec 2023 12:01:28 +0530 Subject: [PATCH 102/112] style: fix typo --- tests/parser.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/parser.spec.ts b/tests/parser.spec.ts index d77f6d7..3fae5f7 100644 --- a/tests/parser.spec.ts +++ b/tests/parser.spec.ts @@ -299,7 +299,7 @@ test.group('Parser | flags', () => { unknownFlags: [], flags: { 'batch-size': 1.1111111111111111e23, // converted to number - 'connection': '111111111111111111111111', // reatains string value + 'connection': '111111111111111111111111', // retains string value }, } ) From a6c5095f5d07e292258391fa6f4faa30f445c2b7 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 19 Dec 2023 12:08:05 +0530 Subject: [PATCH 103/112] chore(release): 12.3.2-3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 04ece0e..f6263da 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/ace", - "version": "12.3.2-2", + "version": "12.3.2-3", "description": "A CLI framework for Node.js", "main": "build/index.js", "type": "module", From b73a8caef04780a9ab0bf59ee4862e375e790cc5 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 19 Dec 2023 13:02:42 +0530 Subject: [PATCH 104/112] fix: handle prompt cancellation error gracefully --- src/exception_handler.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/exception_handler.ts b/src/exception_handler.ts index c9ccaa2..ad082f8 100644 --- a/src/exception_handler.ts +++ b/src/exception_handler.ts @@ -7,6 +7,7 @@ * file that was distributed with this source code. */ +import { errors as promptsErrors } from '@poppinss/prompts' import { errors, Kernel } from '../index.js' import { renderErrorWithSuggestions } from './helpers.js' @@ -75,6 +76,14 @@ export class ExceptionHandler { return } + /** + * Display prompt cancellation error + */ + if (error instanceof promptsErrors.E_PROMPT_CANCELLED) { + this.logError({ message: 'Process exited during prompt cancellation' }, kernel) + return + } + /** * Known errors should always be reported with a message */ From 2855b6aa16a389b96298c1cecd7e7382aee84b7d Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 19 Dec 2023 13:08:57 +0530 Subject: [PATCH 105/112] chore(release): 12.3.2-4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f6263da..18c0c5e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/ace", - "version": "12.3.2-3", + "version": "12.3.2-4", "description": "A CLI framework for Node.js", "main": "build/index.js", "type": "module", From 9c6676d35b0281b30643f15b17859eea52fde9b1 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 22 Dec 2023 15:14:18 +0530 Subject: [PATCH 106/112] refactor: assign 404 status code to E_COMMAND_NOT_FOUND --- src/errors.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/errors.ts b/src/errors.ts index f7e34d9..ca840df 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -24,6 +24,7 @@ export const E_MISSING_COMMAND_NAME = createError<[command: string]>( * Cannot find a command for the given name */ export const E_COMMAND_NOT_FOUND = class CommandNotFound extends Exception { + static status: number = 404 commandName: string constructor(args: [command: string]) { super(`Command "${args[0]}" is not defined`, { code: 'E_COMMAND_NOT_FOUND' }) From 74e548c151cc73645a906463924af8e1998ded08 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 22 Dec 2023 15:28:29 +0530 Subject: [PATCH 107/112] refactor: prompt cancellation error message --- src/exception_handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/exception_handler.ts b/src/exception_handler.ts index ad082f8..3589cf3 100644 --- a/src/exception_handler.ts +++ b/src/exception_handler.ts @@ -80,7 +80,7 @@ export class ExceptionHandler { * Display prompt cancellation error */ if (error instanceof promptsErrors.E_PROMPT_CANCELLED) { - this.logError({ message: 'Process exited during prompt cancellation' }, kernel) + this.logError({ message: 'Prompt cancelled' }, kernel) return } From b6738691a5a901f84129306868cba0d992ba9b6d Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 22 Dec 2023 15:30:52 +0530 Subject: [PATCH 108/112] chore(release): 12.3.2-5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 18c0c5e..c06c5ef 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/ace", - "version": "12.3.2-4", + "version": "12.3.2-5", "description": "A CLI framework for Node.js", "main": "build/index.js", "type": "module", From 8ff01ddceb991a362438079692ac922bacd1cc2e Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 27 Dec 2023 09:29:15 +0530 Subject: [PATCH 109/112] chore: update dependencies --- package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index c06c5ef..49eadb8 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "author": "virk,adonisjs", "license": "MIT", "dependencies": { - "@poppinss/cliui": "^6.2.3", + "@poppinss/cliui": "^6.3.0", "@poppinss/hooks": "^7.2.2", "@poppinss/macroable": "^1.0.1", "@poppinss/prompts": "^3.1.2", @@ -58,14 +58,14 @@ "youch-terminal": "^2.2.3" }, "devDependencies": { - "@adonisjs/eslint-config": "^1.2.0", - "@adonisjs/prettier-config": "^1.2.0", - "@adonisjs/tsconfig": "^1.2.0", + "@adonisjs/eslint-config": "^1.2.1", + "@adonisjs/prettier-config": "^1.2.1", + "@adonisjs/tsconfig": "^1.2.1", "@commitlint/cli": "^18.4.3", "@commitlint/config-conventional": "^18.4.3", "@japa/assert": "^2.1.0", "@japa/expect-type": "^2.0.1", - "@japa/file-system": "^2.1.0", + "@japa/file-system": "^2.1.1", "@japa/runner": "^3.1.1", "@swc/core": "^1.3.101", "@types/node": "^20.10.5", From 04f6652c2eb46c946f325d9b56aad039433cec49 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 27 Dec 2023 09:35:25 +0530 Subject: [PATCH 110/112] chore(release): 12.3.2-6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 49eadb8..accca61 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/ace", - "version": "12.3.2-5", + "version": "12.3.2-6", "description": "A CLI framework for Node.js", "main": "build/index.js", "type": "module", From f68b2385b067e77ab008e173ae643cd98ee8302d Mon Sep 17 00:00:00 2001 From: Aliaksei Date: Sun, 31 Dec 2023 05:10:56 +0000 Subject: [PATCH 111/112] fix: incorporate missing spaces in 'list command' help text (#157) --- src/commands/list.ts | 4 ++-- tests/commands/list.spec.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/commands/list.ts b/src/commands/list.ts index 3f177b7..4ddf8de 100644 --- a/src/commands/list.ts +++ b/src/commands/list.ts @@ -27,10 +27,10 @@ export class ListCommand extends BaseCommand { static description: string = 'View list of available commands' static help = [ 'The list command displays a list of all the commands:', - ' {{ binaryName }}list', + ' {{ binaryName }} list', '', 'You can also display the commands for a specific namespace:', - ' {{ binaryName }}list ', + ' {{ binaryName }} list ', ] /** diff --git a/tests/commands/list.spec.ts b/tests/commands/list.spec.ts index 1c1d2cd..6c57c8f 100644 --- a/tests/commands/list.spec.ts +++ b/tests/commands/list.spec.ts @@ -251,10 +251,10 @@ test.group('List command', () => { description: 'View list of available commands', help: [ 'The list command displays a list of all the commands:', - ' {{ binaryName }}list', + ' {{ binaryName }} list', '', 'You can also display the commands for a specific namespace:', - ' {{ binaryName }}list ', + ' {{ binaryName }} list ', ], namespace: null, aliases: [], From 71372087d0af2762e877cd7d78869a3b7ab9a04a Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Sun, 7 Jan 2024 19:00:21 +0530 Subject: [PATCH 112/112] chore: update dependencies --- package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index accca61..4372bc3 100644 --- a/package.json +++ b/package.json @@ -61,16 +61,16 @@ "@adonisjs/eslint-config": "^1.2.1", "@adonisjs/prettier-config": "^1.2.1", "@adonisjs/tsconfig": "^1.2.1", - "@commitlint/cli": "^18.4.3", - "@commitlint/config-conventional": "^18.4.3", + "@commitlint/cli": "^18.4.4", + "@commitlint/config-conventional": "^18.4.4", "@japa/assert": "^2.1.0", "@japa/expect-type": "^2.0.1", "@japa/file-system": "^2.1.1", "@japa/runner": "^3.1.1", - "@swc/core": "^1.3.101", - "@types/node": "^20.10.5", + "@swc/core": "^1.3.102", + "@types/node": "^20.10.6", "@types/yargs-parser": "^21.0.3", - "c8": "^8.0.1", + "c8": "^9.0.0", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", "del-cli": "^5.1.0",