From 2d90f72a9ecd7a4e63493308ec0b46043b955671 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lenon?= Date: Mon, 16 Oct 2023 12:11:44 +0100 Subject: [PATCH] feat(docs): add missing API's examples --- .../application-lifecycle.mdx | 4 +- docs/cli-application/commands.mdx | 25 ++++- docs/cli-application/error-handling.mdx | 2 +- docs/cli-application/publishing.mdx | 2 +- docs/cli-application/running.mdx | 20 ++-- docs/digging-deeper/repl.mdx | 99 ++++++++++++++++--- docs/testing/cli-tests.mdx | 72 +++++++++++++- docs/testing/mocking.mdx | 12 ++- docs/testing/rest-api-testing.mdx | 2 +- docs/the-basics/helpers.mdx | 88 +++++++++++++++-- 10 files changed, 281 insertions(+), 45 deletions(-) diff --git a/docs/architecture-concepts/application-lifecycle.mdx b/docs/architecture-concepts/application-lifecycle.mdx index 791c0405..7cbbc245 100644 --- a/docs/architecture-concepts/application-lifecycle.mdx +++ b/docs/architecture-concepts/application-lifecycle.mdx @@ -256,7 +256,7 @@ The Kernel is also responsible for registering your commands defined in your `.athennarc.json` file. By default, Athenna will always use the default implementation `ConsoleKernel` class imported from `@athenna/http` package. If you prefer, you can create your custom Kernel implementation, extending the -default ConsoleKernel class and registering it in your `Ignite.artisan` method +default ConsoleKernel class and registering it in your `Ignite.console()` method call: ```typescript @@ -283,7 +283,7 @@ const ignite = await new Ignite().load(import.meta.url, { bootLogs: false }) -await ignite.artisan({ kernelPath: '#app/http/CustomKernel' }) +await ignite.console({ kernelPath: '#app/http/CustomKernel' }) ``` ### Execution diff --git a/docs/cli-application/commands.mdx b/docs/cli-application/commands.mdx index f2870251..100ce241 100644 --- a/docs/cli-application/commands.mdx +++ b/docs/cli-application/commands.mdx @@ -938,7 +938,7 @@ sticker.render() Create a task runner that will log the status of each task. The task -method is very useful when you need to +method is beneficial when you need to do a lot of tasks in order, giving a status to all the tasks and how much time it has taken to execute: @@ -961,6 +961,27 @@ task.add('Second task', async task => { await task.run() ``` +You can also use the `task.addPromise()` method to don't +mind about calling `task.complete()` or `task.fail()` method. +This method will automatically call it for you if the promise +resolves or rejects: + +```typescript +import { Exec } from '@athenna/common' + +const task = this.logger.task() + +task.addPromise('First task', async () => { + await Exec.sleep(1000) +}) + +task.addPromise('Second task', async () => { + await Exec.sleep(1000) +}) + +await task.run() +``` + ## Generating templates in commands Artisan has support to generate files from templates. @@ -1052,7 +1073,7 @@ that is exactly what we do internally with other #### `this.generator.path()` Set the file path where the file will be generated. -Rememeber that the file name in the path will be used +Remember that the file name in the path will be used to define the name properties: ```typescript diff --git a/docs/cli-application/error-handling.mdx b/docs/cli-application/error-handling.mdx index dd8ce8f5..d9489d57 100644 --- a/docs/cli-application/error-handling.mdx +++ b/docs/cli-application/error-handling.mdx @@ -157,7 +157,7 @@ Now you need to register your exception handler when bootstrapping your application: ```typescript title="Path.bootstrap('main.ts')" -await ignite.artisan(process.argv, { +await ignite.console(process.argv, { displayName: 'Athenna', exceptionHandlerPath: '#app/console/exceptions/Handler', 👈 }) diff --git a/docs/cli-application/publishing.mdx b/docs/cli-application/publishing.mdx index 8c54db95..29871d73 100644 --- a/docs/cli-application/publishing.mdx +++ b/docs/cli-application/publishing.mdx @@ -17,7 +17,7 @@ you can publish a global CLI application in NPM registry. To publish your package on the NPM registry, you need to have an account at NPM. If you don't have an account, -visit the [NPM sign up page](https://www.npmjs.com/signup) +visit the [NPM sign-up page](https://www.npmjs.com/signup) to create one. After creating the account, open your terminal and run the diff --git a/docs/cli-application/running.mdx b/docs/cli-application/running.mdx index 1784300b..d766f98d 100644 --- a/docs/cli-application/running.mdx +++ b/docs/cli-application/running.mdx @@ -20,7 +20,7 @@ default in your application, we are going to focus in ## Registering your CLI command name -To register your CLI command name you need to add the +To register your CLI command name, you need to add the `bin` object inside your `package.json` file and set the path to the entry point file of your CLI: @@ -34,17 +34,15 @@ path to the entry point file of your CLI: In our example we defined the `./bootstrap/main.js` file as the entrypoint file of our CLI. By default, this -file comes with the shebang line -`#!/usr/bin/env -S node --experimental-import-meta-resolve` +file comes with the shebang line `#!/usr/bin/env node` in the top of the file. Without this line the `npm link` command will not work. So just in case you want to define a -different entrypoint file, remember that -`#!/usr/bin/env -S node --experimental-import-meta-resolve` +different entrypoint file, remember that `#!/usr/bin/env node` should be on the top of this file. ## Linking the CLI -Now you just need to run the following command in your project root: +Now you need to run the following command in your project root: ```shell npm link @@ -63,23 +61,23 @@ name will be rendered in the terminal using [chalk-rainbow](https://www.npmjs.com/package/chalk-rainbow) and [figlet](https://www.npmjs.com/package/figlet). -By default Artisan always display the `Artisan` name, but you can +By default, Artisan always display the `Artisan` name, but you can change it for your own display name by setting the `displayName` -property in `Ignite.artisan()` method: +property in `Ignite.console()` method: ```typescript title="Path.bootstrap('main.ts')" import { Ignite } from '@athenna/core' const ignite = await new Ignite().load(import.meta.url) -await ignite.artisan({ +await ignite.console({ displayName: 'Your CLI Command', 👈 }) ``` :::tip -If you wish to disable the display name, just set the `displayName` +If you wish to disable the display name, set the `displayName` as `null`: ```typescript title="Path.bootstrap('main.ts')" @@ -87,7 +85,7 @@ import { Ignite } from '@athenna/core' const ignite = await new Ignite().load(import.meta.url) -await ignite.artisan({ +await ignite.console({ displayName: null, 👈 }) ``` diff --git a/docs/digging-deeper/repl.mdx b/docs/digging-deeper/repl.mdx index 28e3f281..3822883e 100644 --- a/docs/digging-deeper/repl.mdx +++ b/docs/digging-deeper/repl.mdx @@ -65,11 +65,62 @@ commands available. ::: +## Extending REPL Context + +When running your REPL session, it could be painful to +keep declaring the same variables over and over again. To +avoid that you can use the `repl.setIntContext()` method, +and it will be available in all your REPL sessions: + +```typescript title="Path.bootstrap('repl.ts')" +import { Ignite } from '@athenna/core' + +const ignite = await new Ignite().load(import.meta.url, { bootLogs: false }) + +const repl = await ignite.repl() + +repl.setInContext('helloWorld', 'Hello World!') +repl.setInContext('appService', ioc.safeUse('App/Services/AppService')) +``` + +## Custom REPL commands + +You can create custom REPL commands like `.ls` and `.clean` +to make your life easier when running your REPL session: + +```typescript title="Path.bootstrap('repl.ts')" +import { Ignite } from '@athenna/core' + +const ignite = await new Ignite().load(import.meta.url, { bootLogs: false }) + +const repl = await ignite.repl() + +repl + .command('greet') + .help('Greet someone') + .action(function action(name: string) { + this.clearBufferedCommand() + console.log(`Hello ${name}!`) + this.displayPrompt() + }) + .register() +``` + +Now the `.greet` command will always be available in your +REPL sessions: + +```shell +.greet Antoine +> Hello Antoine! +``` + ## Pre-importing modules -You can pre-import modules in your REPL session to avoid keep -importing them everytime inside your REPL session. You can do that -in your `Path.bootstrap('repl.ts')` file: +### Pre-importing module + +Like declaring variables in the REPL context, it could be painful +to import modules in your everytime in your REPL sessions. To avoid +it you can use the `repl.import()` method: ```typescript import { Ignite } from '@athenna/core' @@ -78,19 +129,43 @@ const ignite = await new Ignite().load(import.meta.url, { bootLogs: false }) const repl = await ignite.repl() -await import('@athenna/common').then(common => { - Object.keys(common).forEach(key => ( - repl.context[key] = common[key] 👈 - )) -}) +await repl.import('common', '@athenna/common') +``` + +Now you are able to use the `common` variable in your REPL sessions: -repl.context.helloWorld = 'Hello World!' 👈 -repl.context.appService = ioc.safeUse('App/Services/AppService') 👈 +```typescript +common.Path.pwd() +``` + +### Pre-importing modules names as keys + +Sometimes a module exports a variety of classes, functions, etc. +Like `@athenna/common`, that exports a variety of helpers, and would +be easier if we could directly use the helper name instead +of the `common` variable as we have created in the example above. +With `repl.importAll()` you can import the module names as keys to +the REPL context: + +```typescript +import { Ignite } from '@athenna/core' + +const ignite = await new Ignite().load(import.meta.url, { bootLogs: false }) + +const repl = await ignite.repl() + +await repl.importAll('@athenna/common') +``` + +Now you are able to use the `Path` helper directly in your REPL sessions: + +```typescript +Path.pwd() ``` :::info -As you can see in the implementation above, by default Athenna -pre-imports all the `@athenna/common` helpers to your REPL session. +By default, your `Path.bootstrap('repl.ts')` file already +imports all the `@athenna/common` helpers to your REPL session 🤩. ::: diff --git a/docs/testing/cli-tests.mdx b/docs/testing/cli-tests.mdx index f7c2166f..777cab78 100644 --- a/docs/testing/cli-tests.mdx +++ b/docs/testing/cli-tests.mdx @@ -39,11 +39,11 @@ assertions for inspecting the output. The `command` property in your test context will only be available if you register the command plugin within the -`Runner` class. By default your Athenna application already +`Runner` class. By default, your Athenna application already comes with the command plugin registered. But we are going to cover how to register it manually if needed. -Just call the `Runner.addPlugin()` static method to setup +Just call the `Runner.addPlugin()` static method to set up the request plugin imported from `@athenna/artisan/testing/plugins`: ```typescript title="Path.bootstrap('test.ts')" @@ -88,9 +88,11 @@ export default class ExampleTest { ## Changing Artisan file path +### Changing the default Artisan file path + As mentioned previously, the `command.run()` method invokes a child process using the `Path.bootstrap('artisan.ts')` file. -But for some reason you may want to change which file should be +But for some reason, you may want to change which file should be used to test your commands. To do so, you can call the `TestCommand.setArtisanPath()` static method before running your tests: @@ -114,6 +116,62 @@ await Runner.setTsEnv() .run() ``` +### Changing Artisan file path per command + +When running your tests, you may want to create a different behavior +for a specific command, like mocking the prompts to not block your test +execution or adding some different value for an `Env` or `Config`. + +Since the `command.run()` method invokes a child process, you can't do +this kind of customization in your tests: + +```typescript +import { Config } from '@athenna/config' +import { Test, type Context } from '@athenna/test' + +export default class ExampleTest { + @Test() + public async testConfigCommand({ command }: Context) { + Config.set('app.name', 'MyAppName') + + const output = await command.run('greet') + + output.assertLogged('Hello from MyAppName!') // ❌ + } +``` + +To solve this problem, you can use a different `artisan` file +for each `command.run()` call. Let's first create a new `artisan` +file and save it in our `fixtures` path: + +```typescript title="Path.fixtures('consoles/artisan-set-app-name.ts')" +import { Ignite } from '@athenna/core' +import { Config } from '@athenna/config' + +const ignite = await new Ignite().load(import.meta.url, { bootLogs: false }) + +Config.set('app.name', 'MyAppName') + +await ignite.console(process.argv, { displayName: 'Artisan' }) +``` + +Now, we can use this new `artisan` file to run our command: + +```typescript +import { Path } from '@athenna/common' +import { Test, type Context } from '@athenna/test' + +export default class ExampleTest { + @Test() + public async testConfigCommand({ command }: Context) { + const output = await command.run('greet', { + path: Path.fixtures('consoles/artisan-set-app-name.ts') 👈 + }) + + output.assertLogged('Hello from MyAppName!') // ✅ + } +``` + ## Debugging outputs After executing a test command to your application, @@ -126,7 +184,7 @@ import { Test, type Context } from '@athenna/test' export default class ExampleTest { @Test() public async testBasicCommand({ command }: Context) { - const output = await command.run('/') + const output = await command.run('basic') console.log(output.output.stdout) console.log(output.output.stderr) @@ -189,6 +247,7 @@ Assert the command has logged the expected message: ```typescript output.assertLogged('Hello World!') +output.assertNotLogged('Hello World!') ``` This method validates that the log message will be @@ -198,6 +257,7 @@ argument: ```typescript output.assertLogged('Hello World!', 'stdout') // or stderr +output.assertNotLogged('Hello World!', 'stdout') // or stderr ``` #### `assertLogMatches()` @@ -208,13 +268,15 @@ provided: ```typescript output.assertLogMatches(/Hello World/) +output.assertLogNotMatches(/Hello World/) ``` This method validates that the regex will match in `stdout` or `stderr`. To force the stream type -where this log should match you can set it as second +where this log should match, you can set it as second argument: ```typescript output.assertLogMatches(/Hello World/, 'stdout') // or stderr +output.assertLogNotMatches(/Hello World/, 'stdout') // or stderr ``` diff --git a/docs/testing/mocking.mdx b/docs/testing/mocking.mdx index 5f164a14..c40e67d3 100644 --- a/docs/testing/mocking.mdx +++ b/docs/testing/mocking.mdx @@ -33,10 +33,10 @@ and then mocking the method you want in each test case: ```typescript import { AppService } from '#app/services/AppService' -import { BaseRestTest } from '@athenna/core/testing/BaseRestTest' +import { BaseHttpTest } from '@athenna/core/testing/BaseHttpTest' import { Test, type Context, BeforeAll, Mock, AfterEach } from '@athenna/test' -export default class AppControllerTest extends BaseRestTest { +export default class AppControllerTest extends BaseHttpTest { public appService = new AppService() @BeforeAll() @@ -70,9 +70,9 @@ a mocked version of your service using the `ioc.fake()` method ```typescript import { Test, BeforeAll, type Context } from '@athenna/test' import { AppServiceMock } from '#tests/fixtures/AppServiceMock' -import { BaseRestTest } from '@athenna/core/testing/BaseRestTest' +import { BaseHttpTest } from '@athenna/core/testing/BaseHttpTest' -export default class AppControllerTest extends BaseRestTest { +export default class AppControllerTest extends BaseHttpTest { @BeforeAll() public beforeAll() { ioc @@ -97,3 +97,7 @@ Coming soon... ## Mocking facades Coming soon... + +## Assertions in mocks + +Coming soon... diff --git a/docs/testing/rest-api-testing.mdx b/docs/testing/rest-api-testing.mdx index f6d680f1..c8d83a62 100644 --- a/docs/testing/rest-api-testing.mdx +++ b/docs/testing/rest-api-testing.mdx @@ -38,7 +38,7 @@ JSON structure, and more. The `request` property in your test context will only be available if you register the request plugin within the -`Runner` class. By default your Athenna application already +`Runner` class. By default, your Athenna application already comes with the request plugin registered. But we are going to cover how to register it manually if needed. diff --git a/docs/the-basics/helpers.mdx b/docs/the-basics/helpers.mdx index 6a08799f..c9c0be99 100644 --- a/docs/the-basics/helpers.mdx +++ b/docs/the-basics/helpers.mdx @@ -295,18 +295,56 @@ Execute some command of your OS in a child process: ```typescript import { Exec } from '@athenna/common' -const { stdout } = await Exec.command('ls -la') +const { stdout, stderr } = await Exec.command('ls -la') ``` -If your command fails, Athenna will throw the -`NodeCommandException`, to avoid this you can set the -`ignoreErrors` option: +You can add as second parameter an object with some options: ```typescript +import { Exec, type CommandInput } from '@athenna/common' + +const options: CommandInput = {} + +const { stdout, stderr } = await Exec.command('ls -la', options) +``` + +#### `Exec::shell()` + +Same as `Exec::command()` method, but works only for shell consoles: + +```typescript +import { Exec } from '@athenna/common' + +const { stdout, stderr } = await Exec.shell('ls -la') +``` + +#### `Exec::node()` + +Execute some Node.js script in a child process: + +```typescript +#!/usr/bin/env node + +import { Exec } from '@athenna/common' + +Exec.artisan('./index.ts', { + nodeOptions: ['--import=@athenna/tsconfig'] +}) +``` + +#### `Exec::artisan()` + +This method was created specially to execute `artisan` scripts +in child processes. Under the hood it calls `Exec::node()` method +but with predefined options: + +```typescript +#!/usr/bin/env node + import { Exec } from '@athenna/common' -const { stdout, stderr } = await Exec.command('ls -la', { - ignoreErrors: true +Exec.artisan('./bootstrap/artisan.js', { + nodeOptions: ['--enable-source-maps', '--import=@athenna/tsconfig'] }) ``` @@ -3555,6 +3593,44 @@ process.env.IS_TS = 'false' console.log(Path.ext()) // js ``` +#### `Path::parseExt()` + +Parse a path extension relying on the `IS_TS` environment variable: + +```typescript +const tsPath = 'app/services/MyService.ts' +const jsPath = 'app/services/MyService.js' + +process.env.IS_TS = 'true' +console.log(Path.parseExt(tsPath)) // app/services/MyService.ts +console.log(Path.parseExt(jsPath)) // app/services/MyService.ts + +process.env.IS_TS = 'false' +console.log(Path.parseExt(tsPath)) // app/services/MyService.js +console.log(Path.parseExt(jsPath)) // app/services/MyService.js +``` + +#### `Path::toURL()` + +Parse a path to a URL instance: + +```typescript +import type { URL } from 'node:url' +import { Path } from '@athenna/common' + +const url: URL = Path.toURL('https://athenna.io') +``` + +#### `Path::toHref()` + +Parse a path to a URL href string: + +```typescript +import { Path } from '@athenna/common' + +const href = Path.toHref('https://athenna.io') +``` + #### `Path::pwd()` Return the root path where the project is running: