diff --git a/commands/build.ts b/commands/build.ts index 6cd38faa..22785e45 100644 --- a/commands/build.ts +++ b/commands/build.ts @@ -45,7 +45,7 @@ export const handler: Handler = async function handler(argv, { getWorkspac for (const workspace of workspaces) { if (workspace.private) { - buildableStep.warn(`Not building \`${workspace.name}\` because it is private`); + buildableStep.info(`Skipping "${workspace.name}" because it is private`); continue; } @@ -154,11 +154,11 @@ export const handler: Handler = async function handler(argv, { getWorkspac } } - await buildableStep.end(); + buildableStep.end(); const removeStep = logger.createStep('Clean previous build directories'); await Promise.all(removals.map((dir) => file.remove(dir, { step: removeStep }))); - await removeStep.end(); + removeStep.end(); await batch([...buildProcs, ...typesProcs]); await Promise.all(postCopy.map((fn) => fn())); diff --git a/docs/commands/typedoc.ts b/docs/commands/typedoc.ts index 196a1a08..41044d23 100644 --- a/docs/commands/typedoc.ts +++ b/docs/commands/typedoc.ts @@ -52,6 +52,7 @@ export const handler: Handler = async (argv, { graph, logger }) => { 'typedoc-plugin-markdown', '--useCodeBlocks', 'true', + '--excludeExternals', '--entryFileName', 'index.md', '--options', diff --git a/docs/src/content/docs/api/index.md b/docs/src/content/docs/api/index.md index 99030a7f..b3f10f43 100644 --- a/docs/src/content/docs/api/index.md +++ b/docs/src/content/docs/api/index.md @@ -4,7 +4,7 @@ description: Full API documentation for oneRepo. --- - + ## Variables @@ -16,6 +16,32 @@ const defaultConfig: Required; **Defined in:** [modules/onerepo/src/setup/setup.ts](https://github.com/paularmstrong/onerepo/blob/main/modules/onerepo/src/setup/setup.ts) +## Functions + +### restoreCursor() + +```ts +function restoreCursor(): void; +``` + +Gracefully restore the CLI cursor on exit. + +Prevent the cursor you have hidden interactively from remaining hidden if the process crashes. + +It does nothing if run in a non-TTY context. + +**Returns:** `void` + +#### Example + +``` +import restoreCursor from 'restore-cursor'; + +restoreCursor(); +``` + +**Defined in:** [modules/logger/src/index.ts](https://github.com/paularmstrong/onerepo/blob/main/modules/logger/src/index.ts) + ## Commands ### Argv\ @@ -2030,8 +2056,27 @@ If the current terminal is a TTY, output will be buffered and asynchronous steps See [\`HandlerExtra\`](#handlerextra) for access the the global Logger instance. +#### Properties + +| Property | Type | Defined in | +| -------- | -------- | --------------------------------------------------------------------------------------------------------------- | +| `id` | `string` | [modules/logger/src/Logger.ts](https://github.com/paularmstrong/onerepo/blob/main/modules/logger/src/Logger.ts) | + #### Accessors +##### captureAll + +###### Get Signature + +```ts +get captureAll(): boolean +``` + +**Experimental** +**Returns:** `boolean` + +**Defined in:** [modules/logger/src/Logger.ts](https://github.com/paularmstrong/onerepo/blob/main/modules/logger/src/Logger.ts) + ##### hasError ###### Get Signature @@ -2102,7 +2147,7 @@ Get the logger's verbosity level set verbosity(value): void ``` -Recursively applies the new verbosity to the logger and all of its active steps. +Applies the new verbosity to the main logger and any future steps. **Parameters:** @@ -2129,7 +2174,7 @@ get writable(): boolean ##### createStep() ```ts -createStep(name, __namedParameters?): LogStep +createStep(name, opts?): LogStep ``` Create a sub-step, [\`LogStep\`](#logstep), for the logger. This and any other step will be tracked and required to finish before exit. @@ -2142,11 +2187,13 @@ await step.end(); **Parameters:** -| Parameter | Type | Description | -| ---------------------------------- | --------------------------------- | ----------------------------------------------------------------------------- | -| `name` | `string` | The name to be written and wrapped around any output logged to this new step. | -| `__namedParameters`? | \{ `writePrefixes`: `boolean`; \} | - | -| `__namedParameters.writePrefixes`? | `boolean` | - | +| Parameter | Type | Description | +| --------------------- | ---------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `name` | `string` | The name to be written and wrapped around any output logged to this new step. | +| `opts`? | \{ `description`: `string`; `verbosity`: [`Verbosity`](#verbosity-1); `writePrefixes`: `boolean`; \} | - | +| `opts.description`? | `string` | Optionally include extra information for performance tracing on this step. This description will be passed through to the [`performanceMark.detail`](https://nodejs.org/docs/latest-v20.x/api/perf_hooks.html#performancemarkdetail) recorded internally for this step. Use a [Performance Writer plugin](https://onerepo.tools/plugins/performance-writer/) to read and work with this detail. | +| `opts.verbosity`? | [`Verbosity`](#verbosity-1) | Override the default logger verbosity. Any changes while this step is running to the default logger will result in this step’s verbosity changing as well. | +| `opts.writePrefixes`? | `boolean` | **Deprecated** This option no longer does anything and will be removed in v2.0.0 | **Returns:** [`LogStep`](#logstep) **Defined in:** [modules/logger/src/Logger.ts](https://github.com/paularmstrong/onerepo/blob/main/modules/logger/src/Logger.ts) @@ -2154,7 +2201,7 @@ await step.end(); ##### pause() ```ts -pause(write?): void +pause(): void ``` When the terminal is a TTY, steps are automatically animated with a progress indicator. There are times when it's necessary to stop this animation, like when needing to capture user input from `stdin`. Call the `pause()` method before requesting input and [\`logger.unpause()\`](#unpause) when complete. @@ -2167,12 +2214,6 @@ logger.pause(); logger.unpause(); ``` -**Parameters:** - -| Parameter | Type | -| --------- | --------- | -| `write`? | `boolean` | - **Returns:** `void` **Defined in:** [modules/logger/src/Logger.ts](https://github.com/paularmstrong/onerepo/blob/main/modules/logger/src/Logger.ts) @@ -2182,11 +2223,20 @@ logger.unpause(); unpause(): void ``` -Unpause the logger and resume writing buffered logs to `stderr`. See [\`logger.pause()\`](#pause) for more information. +Unpause the logger and uncork writing buffered logs to the output stream. See [\`logger.pause()\`](#pause) for more information. **Returns:** `void` **Defined in:** [modules/logger/src/Logger.ts](https://github.com/paularmstrong/onerepo/blob/main/modules/logger/src/Logger.ts) +##### waitForClear() + +```ts +waitForClear(): Promise +``` + +**Returns:** `Promise`\<`boolean`\> +**Defined in:** [modules/logger/src/Logger.ts](https://github.com/paularmstrong/onerepo/blob/main/modules/logger/src/Logger.ts) + #### Logging ##### debug() @@ -2204,14 +2254,14 @@ logger.debug('Log this content when verbosity is >= 4'); If a function with zero arguments is passed, the function will be executed before writing. This is helpful for avoiding extra work in the event that the verbosity is not actually high enough to render the logged debug information: ```ts -logger.debug(() => bigArray.map((item) => item.name)); +logger.debug(() => bigArray.map((item) => `- ${item.name}`).join('\n')); ``` **Parameters:** -| Parameter | Type | Description | -| ---------- | --------- | -------------------------------------------------------------------- | -| `contents` | `unknown` | Any value that can be converted to a string for writing to `stderr`. | +| Parameter | Type | Description | +| ---------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `contents` | `unknown` | Any value that can be converted to a string for writing to `stderr`. If a function is given with no arguments, the function will be executed and its response will be stringified for output. | **Returns:** `void` **See also:** @@ -2234,14 +2284,14 @@ logger.error('Log this content when verbosity is >= 1'); If a function with zero arguments is passed, the function will be executed before writing. This is helpful for avoiding extra work in the event that the verbosity is not actually high enough to render the logged error: ```ts -logger.error(() => bigArray.map((item) => item.name)); +logger.error(() => bigArray.map((item) => `- ${item.name}`).join('\n')); ``` **Parameters:** -| Parameter | Type | Description | -| ---------- | --------- | -------------------------------------------------------------------- | -| `contents` | `unknown` | Any value that can be converted to a string for writing to `stderr`. | +| Parameter | Type | Description | +| ---------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `contents` | `unknown` | Any value that can be converted to a string for writing to `stderr`. If a function is given with no arguments, the function will be executed and its response will be stringified for output. | **Returns:** `void` **See also:** @@ -2264,14 +2314,14 @@ logger.info('Log this content when verbosity is >= 1'); If a function with zero arguments is passed, the function will be executed before writing. This is helpful for avoiding extra work in the event that the verbosity is not actually high enough to render the logged information: ```ts -logger.info(() => bigArray.map((item) => item.name)); +logger.info(() => bigArray.map((item) => `- ${item.name}`).join('\n')); ``` **Parameters:** -| Parameter | Type | Description | -| ---------- | --------- | -------------------------------------------------------------------- | -| `contents` | `unknown` | Any value that can be converted to a string for writing to `stderr`. | +| Parameter | Type | Description | +| ---------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `contents` | `unknown` | Any value that can be converted to a string for writing to `stderr`. If a function is given with no arguments, the function will be executed and its response will be stringified for output. | **Returns:** `void` **See also:** @@ -2294,14 +2344,14 @@ logger.log('Log this content when verbosity is >= 3'); If a function with zero arguments is passed, the function will be executed before writing. This is helpful for avoiding extra work in the event that the verbosity is not actually high enough to render the logged information: ```ts -logger.log(() => bigArray.map((item) => item.name)); +logger.log(() => bigArray.map((item) => `- ${item.name}`).join('\n')); ``` **Parameters:** -| Parameter | Type | Description | -| ---------- | --------- | -------------------------------------------------------------------- | -| `contents` | `unknown` | Any value that can be converted to a string for writing to `stderr`. | +| Parameter | Type | Description | +| ---------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `contents` | `unknown` | Any value that can be converted to a string for writing to `stderr`. If a function is given with no arguments, the function will be executed and its response will be stringified for output. | **Returns:** `void` **See also:** @@ -2345,14 +2395,14 @@ logger.warn('Log this content when verbosity is >= 2'); If a function with zero arguments is passed, the function will be executed before writing. This is helpful for avoiding extra work in the event that the verbosity is not actually high enough to render the logged warning: ```ts -logger.warn(() => bigArray.map((item) => item.name)); +logger.warn(() => bigArray.map((item) => `- ${item.name}`).join('\n')); ``` **Parameters:** -| Parameter | Type | Description | -| ---------- | --------- | -------------------------------------------------------------------- | -| `contents` | `unknown` | Any value that can be converted to a string for writing to `stderr`. | +| Parameter | Type | Description | +| ---------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `contents` | `unknown` | Any value that can be converted to a string for writing to `stderr`. If a function is given with no arguments, the function will be executed and its response will be stringified for output. | **Returns:** `void` **See also:** @@ -2360,42 +2410,144 @@ logger.warn(() => bigArray.map((item) => item.name)); **Defined in:** [modules/logger/src/Logger.ts](https://github.com/paularmstrong/onerepo/blob/main/modules/logger/src/Logger.ts) +##### write() + +```ts +write( + chunk, + encoding?, + cb?): boolean +``` + +Write directly to the Logger's output stream, bypassing any formatting and verbosity filtering. + +:::caution[Advanced] +Since [LogStep](#logstep) implements a [Node.js duplex stream](https://nodejs.org/docs/latest-v20.x/api/stream.html#class-streamduplex), it is possible to use internal `write`, `read`, `pipe`, and all other available methods, but may not be fully recommended. +::: + +**Parameters:** + +| Parameter | Type | +| ----------- | ------------------- | +| `chunk` | `any` | +| `encoding`? | `BufferEncoding` | +| `cb`? | (`error`) => `void` | + +**Returns:** `boolean` +**See also:** +[\`LogStep.write\`](#write-1). + +**Defined in:** [modules/logger/src/Logger.ts](https://github.com/paularmstrong/onerepo/blob/main/modules/logger/src/Logger.ts) + --- ### LogStep -Log steps should only be created via the [\`logger.createStep()\`](#createstep) method. +LogSteps are an enhancement of [Node.js duplex streams](https://nodejs.org/docs/latest-v20.x/api/stream.html#class-streamduplex) that enable writing contextual messages to the program's output. + +Always create steps using the [\`logger.createStep()\`](#createstep) method so that they are properly tracked and linked to the parent logger. Creating a LogStep directly may result in errors and unintentional side effects. ```ts -const step = logger.createStep('Do some work'); -// ... long task with a bunch of potential output -await step.end(); +const myStep = logger.createStep(); +// Do work +myStep.info('Did some work'); +myStep.end(); ``` +#### Extends + +- `Duplex` + #### Properties -| Property | Type | Description | Defined in | -| ------------ | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -| `hasError` | `boolean` | Whether or not an error has been sent to the step. This is not necessarily indicative of uncaught thrown errors, but solely on whether `.error()` has been called in this step. | [modules/logger/src/LogStep.ts](https://github.com/paularmstrong/onerepo/blob/main/modules/logger/src/LogStep.ts) | -| `hasInfo` | `boolean` | Whether or not an info message has been sent to this step. | [modules/logger/src/LogStep.ts](https://github.com/paularmstrong/onerepo/blob/main/modules/logger/src/LogStep.ts) | -| `hasLog` | `boolean` | Whether or not a log message has been sent to this step. | [modules/logger/src/LogStep.ts](https://github.com/paularmstrong/onerepo/blob/main/modules/logger/src/LogStep.ts) | -| `hasWarning` | `boolean` | Whether or not a warning has been sent to this step. | [modules/logger/src/LogStep.ts](https://github.com/paularmstrong/onerepo/blob/main/modules/logger/src/LogStep.ts) | +| Property | Type | Defined in | +| ----------- | --------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| `verbosity` | [`Verbosity`](#verbosity-1) | [modules/logger/src/LogStep.ts](https://github.com/paularmstrong/onerepo/blob/main/modules/logger/src/LogStep.ts) | + +#### Accessors + +##### hasError + +###### Get Signature + +```ts +get hasError(): boolean +``` + +Whether this step has logged an error message. + +**Returns:** `boolean` +**Defined in:** [modules/logger/src/LogStep.ts](https://github.com/paularmstrong/onerepo/blob/main/modules/logger/src/LogStep.ts) + +##### hasInfo + +###### Get Signature + +```ts +get hasInfo(): boolean +``` + +Whether this step has logged an info-level message. + +**Returns:** `boolean` +**Defined in:** [modules/logger/src/LogStep.ts](https://github.com/paularmstrong/onerepo/blob/main/modules/logger/src/LogStep.ts) + +##### hasLog + +###### Get Signature + +```ts +get hasLog(): boolean +``` + +Whether this step has logged a log-level message. + +**Returns:** `boolean` +**Defined in:** [modules/logger/src/LogStep.ts](https://github.com/paularmstrong/onerepo/blob/main/modules/logger/src/LogStep.ts) + +##### hasWarning + +###### Get Signature + +```ts +get hasWarning(): boolean +``` + +Whether this step has logged a warning message. + +**Returns:** `boolean` +**Defined in:** [modules/logger/src/LogStep.ts](https://github.com/paularmstrong/onerepo/blob/main/modules/logger/src/LogStep.ts) #### Methods ##### end() ```ts -end(): Promise +end(callback?): this ``` -Finish this step and flush all buffered logs. Once a step is ended, it will no longer accept any logging output and will be effectively removed from the base logger. Consider this method similar to a destructor or teardown. +Signal the end of this step. After this method is called, it will no longer accept any more logs of any variety and will be removed from the parent Logger's queue. + +Failure to call this method will result in a warning and potentially fail oneRepo commands. It is important to ensure that each step is cleanly ended before returning from commands. ```ts -await step.end(); +const myStep = logger.createStep('My step'); +// do work +myStep.end(); ``` -**Returns:** `Promise`\<`void`\> +**Parameters:** + +| Parameter | Type | +| ----------- | ------------ | +| `callback`? | () => `void` | + +**Returns:** `this` + +###### Overrides + +`Duplex.end` + **Defined in:** [modules/logger/src/LogStep.ts](https://github.com/paularmstrong/onerepo/blob/main/modules/logger/src/LogStep.ts) #### Logging @@ -2406,23 +2558,25 @@ await step.end(); debug(contents): void ``` -Extra debug logging when verbosity greater than or equal to 4. +Log a debug message for this step. Debug messages will only be written to the program output if the [\`verbosity\`](#verbosity) is set to 4 or greater. ```ts -step.debug('Log this content when verbosity is >= 4'); +const step = logger.createStep('My step'); +step.debug('This message will be recorded and written out as an "DBG" labeled message'); +step.end(); ``` If a function with zero arguments is passed, the function will be executed before writing. This is helpful for avoiding extra work in the event that the verbosity is not actually high enough to render the logged debug information: ```ts -step.debug(() => bigArray.map((item) => item.name)); +step.debug(() => bigArray.map((item) => `- ${item.name}`).join('\n')); ``` **Parameters:** -| Parameter | Type | Description | -| ---------- | --------- | -------------------------------------------------------------------- | -| `contents` | `unknown` | Any value that can be converted to a string for writing to `stderr`. | +| Parameter | Type | Description | +| ---------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `contents` | `unknown` | Any value may be logged as a debug message, but will be stringified upon output. If a function is given with no arguments, the function will be executed and its response will be stringified for output. | **Returns:** `void` **Defined in:** [modules/logger/src/LogStep.ts](https://github.com/paularmstrong/onerepo/blob/main/modules/logger/src/LogStep.ts) @@ -2433,23 +2587,25 @@ step.debug(() => bigArray.map((item) => item.name)); error(contents): void ``` -Log an error. This will cause the root logger to include an error and fail a command. +Log an error message for this step. Any error log will cause the entire command run in oneRepo to fail and exit with code `1`. Error messages will only be written to the program output if the [\`verbosity\`](#verbosity) is set to 1 or greater – even if not written, the command will still fail and include an exit code. ```ts -step.error('Log this content when verbosity is >= 1'); +const step = logger.createStep('My step'); +step.error('This message will be recorded and written out as an "ERR" labeled message'); +step.end(); ``` If a function with zero arguments is passed, the function will be executed before writing. This is helpful for avoiding extra work in the event that the verbosity is not actually high enough to render the logged error: ```ts -step.error(() => bigArray.map((item) => item.name)); +step.error(() => bigArray.map((item) => `- ${item.name}`).join('\n')); ``` **Parameters:** -| Parameter | Type | Description | -| ---------- | --------- | -------------------------------------------------------------------- | -| `contents` | `unknown` | Any value that can be converted to a string for writing to `stderr`. | +| Parameter | Type | Description | +| ---------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `contents` | `unknown` | Any value may be logged as an error, but will be stringified upon output. If a function is given with no arguments, the function will be executed and its response will be stringified for output. | **Returns:** `void` **Defined in:** [modules/logger/src/LogStep.ts](https://github.com/paularmstrong/onerepo/blob/main/modules/logger/src/LogStep.ts) @@ -2460,23 +2616,25 @@ step.error(() => bigArray.map((item) => item.name)); info(contents): void ``` -Log an informative message. Should be used when trying to convey information with a user that is important enough to always be returned. +Log an informative message for this step. Info messages will only be written to the program output if the [\`verbosity\`](#verbosity) is set to 1 or greater. ```ts -step.info('Log this content when verbosity is >= 1'); +const step = logger.createStep('My step'); +step.info('This message will be recorded and written out as an "INFO" labeled message'); +step.end(); ``` If a function with zero arguments is passed, the function will be executed before writing. This is helpful for avoiding extra work in the event that the verbosity is not actually high enough to render the logged information: ```ts -step.info(() => bigArray.map((item) => item.name)); +step.info(() => bigArray.map((item) => `- ${item.name}`).join('\n')); ``` **Parameters:** -| Parameter | Type | Description | -| ---------- | --------- | -------------------------------------------------------------------- | -| `contents` | `unknown` | Any value that can be converted to a string for writing to `stderr`. | +| Parameter | Type | Description | +| ---------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `contents` | `unknown` | Any value may be logged as info, but will be stringified upon output. If a function is given with no arguments, the function will be executed and its response will be stringified for output. | **Returns:** `void` **Defined in:** [modules/logger/src/LogStep.ts](https://github.com/paularmstrong/onerepo/blob/main/modules/logger/src/LogStep.ts) @@ -2487,23 +2645,25 @@ step.info(() => bigArray.map((item) => item.name)); log(contents): void ``` -General logging information. Useful for light informative debugging. Recommended to use sparingly. +Log a message for this step. Log messages will only be written to the program output if the [\`verbosity\`](#verbosity) is set to 3 or greater. ```ts -step.log('Log this content when verbosity is >= 3'); +const step = logger.createStep('My step'); +step.log('This message will be recorded and written out as an "LOG" labeled message'); +step.end(); ``` If a function with zero arguments is passed, the function will be executed before writing. This is helpful for avoiding extra work in the event that the verbosity is not actually high enough to render the logged information: ```ts -step.log(() => bigArray.map((item) => item.name)); +step.log(() => bigArray.map((item) => `- ${item.name}`).join('\n')); ``` **Parameters:** -| Parameter | Type | Description | -| ---------- | --------- | -------------------------------------------------------------------- | -| `contents` | `unknown` | Any value that can be converted to a string for writing to `stderr`. | +| Parameter | Type | Description | +| ---------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `contents` | `unknown` | Any value may be logged, but will be stringified upon output. If a function is given with no arguments, the function will be executed and its response will be stringified for output. | **Returns:** `void` **Defined in:** [modules/logger/src/LogStep.ts](https://github.com/paularmstrong/onerepo/blob/main/modules/logger/src/LogStep.ts) @@ -2514,14 +2674,25 @@ step.log(() => bigArray.map((item) => item.name)); timing(start, end): void ``` -Log timing information between two [Node.js performance mark names](https://nodejs.org/dist/latest-v18.x/docs/api/perf_hooks.html#performancemarkname-options). +Log extra performance timing information. + +Timing information will only be written to the program output if the [\`verbosity\`](#verbosity) is set to 5. + +```ts +const myStep = logger.createStep('My step'); +performance.mark('start'); +// do work +performance.mark('end'); +myStep.timing('start', 'end'); +myStep.end(); +``` **Parameters:** -| Parameter | Type | Description | -| --------- | -------- | ------------------------------ | -| `start` | `string` | A `PerformanceMark` entry name | -| `end` | `string` | A `PerformanceMark` entry name | +| Parameter | Type | +| --------- | -------- | +| `start` | `string` | +| `end` | `string` | **Returns:** `void` **Defined in:** [modules/logger/src/LogStep.ts](https://github.com/paularmstrong/onerepo/blob/main/modules/logger/src/LogStep.ts) @@ -2532,44 +2703,86 @@ Log timing information between two [Node.js performance mark names](https://node warn(contents): void ``` -Log a warning. Does not have any effect on the command run, but will be called out. +Log a warning message for this step. Warnings will _not_ cause oneRepo commands to fail. Warning messages will only be written to the program output if the [\`verbosity\`](#verbosity) is set to 2 or greater. ```ts -step.warn('Log this content when verbosity is >= 2'); +const step = logger.createStep('My step'); +step.warn('This message will be recorded and written out as a "WRN" labeled message'); +step.end(); ``` If a function with zero arguments is passed, the function will be executed before writing. This is helpful for avoiding extra work in the event that the verbosity is not actually high enough to render the logged warning: ```ts -step.warn(() => bigArray.map((item) => item.name)); +step.warn(() => bigArray.map((item) => `- ${item.name}`).join('\n')); ``` **Parameters:** -| Parameter | Type | Description | -| ---------- | --------- | -------------------------------------------------------------------- | -| `contents` | `unknown` | Any value that can be converted to a string for writing to `stderr`. | +| Parameter | Type | Description | +| ---------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `contents` | `unknown` | Any value may be logged as a warning, but will be stringified upon output. If a function is given with no arguments, the function will be executed and its response will be stringified for output. | **Returns:** `void` **Defined in:** [modules/logger/src/LogStep.ts](https://github.com/paularmstrong/onerepo/blob/main/modules/logger/src/LogStep.ts) +##### write() + +```ts +write( + chunk, + encoding?, + cb?): boolean +``` + +Write directly to the step's stream, bypassing any formatting and verbosity filtering. + +:::caution[Advanced] +Since [LogStep](#logstep) implements a [Node.js duplex stream](https://nodejs.org/docs/latest-v20.x/api/stream.html#class-streamduplex) in `objectMode`, it is possible to use internal `write`, `read`, `pipe`, and all other available methods, but may not be fully recommended. +::: + +**Parameters:** + +| Parameter | Type | +| ----------- | -------------------------- | +| `chunk` | `string` \| `LoggedBuffer` | +| `encoding`? | `BufferEncoding` | +| `cb`? | (`error`) => `void` | + +**Returns:** `boolean` + +###### Overrides + +`Duplex.write` + +**Defined in:** [modules/logger/src/LogStep.ts](https://github.com/paularmstrong/onerepo/blob/main/modules/logger/src/LogStep.ts) + --- ### LoggerOptions ```ts type LoggerOptions: { - stream: Writable; + captureAll: boolean; + stream: Writable | LogStep; verbosity: Verbosity; }; ``` #### Type declaration +##### captureAll? + +```ts +optional captureAll: boolean; +``` + +**Experimental** + ##### stream? ```ts -optional stream: Writable; +optional stream: Writable | LogStep; ``` Advanced – override the writable stream in order to pipe logs elsewhere. Mostly used for dependency injection for `@onerepo/test-cli`. @@ -2586,6 +2799,48 @@ Control how much and what kind of output the Logger will provide. --- +### LogStepOptions + +```ts +type LogStepOptions: { + description: string; + name: string; + verbosity: Verbosity; +}; +``` + +#### Type declaration + +##### description? + +```ts +optional description: string; +``` + +Optionally include extra information for performance tracing on this step. This description will be passed through to the [`performanceMark.detail`](https://nodejs.org/docs/latest-v20.x/api/perf_hooks.html#performancemarkdetail) recorded internally for this step. + +Use a [Performance Writer plugin](https://onerepo.tools/plugins/performance-writer/) to read and work with this detail. + +##### name + +```ts +name: string; +``` + +Wraps all step output within the name provided for the step. + +##### verbosity + +```ts +verbosity: Verbosity; +``` + +The verbosity for this step, inherited from its parent [Logger](#logger). + +**Defined in:** [modules/logger/src/LogStep.ts](https://github.com/paularmstrong/onerepo/blob/main/modules/logger/src/LogStep.ts) + +--- + ### Verbosity ```ts @@ -2609,7 +2864,7 @@ Control the verbosity of the log output | `>= 4` | Debug | `logger.debug()` will be included | | `>= 5` | Timing | Extra performance timing metrics will be written | -**Defined in:** [modules/logger/src/Logger.ts](https://github.com/paularmstrong/onerepo/blob/main/modules/logger/src/Logger.ts) +**Defined in:** [modules/logger/src/types.ts](https://github.com/paularmstrong/onerepo/blob/main/modules/logger/src/types.ts) ## Package management @@ -3288,40 +3543,9 @@ new BatchError(errors, options?): BatchError #### Properties -| Property | Modifier | Type | Description | Inherited from | Defined in | -| -------------------- | -------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ | ------------------------- | --------------------------------------------------------------------------------------------------------------------- | -| `cause?` | `public` | `unknown` | - | `Error.cause` | node_modules/typescript/lib/lib.es2022.error.d.ts:26 | -| `errors` | `public` | (`string` \| [`SubprocessError`](#subprocesserror))[] | - | - | [modules/subprocess/src/index.ts](https://github.com/paularmstrong/onerepo/blob/main/modules/subprocess/src/index.ts) | -| `message` | `public` | `string` | - | `Error.message` | node_modules/typescript/lib/lib.es5.d.ts:1077 | -| `name` | `public` | `string` | - | `Error.name` | node_modules/typescript/lib/lib.es5.d.ts:1076 | -| `prepareStackTrace?` | `static` | (`err`: `Error`, `stackTraces`: `CallSite`[]) => `any` | Optional override for formatting stack traces **See** https://v8.dev/docs/stack-trace-api#customizing-stack-traces | `Error.prepareStackTrace` | node_modules/@types/node/globals.d.ts:98 | -| `stack?` | `public` | `string` | - | `Error.stack` | node_modules/typescript/lib/lib.es5.d.ts:1078 | -| `stackTraceLimit` | `static` | `number` | - | `Error.stackTraceLimit` | node_modules/@types/node/globals.d.ts:100 | - -#### Methods - -##### captureStackTrace() - -```ts -static captureStackTrace(targetObject, constructorOpt?): void -``` - -Create .stack property on a target object - -**Parameters:** - -| Parameter | Type | -| ----------------- | ---------- | -| `targetObject` | `object` | -| `constructorOpt`? | `Function` | - -**Returns:** `void` - -###### Inherited from - -`Error.captureStackTrace` - -**Defined in:** node_modules/@types/node/globals.d.ts:91 +| Property | Type | Defined in | +| -------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | +| `errors` | (`string` \| [`SubprocessError`](#subprocesserror))[] | [modules/subprocess/src/index.ts](https://github.com/paularmstrong/onerepo/blob/main/modules/subprocess/src/index.ts) | --- @@ -3354,42 +3578,6 @@ new SubprocessError(message, options?): SubprocessError **Defined in:** [modules/subprocess/src/index.ts](https://github.com/paularmstrong/onerepo/blob/main/modules/subprocess/src/index.ts) -#### Properties - -| Property | Modifier | Type | Description | Inherited from | Defined in | -| -------------------- | -------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ | ------------------------- | ---------------------------------------------------- | -| `cause?` | `public` | `unknown` | - | `Error.cause` | node_modules/typescript/lib/lib.es2022.error.d.ts:26 | -| `message` | `public` | `string` | - | `Error.message` | node_modules/typescript/lib/lib.es5.d.ts:1077 | -| `name` | `public` | `string` | - | `Error.name` | node_modules/typescript/lib/lib.es5.d.ts:1076 | -| `prepareStackTrace?` | `static` | (`err`: `Error`, `stackTraces`: `CallSite`[]) => `any` | Optional override for formatting stack traces **See** https://v8.dev/docs/stack-trace-api#customizing-stack-traces | `Error.prepareStackTrace` | node_modules/@types/node/globals.d.ts:98 | -| `stack?` | `public` | `string` | - | `Error.stack` | node_modules/typescript/lib/lib.es5.d.ts:1078 | -| `stackTraceLimit` | `static` | `number` | - | `Error.stackTraceLimit` | node_modules/@types/node/globals.d.ts:100 | - -#### Methods - -##### captureStackTrace() - -```ts -static captureStackTrace(targetObject, constructorOpt?): void -``` - -Create .stack property on a target object - -**Parameters:** - -| Parameter | Type | -| ----------------- | ---------- | -| `targetObject` | `object` | -| `constructorOpt`? | `Function` | - -**Returns:** `void` - -###### Inherited from - -`Error.captureStackTrace` - -**Defined in:** node_modules/@types/node/globals.d.ts:91 - --- ### BatchOptions diff --git a/docs/src/content/docs/plugins/docgen.mdx b/docs/src/content/docs/plugins/docgen.mdx index 3bbc185f..7f52bed3 100644 --- a/docs/src/content/docs/plugins/docgen.mdx +++ b/docs/src/content/docs/plugins/docgen.mdx @@ -42,6 +42,44 @@ Check out our very own source level [example](example/) generated directly from ## Configuration +{/* start-auto-generated-from-cli-docgen */} +{/* @generated SignedSource<> */} + +### `one docgen` + +Generate documentation for the oneRepo cli. + +```sh +one docgen [options...] +``` + +Help documentation should always be easy to find. This command will help automate the creation of docs for this command-line interface. If you are reading this somewhere that is not your terminal, there is a very good chance that this command was already run for you! + +Add this command to your one Repo tasks on pre-commit to ensure that your documentation is always up-to-date. + +| Option | Type | Description | +| ----------------- | --------------------------------------------- | -------------------------------------------------------------------------- | +| `--add` | `boolean` | Add the output file to the git stage | +| `--format` | `"markdown"`, `"json"`, default: `"markdown"` | Output format for documentation | +| `--heading-level` | `number` | Heading level to start at for Markdown output | +| `--out-file` | `string` | File to write output to. If not provided, stdout will be used | +| `--out-workspace` | `string` | Workspace name to write the --out-file to | +| `--safe-write` | `boolean` | Write documentation to a portion of the file with start and end sentinels. | + +
+ +Advanced options + +| Option | Type | Description | +| ----------------- | --------- | ------------------------------------------------------------------------------------ | +| `--command` | `string` | Start at the given command, skip the root and any others | +| `--show-advanced` | `boolean` | Pair with `--help` to show advanced options. | +| `--use-defaults` | `boolean` | Use the oneRepo default configuration. Helpful for generating default documentation. | + +
+ +{/* end-auto-generated-from-cli-docgen */} + {/* start-install-typedoc */} {/* @generated SignedSource<<9b847ec5a1fb60fd74a1bd5a66f31900>> */} @@ -142,43 +180,3 @@ optional safeWrite: boolean; Set to true to amend content to the given file using the file.writeSafe \| \`file.writeSafe\` method. {/* end-install-typedoc */} - -## Commands - -{/* start-auto-generated-from-cli-docgen */} -{/* @generated SignedSource<> */} - -### `one docgen` - -Generate documentation for the oneRepo cli. - -```sh -one docgen [options...] -``` - -Help documentation should always be easy to find. This command will help automate the creation of docs for this command-line interface. If you are reading this somewhere that is not your terminal, there is a very good chance that this command was already run for you! - -Add this command to your one Repo tasks on pre-commit to ensure that your documentation is always up-to-date. - -| Option | Type | Description | -| ----------------- | --------------------------------------------- | -------------------------------------------------------------------------- | -| `--add` | `boolean` | Add the output file to the git stage | -| `--format` | `"markdown"`, `"json"`, default: `"markdown"` | Output format for documentation | -| `--heading-level` | `number` | Heading level to start at for Markdown output | -| `--out-file` | `string` | File to write output to. If not provided, stdout will be used | -| `--out-workspace` | `string` | Workspace name to write the --out-file to | -| `--safe-write` | `boolean` | Write documentation to a portion of the file with start and end sentinels. | - -
- -Advanced options - -| Option | Type | Description | -| ----------------- | --------- | ------------------------------------------------------------------------------------ | -| `--command` | `string` | Start at the given command, skip the root and any others | -| `--show-advanced` | `boolean` | Pair with `--help` to show advanced options. | -| `--use-defaults` | `boolean` | Use the oneRepo default configuration. Helpful for generating default documentation. | - -
- -{/* end-auto-generated-from-cli-docgen */} diff --git a/modules/builders/src/getters.ts b/modules/builders/src/getters.ts index 0c5c1b84..a3480525 100644 --- a/modules/builders/src/getters.ts +++ b/modules/builders/src/getters.ts @@ -91,7 +91,7 @@ export function getAffected(graph: Graph, { from, ignore, staged, step, through return graph.workspaces; } - return await graph.affected(Array.from(workspaces)); + return graph.affected(Array.from(workspaces)); }); } @@ -144,7 +144,7 @@ export async function getWorkspaces( } else { const names = workspaces.map((ws) => ws.name); step.log(() => `\`affected\` requested from • ${names.join('\n • ')}`); - workspaces = await graph.affected(names); + workspaces = graph.affected(names); } } @@ -237,7 +237,7 @@ export async function getFilepaths( } } else { step.log('`affected` requested from Workspaces'); - const affected = await graph.affected(argv.workspaces!); + const affected = graph.affected(argv.workspaces!); paths.push(...affected.map((ws) => ws.location)); } } diff --git a/modules/github-action/dist/get-tasks.cjs b/modules/github-action/dist/get-tasks.cjs index 397d64f2..25482392 100644 --- a/modules/github-action/dist/get-tasks.cjs +++ b/modules/github-action/dist/get-tasks.cjs @@ -152,7 +152,7 @@ var require_command = __commonJS({ } }); -// node_modules/@actions/core/node_modules/uuid/dist/esm-node/rng.js +// node_modules/uuid/dist/esm-node/rng.js function rng() { if (poolPtr > rnds8Pool.length - 16) { import_crypto.default.randomFillSync(rnds8Pool); @@ -162,34 +162,34 @@ function rng() { } var import_crypto, rnds8Pool, poolPtr; var init_rng = __esm({ - "node_modules/@actions/core/node_modules/uuid/dist/esm-node/rng.js"() { + "node_modules/uuid/dist/esm-node/rng.js"() { import_crypto = __toESM(require("crypto")); rnds8Pool = new Uint8Array(256); poolPtr = rnds8Pool.length; } }); -// node_modules/@actions/core/node_modules/uuid/dist/esm-node/regex.js +// node_modules/uuid/dist/esm-node/regex.js var regex_default; var init_regex = __esm({ - "node_modules/@actions/core/node_modules/uuid/dist/esm-node/regex.js"() { + "node_modules/uuid/dist/esm-node/regex.js"() { regex_default = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i; } }); -// node_modules/@actions/core/node_modules/uuid/dist/esm-node/validate.js +// node_modules/uuid/dist/esm-node/validate.js function validate(uuid) { return typeof uuid === "string" && regex_default.test(uuid); } var validate_default; var init_validate = __esm({ - "node_modules/@actions/core/node_modules/uuid/dist/esm-node/validate.js"() { + "node_modules/uuid/dist/esm-node/validate.js"() { init_regex(); validate_default = validate; } }); -// node_modules/@actions/core/node_modules/uuid/dist/esm-node/stringify.js +// node_modules/uuid/dist/esm-node/stringify.js function stringify(arr, offset = 0) { const uuid = (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase(); if (!validate_default(uuid)) { @@ -199,7 +199,7 @@ function stringify(arr, offset = 0) { } var byteToHex, stringify_default; var init_stringify = __esm({ - "node_modules/@actions/core/node_modules/uuid/dist/esm-node/stringify.js"() { + "node_modules/uuid/dist/esm-node/stringify.js"() { init_validate(); byteToHex = []; for (let i = 0; i < 256; ++i) { @@ -209,7 +209,7 @@ var init_stringify = __esm({ } }); -// node_modules/@actions/core/node_modules/uuid/dist/esm-node/v1.js +// node_modules/uuid/dist/esm-node/v1.js function v1(options, buf, offset) { let i = buf && offset || 0; const b = buf || new Array(16); @@ -260,7 +260,7 @@ function v1(options, buf, offset) { } var _nodeId, _clockseq, _lastMSecs, _lastNSecs, v1_default; var init_v1 = __esm({ - "node_modules/@actions/core/node_modules/uuid/dist/esm-node/v1.js"() { + "node_modules/uuid/dist/esm-node/v1.js"() { init_rng(); init_stringify(); _lastMSecs = 0; @@ -269,7 +269,7 @@ var init_v1 = __esm({ } }); -// node_modules/@actions/core/node_modules/uuid/dist/esm-node/parse.js +// node_modules/uuid/dist/esm-node/parse.js function parse(uuid) { if (!validate_default(uuid)) { throw TypeError("Invalid UUID"); @@ -296,13 +296,13 @@ function parse(uuid) { } var parse_default; var init_parse = __esm({ - "node_modules/@actions/core/node_modules/uuid/dist/esm-node/parse.js"() { + "node_modules/uuid/dist/esm-node/parse.js"() { init_validate(); parse_default = parse; } }); -// node_modules/@actions/core/node_modules/uuid/dist/esm-node/v35.js +// node_modules/uuid/dist/esm-node/v35.js function stringToBytes(str) { str = unescape(encodeURIComponent(str)); const bytes = []; @@ -347,7 +347,7 @@ function v35_default(name, version2, hashfunc) { } var DNS, URL2; var init_v35 = __esm({ - "node_modules/@actions/core/node_modules/uuid/dist/esm-node/v35.js"() { + "node_modules/uuid/dist/esm-node/v35.js"() { init_stringify(); init_parse(); DNS = "6ba7b810-9dad-11d1-80b4-00c04fd430c8"; @@ -355,7 +355,7 @@ var init_v35 = __esm({ } }); -// node_modules/@actions/core/node_modules/uuid/dist/esm-node/md5.js +// node_modules/uuid/dist/esm-node/md5.js function md5(bytes) { if (Array.isArray(bytes)) { bytes = Buffer.from(bytes); @@ -366,16 +366,16 @@ function md5(bytes) { } var import_crypto2, md5_default; var init_md5 = __esm({ - "node_modules/@actions/core/node_modules/uuid/dist/esm-node/md5.js"() { + "node_modules/uuid/dist/esm-node/md5.js"() { import_crypto2 = __toESM(require("crypto")); md5_default = md5; } }); -// node_modules/@actions/core/node_modules/uuid/dist/esm-node/v3.js +// node_modules/uuid/dist/esm-node/v3.js var v3, v3_default; var init_v3 = __esm({ - "node_modules/@actions/core/node_modules/uuid/dist/esm-node/v3.js"() { + "node_modules/uuid/dist/esm-node/v3.js"() { init_v35(); init_md5(); v3 = v35_default("v3", 48, md5_default); @@ -383,7 +383,7 @@ var init_v3 = __esm({ } }); -// node_modules/@actions/core/node_modules/uuid/dist/esm-node/v4.js +// node_modules/uuid/dist/esm-node/v4.js function v4(options, buf, offset) { options = options || {}; const rnds = options.random || (options.rng || rng)(); @@ -400,14 +400,14 @@ function v4(options, buf, offset) { } var v4_default; var init_v4 = __esm({ - "node_modules/@actions/core/node_modules/uuid/dist/esm-node/v4.js"() { + "node_modules/uuid/dist/esm-node/v4.js"() { init_rng(); init_stringify(); v4_default = v4; } }); -// node_modules/@actions/core/node_modules/uuid/dist/esm-node/sha1.js +// node_modules/uuid/dist/esm-node/sha1.js function sha1(bytes) { if (Array.isArray(bytes)) { bytes = Buffer.from(bytes); @@ -418,16 +418,16 @@ function sha1(bytes) { } var import_crypto3, sha1_default; var init_sha1 = __esm({ - "node_modules/@actions/core/node_modules/uuid/dist/esm-node/sha1.js"() { + "node_modules/uuid/dist/esm-node/sha1.js"() { import_crypto3 = __toESM(require("crypto")); sha1_default = sha1; } }); -// node_modules/@actions/core/node_modules/uuid/dist/esm-node/v5.js +// node_modules/uuid/dist/esm-node/v5.js var v5, v5_default; var init_v5 = __esm({ - "node_modules/@actions/core/node_modules/uuid/dist/esm-node/v5.js"() { + "node_modules/uuid/dist/esm-node/v5.js"() { init_v35(); init_sha1(); v5 = v35_default("v5", 80, sha1_default); @@ -435,15 +435,15 @@ var init_v5 = __esm({ } }); -// node_modules/@actions/core/node_modules/uuid/dist/esm-node/nil.js +// node_modules/uuid/dist/esm-node/nil.js var nil_default; var init_nil = __esm({ - "node_modules/@actions/core/node_modules/uuid/dist/esm-node/nil.js"() { + "node_modules/uuid/dist/esm-node/nil.js"() { nil_default = "00000000-0000-0000-0000-000000000000"; } }); -// node_modules/@actions/core/node_modules/uuid/dist/esm-node/version.js +// node_modules/uuid/dist/esm-node/version.js function version(uuid) { if (!validate_default(uuid)) { throw TypeError("Invalid UUID"); @@ -452,13 +452,13 @@ function version(uuid) { } var version_default; var init_version = __esm({ - "node_modules/@actions/core/node_modules/uuid/dist/esm-node/version.js"() { + "node_modules/uuid/dist/esm-node/version.js"() { init_validate(); version_default = version; } }); -// node_modules/@actions/core/node_modules/uuid/dist/esm-node/index.js +// node_modules/uuid/dist/esm-node/index.js var esm_node_exports = {}; __export(esm_node_exports, { NIL: () => nil_default, @@ -472,7 +472,7 @@ __export(esm_node_exports, { version: () => version_default }); var init_esm_node = __esm({ - "node_modules/@actions/core/node_modules/uuid/dist/esm-node/index.js"() { + "node_modules/uuid/dist/esm-node/index.js"() { init_v1(); init_v3(); init_v4(); diff --git a/modules/github-action/dist/run-task.cjs b/modules/github-action/dist/run-task.cjs index c2584c5f..67dcbd3e 100644 --- a/modules/github-action/dist/run-task.cjs +++ b/modules/github-action/dist/run-task.cjs @@ -152,7 +152,7 @@ var require_command = __commonJS({ } }); -// node_modules/@actions/core/node_modules/uuid/dist/esm-node/rng.js +// node_modules/uuid/dist/esm-node/rng.js function rng() { if (poolPtr > rnds8Pool.length - 16) { import_crypto.default.randomFillSync(rnds8Pool); @@ -162,34 +162,34 @@ function rng() { } var import_crypto, rnds8Pool, poolPtr; var init_rng = __esm({ - "node_modules/@actions/core/node_modules/uuid/dist/esm-node/rng.js"() { + "node_modules/uuid/dist/esm-node/rng.js"() { import_crypto = __toESM(require("crypto")); rnds8Pool = new Uint8Array(256); poolPtr = rnds8Pool.length; } }); -// node_modules/@actions/core/node_modules/uuid/dist/esm-node/regex.js +// node_modules/uuid/dist/esm-node/regex.js var regex_default; var init_regex = __esm({ - "node_modules/@actions/core/node_modules/uuid/dist/esm-node/regex.js"() { + "node_modules/uuid/dist/esm-node/regex.js"() { regex_default = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i; } }); -// node_modules/@actions/core/node_modules/uuid/dist/esm-node/validate.js +// node_modules/uuid/dist/esm-node/validate.js function validate(uuid) { return typeof uuid === "string" && regex_default.test(uuid); } var validate_default; var init_validate = __esm({ - "node_modules/@actions/core/node_modules/uuid/dist/esm-node/validate.js"() { + "node_modules/uuid/dist/esm-node/validate.js"() { init_regex(); validate_default = validate; } }); -// node_modules/@actions/core/node_modules/uuid/dist/esm-node/stringify.js +// node_modules/uuid/dist/esm-node/stringify.js function stringify(arr, offset = 0) { const uuid = (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase(); if (!validate_default(uuid)) { @@ -199,7 +199,7 @@ function stringify(arr, offset = 0) { } var byteToHex, stringify_default; var init_stringify = __esm({ - "node_modules/@actions/core/node_modules/uuid/dist/esm-node/stringify.js"() { + "node_modules/uuid/dist/esm-node/stringify.js"() { init_validate(); byteToHex = []; for (let i = 0; i < 256; ++i) { @@ -209,7 +209,7 @@ var init_stringify = __esm({ } }); -// node_modules/@actions/core/node_modules/uuid/dist/esm-node/v1.js +// node_modules/uuid/dist/esm-node/v1.js function v1(options, buf, offset) { let i = buf && offset || 0; const b = buf || new Array(16); @@ -260,7 +260,7 @@ function v1(options, buf, offset) { } var _nodeId, _clockseq, _lastMSecs, _lastNSecs, v1_default; var init_v1 = __esm({ - "node_modules/@actions/core/node_modules/uuid/dist/esm-node/v1.js"() { + "node_modules/uuid/dist/esm-node/v1.js"() { init_rng(); init_stringify(); _lastMSecs = 0; @@ -269,7 +269,7 @@ var init_v1 = __esm({ } }); -// node_modules/@actions/core/node_modules/uuid/dist/esm-node/parse.js +// node_modules/uuid/dist/esm-node/parse.js function parse(uuid) { if (!validate_default(uuid)) { throw TypeError("Invalid UUID"); @@ -296,13 +296,13 @@ function parse(uuid) { } var parse_default; var init_parse = __esm({ - "node_modules/@actions/core/node_modules/uuid/dist/esm-node/parse.js"() { + "node_modules/uuid/dist/esm-node/parse.js"() { init_validate(); parse_default = parse; } }); -// node_modules/@actions/core/node_modules/uuid/dist/esm-node/v35.js +// node_modules/uuid/dist/esm-node/v35.js function stringToBytes(str) { str = unescape(encodeURIComponent(str)); const bytes = []; @@ -347,7 +347,7 @@ function v35_default(name, version2, hashfunc) { } var DNS, URL2; var init_v35 = __esm({ - "node_modules/@actions/core/node_modules/uuid/dist/esm-node/v35.js"() { + "node_modules/uuid/dist/esm-node/v35.js"() { init_stringify(); init_parse(); DNS = "6ba7b810-9dad-11d1-80b4-00c04fd430c8"; @@ -355,7 +355,7 @@ var init_v35 = __esm({ } }); -// node_modules/@actions/core/node_modules/uuid/dist/esm-node/md5.js +// node_modules/uuid/dist/esm-node/md5.js function md5(bytes) { if (Array.isArray(bytes)) { bytes = Buffer.from(bytes); @@ -366,16 +366,16 @@ function md5(bytes) { } var import_crypto2, md5_default; var init_md5 = __esm({ - "node_modules/@actions/core/node_modules/uuid/dist/esm-node/md5.js"() { + "node_modules/uuid/dist/esm-node/md5.js"() { import_crypto2 = __toESM(require("crypto")); md5_default = md5; } }); -// node_modules/@actions/core/node_modules/uuid/dist/esm-node/v3.js +// node_modules/uuid/dist/esm-node/v3.js var v3, v3_default; var init_v3 = __esm({ - "node_modules/@actions/core/node_modules/uuid/dist/esm-node/v3.js"() { + "node_modules/uuid/dist/esm-node/v3.js"() { init_v35(); init_md5(); v3 = v35_default("v3", 48, md5_default); @@ -383,7 +383,7 @@ var init_v3 = __esm({ } }); -// node_modules/@actions/core/node_modules/uuid/dist/esm-node/v4.js +// node_modules/uuid/dist/esm-node/v4.js function v4(options, buf, offset) { options = options || {}; const rnds = options.random || (options.rng || rng)(); @@ -400,14 +400,14 @@ function v4(options, buf, offset) { } var v4_default; var init_v4 = __esm({ - "node_modules/@actions/core/node_modules/uuid/dist/esm-node/v4.js"() { + "node_modules/uuid/dist/esm-node/v4.js"() { init_rng(); init_stringify(); v4_default = v4; } }); -// node_modules/@actions/core/node_modules/uuid/dist/esm-node/sha1.js +// node_modules/uuid/dist/esm-node/sha1.js function sha1(bytes) { if (Array.isArray(bytes)) { bytes = Buffer.from(bytes); @@ -418,16 +418,16 @@ function sha1(bytes) { } var import_crypto3, sha1_default; var init_sha1 = __esm({ - "node_modules/@actions/core/node_modules/uuid/dist/esm-node/sha1.js"() { + "node_modules/uuid/dist/esm-node/sha1.js"() { import_crypto3 = __toESM(require("crypto")); sha1_default = sha1; } }); -// node_modules/@actions/core/node_modules/uuid/dist/esm-node/v5.js +// node_modules/uuid/dist/esm-node/v5.js var v5, v5_default; var init_v5 = __esm({ - "node_modules/@actions/core/node_modules/uuid/dist/esm-node/v5.js"() { + "node_modules/uuid/dist/esm-node/v5.js"() { init_v35(); init_sha1(); v5 = v35_default("v5", 80, sha1_default); @@ -435,15 +435,15 @@ var init_v5 = __esm({ } }); -// node_modules/@actions/core/node_modules/uuid/dist/esm-node/nil.js +// node_modules/uuid/dist/esm-node/nil.js var nil_default; var init_nil = __esm({ - "node_modules/@actions/core/node_modules/uuid/dist/esm-node/nil.js"() { + "node_modules/uuid/dist/esm-node/nil.js"() { nil_default = "00000000-0000-0000-0000-000000000000"; } }); -// node_modules/@actions/core/node_modules/uuid/dist/esm-node/version.js +// node_modules/uuid/dist/esm-node/version.js function version(uuid) { if (!validate_default(uuid)) { throw TypeError("Invalid UUID"); @@ -452,13 +452,13 @@ function version(uuid) { } var version_default; var init_version = __esm({ - "node_modules/@actions/core/node_modules/uuid/dist/esm-node/version.js"() { + "node_modules/uuid/dist/esm-node/version.js"() { init_validate(); version_default = version; } }); -// node_modules/@actions/core/node_modules/uuid/dist/esm-node/index.js +// node_modules/uuid/dist/esm-node/index.js var esm_node_exports = {}; __export(esm_node_exports, { NIL: () => nil_default, @@ -472,7 +472,7 @@ __export(esm_node_exports, { version: () => version_default }); var init_esm_node = __esm({ - "node_modules/@actions/core/node_modules/uuid/dist/esm-node/index.js"() { + "node_modules/uuid/dist/esm-node/index.js"() { init_v1(); init_v3(); init_v4(); diff --git a/modules/logger/.changes/000-little-mugs-vanish.md b/modules/logger/.changes/000-little-mugs-vanish.md new file mode 100644 index 00000000..c529d28b --- /dev/null +++ b/modules/logger/.changes/000-little-mugs-vanish.md @@ -0,0 +1,7 @@ +--- +type: minor +--- + +The `Logger` and `LogStep` now implement streaming output differently in order to always fully capture potential output and switch on verbosity. + +This is a major internal rewrite, but should be fully compatible with the previous implementations. diff --git a/modules/logger/package.json b/modules/logger/package.json index 59a3c734..e0277689 100644 --- a/modules/logger/package.json +++ b/modules/logger/package.json @@ -21,8 +21,8 @@ "./CHANGELOG.md" ], "dependencies": { - "log-update": "^5.0.1", - "picocolors": "^1.0.0" + "picocolors": "^1.0.0", + "restore-cursor": "^5.0.0" }, "devDependencies": { "@internal/jest-config": "workspace:^", diff --git a/modules/logger/src/LogBuffer.ts b/modules/logger/src/LogBuffer.ts deleted file mode 100644 index 32f8c0e7..00000000 --- a/modules/logger/src/LogBuffer.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Duplex } from 'node:stream'; - -export class LogBuffer extends Duplex { - _read() {} - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _write(chunk: string, encoding = 'utf8', callback: () => void) { - this.push(chunk); - callback(); - } - - _final(callback: () => void) { - this.push(null); - callback(); - } -} diff --git a/modules/logger/src/LogStep.ts b/modules/logger/src/LogStep.ts index 46fde1aa..927ff060 100644 --- a/modules/logger/src/LogStep.ts +++ b/modules/logger/src/LogStep.ts @@ -1,433 +1,382 @@ -import { performance } from 'node:perf_hooks'; -import type { Writable } from 'node:stream'; +import { Duplex } from 'node:stream'; import pc from 'picocolors'; -import { LogBuffer } from './LogBuffer'; +import { stringify } from './utils/string'; +import type { LineType, LoggedBuffer, Verbosity } from './types'; -type StepOptions = { - verbosity: number; - onEnd: (step: LogStep) => Promise; - onMessage: (type: 'error' | 'warn' | 'info' | 'log' | 'debug') => void; - stream?: Writable; +/** + * @group Logger + */ +export type LogStepOptions = { + /** + * Wraps all step output within the name provided for the step. + */ + name: string; + /** + * Optionally include extra information for performance tracing on this step. This description will be passed through to the [`performanceMark.detail`](https://nodejs.org/docs/latest-v20.x/api/perf_hooks.html#performancemarkdetail) recorded internally for this step. + * + * Use a [Performance Writer plugin](https://onerepo.tools/plugins/performance-writer/) to read and work with this detail. + */ description?: string; - writePrefixes?: boolean; -}; - -const prefix = { - FAIL: pc.red('✘'), - SUCCESS: pc.green('✔'), - TIMER: pc.red('⏳'), - START: pc.dim(pc.bold('▶︎')), - END: pc.dim(pc.bold('■')), - ERR: pc.red(pc.bold('ERR')), - WARN: pc.yellow(pc.bold('WRN')), - LOG: pc.cyan(pc.bold('LOG')), - DBG: pc.magenta(pc.bold('DBG')), - INFO: pc.blue(pc.bold('INFO')), + /** + * The verbosity for this step, inherited from its parent {@link Logger}. + */ + verbosity: Verbosity; }; /** - * Log steps should only be created via the {@link Logger#createStep | `logger.createStep()`} method. + * LogSteps are an enhancement of [Node.js duplex streams](https://nodejs.org/docs/latest-v20.x/api/stream.html#class-streamduplex) that enable writing contextual messages to the program's output. + * + * Always create steps using the {@link Logger.createStep | `logger.createStep()`} method so that they are properly tracked and linked to the parent logger. Creating a LogStep directly may result in errors and unintentional side effects. * * ```ts - * const step = logger.createStep('Do some work'); - * // ... long task with a bunch of potential output - * await step.end(); + * const myStep = logger.createStep(); + * // Do work + * myStep.info('Did some work'); + * myStep.end(); * ``` - * * @group Logger */ -export class LogStep { - #name: string; - #verbosity: number; - #buffer: LogBuffer; - #stream: Writable; - #active = false; - #onEnd: StepOptions['onEnd']; - #onMessage: StepOptions['onMessage']; - #lastThree: Array = []; - #writing: boolean = false; - #writePrefixes: boolean = true; - #startMark: string; - - /** - * Whether or not an error has been sent to the step. This is not necessarily indicative of uncaught thrown errors, but solely on whether `.error()` has been called in this step. - */ - hasError = false; - /** - * Whether or not a warning has been sent to this step. - */ - hasWarning = false; +export class LogStep extends Duplex { /** - * Whether or not an info message has been sent to this step. + * @internal */ - hasInfo = false; + name?: string; /** - * Whether or not a log message has been sent to this step. + * @internal */ - hasLog = false; + isPiped: boolean = false; + verbosity: Verbosity; + + #startMark: string; + + #hasError: boolean = false; + #hasWarning: boolean = false; + #hasInfo: boolean = false; + #hasLog: boolean = false; /** * @internal */ - constructor(name: string, { onEnd, onMessage, verbosity, stream, description, writePrefixes }: StepOptions) { + constructor(options: LogStepOptions) { + const { description, name, verbosity } = options; + super({ decodeStrings: false, objectMode: true }); + this.verbosity = verbosity; + this.#startMark = name || `${performance.now()}`; performance.mark(`onerepo_start_${this.#startMark}`, { detail: description, }); - this.#verbosity = verbosity; - this.#name = name; - this.#onEnd = onEnd; - this.#onMessage = onMessage; - this.#buffer = new LogBuffer({}); - this.#stream = stream ?? process.stderr; - this.#writePrefixes = writePrefixes ?? true; - - if (this.name) { - if (process.env.GITHUB_RUN_ID) { - this.#writeStream(`::group::${this.name}\n`); - } - this.#writeStream(this.#prefixStart(this.name)); - } + + this.name = name; + this.#write('start', name); } /** - * @internal + * Write directly to the step's stream, bypassing any formatting and verbosity filtering. + * + * :::caution[Advanced] + * Since {@link LogStep} implements a [Node.js duplex stream](https://nodejs.org/docs/latest-v20.x/api/stream.html#class-streamduplex) in `objectMode`, it is possible to use internal `write`, `read`, `pipe`, and all other available methods, but may not be fully recommended. + * ::: + * + * @group Logging */ - get writable() { - return this.#stream.writable && this.#buffer.writable; - } + write( + chunk: LoggedBuffer | string, + encoding?: BufferEncoding, + cb?: (error: Error | null | undefined) => void, + ): boolean; /** * @internal */ - get name() { - return this.#name; + write(chunk: LoggedBuffer | string, cb?: (error: Error | null | undefined) => void): boolean; + + write( + // @ts-expect-error + ...args + ) { + // @ts-expect-error + return super.write(...args); } /** * @internal */ - get active() { - return this.#active; - } + _read() {} /** - * While buffering logs, returns the status line and last 3 lines of buffered output. - * * @internal */ - get status(): Array { - return [this.#prefixStart(this.name), ...this.#lastThree]; + _write( + chunk: string | LoggedBuffer, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + encoding = 'utf8', + callback: () => void, + ) { + this.push(chunk); + callback(); } /** * @internal */ - set verbosity(verbosity: number) { - this.#verbosity = verbosity; + _final(callback: () => void) { + this.push(null); + callback(); + } + + #write(type: LineType, contents: unknown) { + this.write({ + type, + contents: stringify(contents), + group: this.name, + verbosity: this.verbosity, + } satisfies LoggedBuffer); } /** * @internal */ - get verbosity() { - return this.#verbosity; + set hasError(hasError: boolean) { + this.#hasError = this.#hasError || hasError; } /** - * Activate a step. This is typically only called from within the root `Logger` instance and should not be done manually. - * - * @internal + * Whether this step has logged an error message. */ - activate(enableWrite = !('isTTY' in this.#stream && this.#stream.isTTY)) { - if (this.#active && this.#writing === enableWrite) { - return; - } - - this.#active = true; - - if (enableWrite) { - this.#enableWrite(); - } + get hasError() { + return this.#hasError; } /** * @internal */ - deactivate() { - if (!this.#active) { - return; - } - - this.#active = false; - if (this.#writing) { - this.#buffer.cork(); - this.#writing = false; - } + set hasWarning(hasWarning: boolean) { + this.#hasWarning = this.#hasWarning || hasWarning; } - #enableWrite() { - if (this.#writing) { - return; - } - - if (this.verbosity <= 0) { - this.#writing = false; - return; - } - - if (this.#buffer.writableCorked) { - this.#buffer.uncork(); - this.#writing = true; - return; - } - - this.#buffer.pipe(this.#stream); - this.#buffer.read(); - this.#writing = true; + /** + * Whether this step has logged a warning message. + */ + get hasWarning() { + return this.#hasWarning; } /** - * Finish this step and flush all buffered logs. Once a step is ended, it will no longer accept any logging output and will be effectively removed from the base logger. Consider this method similar to a destructor or teardown. - * - * ```ts - * await step.end(); - * ``` + * @internal */ - async end() { - const endMark = performance.mark(`onerepo_end_${this.#startMark}`); - const [startMark] = performance.getEntriesByName(`onerepo_start_${this.#startMark}`); - - // TODO: jest.useFakeTimers does not seem to be applying to performance correctly - const duration = - !startMark || process.env.NODE_ENV === 'test' ? 0 : Math.round(endMark.startTime - startMark.startTime); - const text = this.name - ? pc.dim(`${duration}ms`) - : `Completed${this.hasError ? ' with errors' : ''} ${pc.dim(`${duration}ms`)}`; - this.#writeStream(ensureNewline(this.#prefixEnd(`${this.hasError ? prefix.FAIL : prefix.SUCCESS} ${text}`))); - if (this.name && process.env.GITHUB_RUN_ID) { - this.#writeStream('::endgroup::\n'); - } + set hasInfo(hasInfo: boolean) { + this.#hasInfo = this.#hasInfo || hasInfo; + } - return this.#onEnd(this); + /** + * Whether this step has logged an info-level message. + */ + get hasInfo() { + return this.#hasInfo; } /** * @internal */ - async flush(): Promise { - this.#active = true; - this.#enableWrite(); - - // if not writable, then we can't actually flush/end anything - if (!this.#buffer.writable) { - return; - } - - await new Promise((resolve) => { - setImmediate(() => { - resolve(); - }); - }); + set hasLog(hasLog: boolean) { + this.#hasLog = this.#hasLog || hasLog; + } - // Unpipe the buffer, helps with memory/gc - // But do it after a tick (above) otherwise the buffer may not be done flushing to stream - this.#buffer.unpipe(); + /** + * Whether this step has logged a log-level message. + */ + get hasLog() { + return this.#hasLog; } /** - * Log an informative message. Should be used when trying to convey information with a user that is important enough to always be returned. + * Log an error message for this step. Any error log will cause the entire command run in oneRepo to fail and exit with code `1`. Error messages will only be written to the program output if the {@link Logger.verbosity | `verbosity`} is set to 1 or greater – even if not written, the command will still fail and include an exit code. + * * * ```ts - * step.info('Log this content when verbosity is >= 1'); + * const step = logger.createStep('My step'); + * step.error('This message will be recorded and written out as an "ERR" labeled message'); + * step.end(); * ``` * - * If a function with zero arguments is passed, the function will be executed before writing. This is helpful for avoiding extra work in the event that the verbosity is not actually high enough to render the logged information: + * If a function with zero arguments is passed, the function will be executed before writing. This is helpful for avoiding extra work in the event that the verbosity is not actually high enough to render the logged error: * * ```ts - * step.info(() => bigArray.map((item) => item.name)); + * step.error(() => bigArray.map((item) => `- ${item.name}`).join('\n')); * ``` * + * @param contents Any value may be logged as an error, but will be stringified upon output. If a function is given with no arguments, the function will be executed and its response will be stringified for output. + * * @group Logging - * @param contents Any value that can be converted to a string for writing to `stderr`. */ - info(contents: unknown) { - this.#onMessage('info'); - this.hasInfo = true; - if (this.verbosity >= 1) { - this.#writeStream(this.#prefix(prefix.INFO, stringify(contents))); - } + error(contents: unknown) { + this.hasError = true; + this.#write('error', contents); } /** - * Log an error. This will cause the root logger to include an error and fail a command. + * Log a warning message for this step. Warnings will _not_ cause oneRepo commands to fail. Warning messages will only be written to the program output if the {@link Logger.verbosity | `verbosity`} is set to 2 or greater. + * * * ```ts - * step.error('Log this content when verbosity is >= 1'); + * const step = logger.createStep('My step'); + * step.warn('This message will be recorded and written out as a "WRN" labeled message'); + * step.end(); * ``` * - * If a function with zero arguments is passed, the function will be executed before writing. This is helpful for avoiding extra work in the event that the verbosity is not actually high enough to render the logged error: + * If a function with zero arguments is passed, the function will be executed before writing. This is helpful for avoiding extra work in the event that the verbosity is not actually high enough to render the logged warning: * * ```ts - * step.error(() => bigArray.map((item) => item.name)); + * step.warn(() => bigArray.map((item) => `- ${item.name}`).join('\n')); * ``` * + * @param contents Any value may be logged as a warning, but will be stringified upon output. If a function is given with no arguments, the function will be executed and its response will be stringified for output. + * * @group Logging - * @param contents Any value that can be converted to a string for writing to `stderr`. */ - error(contents: unknown) { - this.#onMessage('error'); - this.hasError = true; - if (this.verbosity >= 1) { - this.#writeStream(this.#prefix(prefix.ERR, stringify(contents))); - } + warn(contents: unknown) { + this.hasWarning = true; + this.#write('warn', contents); } /** - * Log a warning. Does not have any effect on the command run, but will be called out. + * Log an informative message for this step. Info messages will only be written to the program output if the {@link Logger.verbosity | `verbosity`} is set to 1 or greater. * * ```ts - * step.warn('Log this content when verbosity is >= 2'); + * const step = logger.createStep('My step'); + * step.info('This message will be recorded and written out as an "INFO" labeled message'); + * step.end(); * ``` * - * If a function with zero arguments is passed, the function will be executed before writing. This is helpful for avoiding extra work in the event that the verbosity is not actually high enough to render the logged warning: + * If a function with zero arguments is passed, the function will be executed before writing. This is helpful for avoiding extra work in the event that the verbosity is not actually high enough to render the logged information: * * ```ts - * step.warn(() => bigArray.map((item) => item.name)); + * step.info(() => bigArray.map((item) => `- ${item.name}`).join('\n')); * ``` * + * @param contents Any value may be logged as info, but will be stringified upon output. If a function is given with no arguments, the function will be executed and its response will be stringified for output. + * * @group Logging - * @param contents Any value that can be converted to a string for writing to `stderr`. */ - warn(contents: unknown) { - this.#onMessage('warn'); - this.hasWarning = true; - if (this.verbosity >= 2) { - this.#writeStream(this.#prefix(prefix.WARN, stringify(contents))); - } + info(contents: unknown) { + this.hasInfo = true; + this.#write('info', contents); } /** - * General logging information. Useful for light informative debugging. Recommended to use sparingly. + * Log a message for this step. Log messages will only be written to the program output if the {@link Logger.verbosity | `verbosity`} is set to 3 or greater. * * ```ts - * step.log('Log this content when verbosity is >= 3'); + * const step = logger.createStep('My step'); + * step.log('This message will be recorded and written out as an "LOG" labeled message'); + * step.end(); * ``` * * If a function with zero arguments is passed, the function will be executed before writing. This is helpful for avoiding extra work in the event that the verbosity is not actually high enough to render the logged information: * * ```ts - * step.log(() => bigArray.map((item) => item.name)); + * step.log(() => bigArray.map((item) => `- ${item.name}`).join('\n')); * ``` * + * @param contents Any value may be logged, but will be stringified upon output. If a function is given with no arguments, the function will be executed and its response will be stringified for output. + * * @group Logging - * @param contents Any value that can be converted to a string for writing to `stderr`. */ log(contents: unknown) { - this.#onMessage('log'); this.hasLog = true; - if (this.verbosity >= 3) { - this.#writeStream(this.#prefix(this.name ? prefix.LOG : '', stringify(contents))); - } + this.#write('log', contents); } /** - * Extra debug logging when verbosity greater than or equal to 4. + * Log a debug message for this step. Debug messages will only be written to the program output if the {@link Logger.verbosity | `verbosity`} is set to 4 or greater. * * ```ts - * step.debug('Log this content when verbosity is >= 4'); + * const step = logger.createStep('My step'); + * step.debug('This message will be recorded and written out as an "DBG" labeled message'); + * step.end(); * ``` * * If a function with zero arguments is passed, the function will be executed before writing. This is helpful for avoiding extra work in the event that the verbosity is not actually high enough to render the logged debug information: * * ```ts - * step.debug(() => bigArray.map((item) => item.name)); + * step.debug(() => bigArray.map((item) => `- ${item.name}`).join('\n')); * ``` * + * @param contents Any value may be logged as a debug message, but will be stringified upon output. If a function is given with no arguments, the function will be executed and its response will be stringified for output. + * * @group Logging - * @param contents Any value that can be converted to a string for writing to `stderr`. */ debug(contents: unknown) { - this.#onMessage('debug'); - if (this.verbosity >= 4) { - this.#writeStream(this.#prefix(prefix.DBG, stringify(contents))); - } + this.#write('debug', contents); } /** - * Log timing information between two [Node.js performance mark names](https://nodejs.org/dist/latest-v18.x/docs/api/perf_hooks.html#performancemarkname-options). + * Log extra performance timing information. + * + * Timing information will only be written to the program output if the {@link Logger.verbosity | `verbosity`} is set to 5. + * + * ```ts + * const myStep = logger.createStep('My step'); + * performance.mark('start'); + * // do work + * performance.mark('end'); + * myStep.timing('start', 'end'); + * myStep.end(); + * ``` * * @group Logging - * @param start A `PerformanceMark` entry name - * @param end A `PerformanceMark` entry name */ timing(start: string, end: string) { - if (this.verbosity >= 5) { - const [startMark] = performance.getEntriesByName(start); - const [endMark] = performance.getEntriesByName(end); - if (!startMark || !endMark) { - this.warn(`Unable to log timing. Missing either mark ${start} → ${end}`); - return; - } - this.#writeStream( - this.#prefix(prefix.TIMER, `${start} → ${end}: ${Math.round(endMark.startTime - startMark.startTime)}ms`), - ); + const [startMark] = performance.getEntriesByName(start); + const [endMark] = performance.getEntriesByName(end); + if (!startMark || !endMark) { + this.warn(`Unable to log timing. Missing either mark ${start} → ${end}`); + return; } + this.#write( + 'timing', + `${startMark.name} → ${endMark.name}: ${Math.round(endMark.startTime - startMark.startTime)}ms`, + ); } - #writeStream(line: string) { - this.#buffer.write(ensureNewline(line)); - if (this.#active) { - const lines = line.split('\n'); - const lastThree = lines.slice(-3); - this.#lastThree.push(...lastThree.map(pc.dim)); - this.#lastThree.splice(0, this.#lastThree.length - 3); + /** + * Signal the end of this step. After this method is called, it will no longer accept any more logs of any variety and will be removed from the parent Logger's queue. + * + * Failure to call this method will result in a warning and potentially fail oneRepo commands. It is important to ensure that each step is cleanly ended before returning from commands. + * + * ```ts + * const myStep = logger.createStep('My step'); + * // do work + * myStep.end(); + * ``` + */ + end(callback?: () => void) { + // Makes calling `.end()` multiple times safe. + // TODO: make this unnecessary + if (this.writableEnded) { + throw new Error(`Called step.end() multiple times on step "${this.name}"`); } - } - #prefixStart(output: string) { - return ` ${this.name ? '┌' : prefix.START} ${output}`; - } - - #prefix(prefix: string, output: string) { - return output - .split('\n') - .map((line) => ` ${this.name ? '│' : ''}${this.#writePrefixes ? ` ${prefix} ` : ''}${line}`) - .join('\n'); - } - - #prefixEnd(output: string) { - return ` ${this.name ? '└' : prefix.END} ${output}`; - } -} - -function stringify(item: unknown): string { - if (typeof item === 'string') { - return item.replace(/^\n+/, '').replace(/\n*$/g, ''); - } - - if ( - Array.isArray(item) || - (typeof item === 'object' && item !== null && item.constructor === Object) || - item === null - ) { - return JSON.stringify(item, null, 2); - } - - if (item instanceof Date) { - return item.toISOString(); - } - - if (typeof item === 'function' && item.length === 0) { - return stringify(item()); - } + const endMark = performance.mark(`onerepo_end_${this.#startMark}`); + const [startMark] = performance.getEntriesByName(`onerepo_start_${this.#startMark}`); - return `${String(item)}`; -} + // TODO: jest.useFakeTimers does not seem to be applying to performance correctly + const duration = + !startMark || process.env.NODE_ENV === 'test' ? 0 : Math.round(endMark.startTime - startMark.startTime); + const contents = this.name + ? pc.dim(`${duration}ms`) + : `Completed${this.hasError ? ' with errors' : ''} ${pc.dim(`${duration}ms`)}`; -function ensureNewline(str: string): string { - if (/^\S*$/.test(str)) { - return ''; + return super.end( + { + type: 'end', + contents: stringify(contents), + group: this.name, + hasError: this.#hasError, + verbosity: this.verbosity, + } satisfies LoggedBuffer, + callback, + ); } - return str.endsWith('\n') ? str : str.replace(/\n*$/g, '\n'); } diff --git a/modules/logger/src/Logger.ts b/modules/logger/src/Logger.ts index e8567a26..a2400251 100644 --- a/modules/logger/src/Logger.ts +++ b/modules/logger/src/Logger.ts @@ -1,26 +1,11 @@ -import type { Writable } from 'node:stream'; -import { createLogUpdate } from 'log-update'; -import type logUpdate from 'log-update'; -import { LogStep } from './LogStep'; +import type { Duplex, Transform, Writable } from 'node:stream'; +import { randomUUID } from 'node:crypto'; import { destroyCurrent, setCurrent } from './global'; - -type LogUpdate = typeof logUpdate; - -/** - * Control the verbosity of the log output - * - * | Value | What | Description | - * | ------ | -------------- | ------------------------------------------------ | - * | `<= 0` | Silent | No output will be read or written. | - * | `>= 1` | Error, Info | | - * | `>= 2` | Warnings | | - * | `>= 3` | Log | | - * | `>= 4` | Debug | `logger.debug()` will be included | - * | `>= 5` | Timing | Extra performance timing metrics will be written | - * - * @group Logger - */ -export type Verbosity = 0 | 1 | 2 | 3 | 4 | 5; +import { LogStep } from './LogStep'; +import { LogStepToString } from './transforms/LogStepToString'; +import { LogProgress } from './transforms/LogProgress'; +import { hideCursor, showCursor } from './utils/cursor'; +import type { Verbosity } from './types'; /** * @group Logger @@ -33,11 +18,13 @@ export type LoggerOptions = { /** * Advanced – override the writable stream in order to pipe logs elsewhere. Mostly used for dependency injection for `@onerepo/test-cli`. */ - stream?: Writable; + stream?: Writable | LogStep; + /** + * @experimental + */ + captureAll?: boolean; }; -const frames: Array = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; - /** * The oneRepo logger helps build commands and capture output from spawned subprocess in a way that's both delightful to the end user and includes easy to scan and follow output. * @@ -53,43 +40,37 @@ export class Logger { #defaultLogger: LogStep; #steps: Array = []; #verbosity: Verbosity = 0; - #updater: LogUpdate; - #frame = 0; - #stream: Writable; - - #paused = false; - #updaterTimeout: NodeJS.Timeout | undefined; + #stream: Writable | Duplex | Transform | LogStep; #hasError = false; #hasWarning = false; #hasInfo = false; #hasLog = false; + #captureAll = false; + + id: string; + /** * @internal */ constructor(options: LoggerOptions) { - this.verbosity = options.verbosity; - + this.#defaultLogger = new LogStep({ name: '', verbosity: options.verbosity }); this.#stream = options.stream ?? process.stderr; - this.#updater = createLogUpdate(this.#stream); - - this.#defaultLogger = new LogStep('', { - onEnd: this.#onEnd, - onMessage: this.#onMessage, - verbosity: this.verbosity, - stream: this.#stream, - }); - - if (this.#stream === process.stderr && process.stderr.isTTY && process.env.NODE_ENV !== 'test') { - process.nextTick(() => { - this.#runUpdater(); - }); - } + this.#captureAll = !!options.captureAll; + this.verbosity = options.verbosity; + this.id = randomUUID(); setCurrent(this); } + /** + * @experimental + */ + get captureAll() { + return this.#captureAll; + } + /** * Get the logger's verbosity level */ @@ -98,14 +79,14 @@ export class Logger { } /** - * Recursively applies the new verbosity to the logger and all of its active steps. + * Applies the new verbosity to the main logger and any future steps. */ set verbosity(value: Verbosity) { this.#verbosity = Math.max(0, value) as Verbosity; if (this.#defaultLogger) { this.#defaultLogger.verbosity = this.#verbosity; - this.#defaultLogger.activate(true); + // this.#activate(this.#defaultLogger); } this.#steps.forEach((step) => (step.verbosity = this.#verbosity)); @@ -119,37 +100,42 @@ export class Logger { * Whether or not an error has been sent to the logger or any of its steps. This is not necessarily indicative of uncaught thrown errors, but solely on whether `.error()` has been called in the `Logger` or any `Step` instance. */ get hasError() { - return this.#hasError; + return this.#defaultLogger.hasError; } /** * Whether or not a warning has been sent to the logger or any of its steps. */ get hasWarning() { - return this.#hasWarning; + return this.#defaultLogger.hasWarning; } /** * Whether or not an info message has been sent to the logger or any of its steps. */ get hasInfo() { - return this.#hasInfo; + return this.#defaultLogger.hasInfo; } /** * Whether or not a log message has been sent to the logger or any of its steps. */ get hasLog() { - return this.#hasLog; + return this.#defaultLogger.hasLog; } /** * @internal */ - set stream(stream: Writable) { + set stream(stream: Writable | LogStep) { this.#stream = stream; - this.#updater.clear(); - this.#updater = createLogUpdate(this.#stream); + } + + /** + * @internal + */ + get stream() { + return this.#stream; } /** @@ -163,41 +149,17 @@ export class Logger { * logger.unpause(); * ``` */ - pause(write: boolean = true) { - this.#paused = true; - clearTimeout(this.#updaterTimeout); - if (write) { - this.#writeSteps(); - } + pause() { + this.#stream.cork(); + showCursor(); } /** - * Unpause the logger and resume writing buffered logs to `stderr`. See {@link Logger#pause | `logger.pause()`} for more information. + * Unpause the logger and uncork writing buffered logs to the output stream. See {@link Logger#pause | `logger.pause()`} for more information. */ unpause() { - this.#updater.clear(); - this.#paused = false; - this.#runUpdater(); - } - - #runUpdater() { - if (this.#paused) { - return; - } - this.#updaterTimeout = setTimeout(() => { - this.#writeSteps(); - this.#frame += 1; - this.#runUpdater(); - }, 80); - } - - #writeSteps() { - if (process.env.NODE_ENV === 'test' || this.verbosity <= 0) { - return; - } - this.#updater( - this.#steps.map((step) => [...step.status, ` └ ${frames[this.#frame % frames.length]}`].join('\n')).join('\n'), - ); + this.#stream.uncork(); + hideCursor(); } /** @@ -211,19 +173,67 @@ export class Logger { * * @param name The name to be written and wrapped around any output logged to this new step. */ - createStep(name: string, { writePrefixes }: { writePrefixes?: boolean } = {}) { - const step = new LogStep(name, { - onEnd: this.#onEnd, - onMessage: this.#onMessage, - verbosity: this.verbosity, - stream: this.#stream, - writePrefixes, - }); + createStep( + name: string, + opts: { + /** + * Optionally include extra information for performance tracing on this step. This description will be passed through to the [`performanceMark.detail`](https://nodejs.org/docs/latest-v20.x/api/perf_hooks.html#performancemarkdetail) recorded internally for this step. + * + * Use a [Performance Writer plugin](https://onerepo.tools/plugins/performance-writer/) to read and work with this detail. + */ + description?: string; + /** + * Override the default logger verbosity. Any changes while this step is running to the default logger will result in this step’s verbosity changing as well. + */ + verbosity?: Verbosity; + /** + * @deprecated This option no longer does anything and will be removed in v2.0.0 + */ + writePrefixes?: boolean; + } = {}, + ) { + const step = new LogStep({ name, verbosity: opts.verbosity ?? this.#verbosity, description: opts.description }); this.#steps.push(step); + step.on('end', () => this.#onEnd(step)); + this.#activate(step); return step; } + /** + * Write directly to the Logger's output stream, bypassing any formatting and verbosity filtering. + * + * :::caution[Advanced] + * Since {@link LogStep} implements a [Node.js duplex stream](https://nodejs.org/docs/latest-v20.x/api/stream.html#class-streamduplex), it is possible to use internal `write`, `read`, `pipe`, and all other available methods, but may not be fully recommended. + * ::: + * + * @group Logging + * @see {@link LogStep.write | `LogStep.write`}. + */ + write( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + chunk: any, + encoding?: BufferEncoding, + cb?: (error: Error | null | undefined) => void, + ): boolean; + + /** + * @internal + */ + write( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + chunk: any, + cb?: (error: Error | null | undefined) => void, + ): boolean; + + write( + // @ts-expect-error + ...args + ) { + // @ts-expect-error + return super.write(...args); + } + /** * Should be used to convey information or instructions through the log, will log when verbositu >= 1 * @@ -234,15 +244,15 @@ export class Logger { * If a function with zero arguments is passed, the function will be executed before writing. This is helpful for avoiding extra work in the event that the verbosity is not actually high enough to render the logged information: * * ```ts - * logger.info(() => bigArray.map((item) => item.name)); + * logger.info(() => bigArray.map((item) => `- ${item.name}`).join('\n')); * ``` * - * * @group Logging - * @param contents Any value that can be converted to a string for writing to `stderr`. + * @param contents Any value that can be converted to a string for writing to `stderr`. If a function is given with no arguments, the function will be executed and its response will be stringified for output. * @see {@link LogStep#info | `info()`} This is a pass-through for the main step’s {@link LogStep#info | `info()`} method. */ info(contents: unknown) { + this.#hasInfo = true; this.#defaultLogger.info(contents); } @@ -256,14 +266,15 @@ export class Logger { * If a function with zero arguments is passed, the function will be executed before writing. This is helpful for avoiding extra work in the event that the verbosity is not actually high enough to render the logged error: * * ```ts - * logger.error(() => bigArray.map((item) => item.name)); + * logger.error(() => bigArray.map((item) => `- ${item.name}`).join('\n')); * ``` * * @group Logging - * @param contents Any value that can be converted to a string for writing to `stderr`. + * @param contents Any value that can be converted to a string for writing to `stderr`. If a function is given with no arguments, the function will be executed and its response will be stringified for output. * @see {@link LogStep#error | `error()`} This is a pass-through for the main step’s {@link LogStep#error | `error()`} method. */ error(contents: unknown) { + this.#hasError = true; this.#defaultLogger.error(contents); } @@ -277,14 +288,15 @@ export class Logger { * If a function with zero arguments is passed, the function will be executed before writing. This is helpful for avoiding extra work in the event that the verbosity is not actually high enough to render the logged warning: * * ```ts - * logger.warn(() => bigArray.map((item) => item.name)); + * logger.warn(() => bigArray.map((item) => `- ${item.name}`).join('\n')); * ``` * * @group Logging - * @param contents Any value that can be converted to a string for writing to `stderr`. + * @param contents Any value that can be converted to a string for writing to `stderr`. If a function is given with no arguments, the function will be executed and its response will be stringified for output. * @see {@link LogStep#warn | `warn()`} This is a pass-through for the main step’s {@link LogStep#warn | `warn()`} method. */ warn(contents: unknown) { + this.#hasWarning = true; this.#defaultLogger.warn(contents); } @@ -298,14 +310,15 @@ export class Logger { * If a function with zero arguments is passed, the function will be executed before writing. This is helpful for avoiding extra work in the event that the verbosity is not actually high enough to render the logged information: * * ```ts - * logger.log(() => bigArray.map((item) => item.name)); + * logger.log(() => bigArray.map((item) => `- ${item.name}`).join('\n')); * ``` * * @group Logging - * @param contents Any value that can be converted to a string for writing to `stderr`. + * @param contents Any value that can be converted to a string for writing to `stderr`. If a function is given with no arguments, the function will be executed and its response will be stringified for output. * @see {@link LogStep#log | `log()`} This is a pass-through for the main step’s {@link LogStep#log | `log()`} method. */ log(contents: unknown) { + this.#hasLog = true; this.#defaultLogger.log(contents); } @@ -319,11 +332,11 @@ export class Logger { * If a function with zero arguments is passed, the function will be executed before writing. This is helpful for avoiding extra work in the event that the verbosity is not actually high enough to render the logged debug information: * * ```ts - * logger.debug(() => bigArray.map((item) => item.name)); + * logger.debug(() => bigArray.map((item) => `- ${item.name}`).join('\n')); * ``` * * @group Logging - * @param contents Any value that can be converted to a string for writing to `stderr`. + * @param contents Any value that can be converted to a string for writing to `stderr`. If a function is given with no arguments, the function will be executed and its response will be stringified for output. * @see {@link LogStep#debug | `debug()`} This is a pass-through for the main step’s {@link LogStep#debug | `debug()`} method. */ debug(contents: unknown) { @@ -342,47 +355,74 @@ export class Logger { this.#defaultLogger.timing(start, end); } + async waitForClear() { + return await new Promise((resolve) => { + setImmediate(() => { + resolve(this.#steps.length === 0); + }); + }); + } + /** * @internal */ async end() { - this.pause(false); - clearTimeout(this.#updaterTimeout); - - for (const step of this.#steps) { - this.#activate(step); - step.warn( - 'Step did not finish before command shutdown. Fix this issue by updating this command to `await step.end();` at the appropriate time.', - ); - await step.end(); + this.unpause(); + + const now = Date.now(); + while ((await this.waitForClear()) === false) { + if (Date.now() - now > 100) { + const openStep = this.#steps[0]; + if (openStep) { + openStep.error( + 'Did not complete before command shutdown. Fix this issue by updating this command to call `step.end();` at the appropriate time.', + ); + openStep.end(); + } + } + continue; } - await this.#defaultLogger.end(); - await this.#defaultLogger.flush(); + this.#activate(this.#defaultLogger); + this.#defaultLogger.uncork(); + await new Promise((resolve) => { + this.#defaultLogger.end(() => { + resolve(); + }); + }); + + this.#defaultLogger.unpipe(); + destroyCurrent(); + showCursor(); } #activate(step: LogStep) { - const activeStep = this.#steps.find((step) => step.active); - if (activeStep) { + const activeStep = this.#steps.find((step) => step.isPiped); + + if (activeStep || step.isPiped) { return; } - if (step !== this.#defaultLogger && this.#defaultLogger.active) { - this.#defaultLogger.deactivate(); - } - if (!(this.#stream === process.stderr && process.stderr.isTTY)) { - step.activate(); - return; + if (step !== this.#defaultLogger && !this.#defaultLogger.isPaused()) { + this.#defaultLogger.cork(); } - setImmediate(() => { - step.activate(); - }); + hideCursor(); + + if (!step.name || !(this.#stream as typeof process.stderr).isTTY) { + step.pipe(new LogStepToString()).pipe(this.#stream as Writable); + } else { + step + .pipe(new LogStepToString()) + .pipe(new LogProgress()) + .pipe(this.#stream as Writable); + } + step.isPiped = true; } #onEnd = async (step: LogStep) => { - if (step === this.#defaultLogger) { + if (step === this.#defaultLogger || !step.isPiped) { return; } @@ -391,45 +431,41 @@ export class Logger { return; } - this.#updater.clear(); - await step.flush(); + this.#setState(step); + + step.unpipe(); + step.isPiped = false; + step.destroy(); - this.#defaultLogger.activate(true); + this.#defaultLogger.uncork(); if (step.hasError && process.env.GITHUB_RUN_ID) { this.error('The previous step has errors.'); } + // Remove this step + this.#steps.splice(index, 1); + + if (this.#steps.length < 1) { + return; + } + await new Promise((resolve) => { + // setTimeout(() => { setImmediate(() => { - setImmediate(() => { - this.#defaultLogger.deactivate(); - resolve(); - }); + this.#defaultLogger.cork(); + resolve(); }); + // }, 60); }); - this.#steps.splice(index, 1); - this.#activate(this.#steps[0] ?? this.#defaultLogger); + this.#activate(this.#steps[0]); }; - #onMessage = (type: 'error' | 'warn' | 'info' | 'log' | 'debug') => { - switch (type) { - case 'error': - this.#hasError = true; - this.#defaultLogger.hasError = true; - break; - case 'warn': - this.#hasWarning = true; - break; - case 'info': - this.#hasInfo = true; - break; - case 'log': - this.#hasLog = true; - break; - default: - // no default - } + #setState = (step: LogStep) => { + this.#defaultLogger.hasError = step.hasError || this.#defaultLogger.hasError; + this.#defaultLogger.hasWarning = step.hasWarning || this.#defaultLogger.hasWarning; + this.#defaultLogger.hasInfo = step.hasInfo || this.#defaultLogger.hasInfo; + this.#defaultLogger.hasLog = step.hasLog || this.#defaultLogger.hasLog; }; } diff --git a/modules/logger/src/__tests__/LogStep.test.ts b/modules/logger/src/__tests__/LogStep.test.ts index 908c3347..f9e6bdb7 100644 --- a/modules/logger/src/__tests__/LogStep.test.ts +++ b/modules/logger/src/__tests__/LogStep.test.ts @@ -1,79 +1,45 @@ import { PassThrough } from 'node:stream'; -import pc from 'picocolors'; import { LogStep } from '../LogStep'; +import type { Verbosity } from '../types'; -describe('LogStep', () => { - let runId: string | undefined; - - beforeEach(() => { - runId = process.env.GITHUB_RUN_ID; - delete process.env.GITHUB_RUN_ID; +const parser = (out: Array) => { + const stream = new PassThrough(); + stream.on('data', (chunk) => { + out.push(JSON.parse(chunk.toString())); }); - - afterEach(() => { - process.env.GITHUB_RUN_ID = runId; + return stream; +}; + +const waitAtick = () => + new Promise((resolve) => { + setImmediate(() => { + setImmediate(() => { + resolve(); + }); + }); }); +describe('LogStep', () => { test('setup', async () => { - const onEnd = vi.fn(); - const step = new LogStep('tacos', { onEnd, verbosity: 3, onMessage: () => {} }); + const step = new LogStep({ name: 'tacos', verbosity: 3 }); expect(step.name).toBe('tacos'); - expect(step.verbosity).toBe(3); - expect(step.active).toBe(false); - expect(step.status).toEqual([' ┌ tacos']); - }); - - test('can be activated', async () => { - const onEnd = vi.fn(); - const step = new LogStep('tacos', { onEnd, verbosity: 3, onMessage: () => {} }); - step.activate(); - - expect(step.active).toBe(true); - }); - - test('writes group & endgroup when GITHUB_RUN_ID is set', async () => { - process.env.GITHUB_RUN_ID = 'yes'; - const onEnd = vi.fn(() => Promise.resolve()); - const stream = new PassThrough(); - const step = new LogStep('tacos', { onEnd, verbosity: 4, stream, onMessage: () => {} }); - - let out = ''; - stream.on('data', (chunk) => { - out += chunk.toString(); - }); - step.activate(); - - step.log('hello'); - await step.end(); - await step.flush(); - - expect(out).toMatch(/^::group::tacos\n/); - expect(out).toMatch(/::endgroup::\n$/); + expect(step.isPiped).toBe(false); }); - test('when activated, flushes its logs to the stream', async () => { - vi.restoreAllMocks(); - const onEnd = vi.fn(() => Promise.resolve()); - const stream = new PassThrough(); - const step = new LogStep('tacos', { onEnd, verbosity: 3, stream, onMessage: () => {} }); + test('can be piped', async () => { + const out: Array = []; + const step = new LogStep({ name: 'tacos', verbosity: 3 }); + const stream = parser(out); + step.pipe(stream); + step.end(); - let out = ''; - stream.on('data', (chunk) => { - out += chunk.toString(); - }); - - step.log('hellooooo'); - step.activate(); - await step.end(); - await step.flush(); + await waitAtick(); - expect(out).toEqual( - ` ┌ tacos - │ ${pc.cyan(pc.bold('LOG'))} hellooooo - └ ${pc.green('✔')} ${pc.dim('0ms')} -`, - ); + expect(out).toEqual([ + { type: 'start', contents: 'tacos', group: 'tacos', verbosity: 3 }, + { type: 'end', contents: expect.stringContaining('ms'), group: 'tacos', hasError: false, verbosity: 3 }, + ]); }); test.concurrent.each([ @@ -83,26 +49,20 @@ describe('LogStep', () => { [3, ['info', 'error', 'warn', 'log']], [4, ['info', 'error', 'warn', 'log', 'debug']], [5, ['info', 'error', 'warn', 'log', 'debug', 'timing']], - ] as Array<[number, Array]>)('verbosity = %d writes %j', async (verbosity, methods) => { - const onEnd = vi.fn(() => Promise.resolve()); - const stream = new PassThrough(); - const step = new LogStep('tacos', { onEnd, verbosity, stream, onMessage: () => {} }); - + ] as Array<[Verbosity, Array]>)('verbosity = %d writes %j', async (verbosity, methods) => { const logs = { - info: `${pc.blue(pc.bold('INFO'))} some information`, - error: ` │ ${pc.red(pc.bold('ERR'))} an error`, - warn: ` │ ${pc.yellow(pc.bold('WRN'))} a warning`, - log: ` │ ${pc.cyan(pc.bold('LOG'))} a log`, - debug: ` │ ${pc.magenta(pc.bold('DBG'))} a debug`, - timing: ` │ ${pc.red('⏳')} foo → bar: 0ms`, + info: { type: 'info', contents: 'some information', verbosity, group: 'tacos' }, + error: { type: 'error', contents: 'an error', verbosity, group: 'tacos' }, + warn: { type: 'warn', contents: 'a warning', verbosity, group: 'tacos' }, + log: { type: 'log', contents: 'a log', verbosity, group: 'tacos' }, + debug: { type: 'debug', contents: 'a debug', verbosity, group: 'tacos' }, + timing: { type: 'timing', contents: 'foo → bar: 0ms', verbosity, group: 'tacos' }, }; - let out = ''; - stream.on('data', (chunk) => { - out += chunk.toString(); - }); - - step.activate(); + const out: Array = []; + const stream = parser(out); + const step = new LogStep({ name: 'tacos', verbosity }); + step.pipe(stream); step.info('some information'); step.error('an error'); @@ -113,21 +73,16 @@ describe('LogStep', () => { performance.mark('bar'); step.timing('foo', 'bar'); - await step.end(); - await step.flush(); + step.end(); - // Some funky stuff happening here - // @ts-ignore - if (verbosity === 0) { - stream.end(); - } + await waitAtick(); for (const [method, str] of Object.entries(logs)) { // @ts-ignore if (!methods.includes(method)) { - expect(out).not.toMatch(str); + expect(out).not.toEqual(expect.arrayContaining(['asdf'])); } else { - expect(out).toMatch(str); + expect(out).toEqual(expect.arrayContaining([str])); } } }); @@ -138,83 +93,49 @@ describe('LogStep', () => { function foo(asdf: unknown) { return asdf; }, - ` │ ${pc.cyan(pc.bold('LOG'))} function foo(asdf) {`, + [{ type: 'log', contents: expect.stringContaining('function foo(asdf) {'), verbosity: 3, group: 'tacos' }], ], [ 'function with zero arguments are executed', function foo() { return 'tacos'; }, - ` │ ${pc.cyan(pc.bold('LOG'))} tacos`, + [{ type: 'log', contents: 'tacos', verbosity: 3, group: 'tacos' }], ], [ 'object', { foo: 'bar' }, - ` │ ${pc.cyan(pc.bold('LOG'))} { - │ ${pc.cyan(pc.bold('LOG'))} "foo": "bar" - │ ${pc.cyan(pc.bold('LOG'))} }`, + [{ type: 'log', contents: JSON.stringify({ foo: 'bar' }, null, 2), verbosity: 3, group: 'tacos' }], ], [ 'array', ['foo', true], - ` │ ${pc.cyan(pc.bold('LOG'))} [ - │ ${pc.cyan(pc.bold('LOG'))} "foo", - │ ${pc.cyan(pc.bold('LOG'))} true - │ ${pc.cyan(pc.bold('LOG'))} ]`, + [{ type: 'log', contents: JSON.stringify(['foo', true], null, 2), verbosity: 3, group: 'tacos' }], + ], + [ + 'date', + new Date('2023-03-11'), + [{ type: 'log', contents: '2023-03-11T00:00:00.000Z', verbosity: 3, group: 'tacos' }], ], - ['date', new Date('2023-03-11'), ` │ ${pc.cyan(pc.bold('LOG'))} 2023-03-11T00:00:00.000Z`], ])('can stringify %s', async (name, obj, exp) => { - const onEnd = vi.fn(() => Promise.resolve()); - const stream = new PassThrough(); - const step = new LogStep('tacos', { onEnd, verbosity: 3, stream, onMessage: () => {} }); - - let out = ''; - stream.on('data', (chunk) => { - out += chunk.toString(); - }); + const out: Array = []; + const stream = parser(out); + const step = new LogStep({ name: 'tacos', verbosity: 3 }); + step.pipe(stream); step.log(obj); - step.activate(); - await step.end(); - await step.flush(); - - expect(out).toMatch(exp); - }); - - test('can omit prefixes', async () => { - const onEnd = vi.fn(() => Promise.resolve()); - const stream = new PassThrough(); - const step = new LogStep('tacos', { onEnd, verbosity: 4, stream, writePrefixes: false, onMessage: () => {} }); + step.end(); - let out = ''; - stream.on('data', (chunk) => { - out += chunk.toString(); - }); + await waitAtick(); - step.error('error'); - step.warn('warn'); - step.info('info'); - step.log('log'); - step.debug('debug'); - step.activate(); - await step.end(); - await step.flush(); - - expect(out).toEqual(` ┌ tacos - │error - │warn - │info - │log - │debug - └ ${pc.red('✘')} ${pc.dim('0ms')} -`); + expect(out).toEqual(expect.arrayContaining(exp)); }); test('sets hasError/etc when messages are added', async () => { - const onEnd = vi.fn(() => Promise.resolve()); - const stream = new PassThrough(); - const step = new LogStep('tacos', { onEnd, verbosity: 4, stream, onMessage: () => {} }); - step.activate(); + const out: Array = []; + const stream = parser(out); + const step = new LogStep({ name: 'tacos', verbosity: 4 }); + step.pipe(stream); expect(step.hasError).toBe(false); expect(step.hasWarning).toBe(false); @@ -233,36 +154,7 @@ describe('LogStep', () => { step.log('foo'); expect(step.hasLog).toBe(true); - await step.end(); - await step.flush(); - stream.destroy(); - }); - - test('calls onMessage as messages are added', async () => { - const onMessage = vi.fn(); - const stream = new PassThrough(); - const step = new LogStep('tacos', { onEnd: () => Promise.resolve(), verbosity: 4, stream, onMessage }); - step.activate(); - - expect(onMessage).not.toHaveBeenCalled(); - - step.error('foo'); - expect(onMessage).toHaveBeenCalledWith('error'); - - step.warn('foo'); - expect(onMessage).toHaveBeenCalledWith('warn'); - - step.info('foo'); - expect(onMessage).toHaveBeenCalledWith('info'); - - step.log('foo'); - expect(onMessage).toHaveBeenCalledWith('log'); - - step.debug('foo'); - expect(onMessage).toHaveBeenCalledWith('debug'); - - await step.end(); - await step.flush(); + step.end(); stream.destroy(); }); }); diff --git a/modules/logger/src/__tests__/Logger.test.ts b/modules/logger/src/__tests__/Logger.test.ts index b7213417..16ac75c1 100644 --- a/modules/logger/src/__tests__/Logger.test.ts +++ b/modules/logger/src/__tests__/Logger.test.ts @@ -43,13 +43,12 @@ describe('Logger', () => { const logger = new Logger({ verbosity, stream }); const logs = { - info: `${pc.blue(pc.bold('INFO'))} some information`, - error: `${pc.red(pc.bold('ERR'))} an error`, - warn: `${pc.yellow(pc.bold('WRN'))} a warning`, - // log: `${pc.cyan(pc.bold('LOG'))} a log`, - log: ' a log', - debug: `${pc.magenta(pc.bold('DBG'))} a debug`, - timing: `${pc.red('⏳')} foo → bar: 0ms`, + info: ` ${pc.blue(pc.bold('INFO '))} some information`, + error: ` ${pc.red(pc.bold('ERR '))} an error`, + warn: ` ${pc.yellow(pc.bold('WRN '))} a warning`, + log: ` ${pc.cyan(pc.bold('LOG '))} a log`, + debug: ` ${pc.magenta(pc.bold('DBG '))} a debug`, + timing: ` ${pc.red('⏳')} foo → bar: 0ms`, }; logger.info('some information'); @@ -75,8 +74,8 @@ describe('Logger', () => { ); test('logs "completed" message', async () => { - const stream = new PassThrough(); let out = ''; + const stream = new PassThrough(); stream.on('data', (chunk) => { out += chunk.toString(); }); @@ -84,11 +83,12 @@ describe('Logger', () => { const logger = new Logger({ verbosity: 2, stream }); const step = logger.createStep('tacos'); - await step.end(); + step.end(); await logger.end(); + await runPendingImmediates(); - expect(out).toMatch(`${pc.dim(pc.bold('■'))} ${pc.green('✔')} Completed`); + expect(out).toEqual(`${pc.dim(pc.bold('■'))} ${pc.green('✔')} ${pc.dim('0ms')}`); }); test('logs "completed with errors" message', async () => { @@ -102,11 +102,12 @@ describe('Logger', () => { const step = logger.createStep('tacos'); step.error('foo'); - await step.end(); + step.end(); await logger.end(); + await runPendingImmediates(); - expect(out).toMatch(`${pc.dim(pc.bold('■'))} ${pc.red('✘')} Completed with errors`); + expect(out).toEqual(`${pc.dim(pc.bold('■'))} ${pc.red('✘')} Completed with errors`); }); test('writes logs if verbosity increased after construction', async () => { @@ -122,7 +123,7 @@ describe('Logger', () => { logger.warn('this is a warning'); await runPendingImmediates(); const step = logger.createStep('tacos'); - await step.end(); + step.end(); await logger.end(); @@ -151,7 +152,7 @@ describe('Logger', () => { expect(out).not.toMatch('::endgroup::'); }); - test.concurrent.each([ + test.each([ ['error', 'hasError'], ['warn', 'hasWarning'], ['info', 'hasInfo'], @@ -163,18 +164,21 @@ describe('Logger', () => { const logger = new Logger({ verbosity: 2, stream }); const step = logger.createStep('tacos'); - await step.end(); + step.end(); expect(logger[getter]).toBe(false); - const step2 = logger.createStep('burritos'); + const step2 = logger.createStep(`${method.toString()} ${getter}`); // @ts-ignore step2[method]('yum'); - await step2.end(); + step2.end(); + await logger.end(); + await runPendingImmediates(); + // @ts-ignore + expect(step2[getter]).toBe(true); expect(logger[getter]).toBe(true); - await logger.end(); stream.destroy(); }, ); diff --git a/modules/logger/src/global.ts b/modules/logger/src/global.ts index 2bba8578..0af5a456 100644 --- a/modules/logger/src/global.ts +++ b/modules/logger/src/global.ts @@ -1,15 +1,9 @@ import type { Logger } from './Logger'; -const sym = Symbol.for('onerepo_loggers'); +const loggers: Array = []; function getLoggers(): Array { - // @ts-ignore Cannot type symbol as key on global - if (!global[sym]) { - // @ts-ignore - global[sym] = []; - } - // @ts-ignore - return global[sym]; + return loggers; } export function getCurrent() { diff --git a/modules/logger/src/index.ts b/modules/logger/src/index.ts index 1f96fcbc..50eadc25 100644 --- a/modules/logger/src/index.ts +++ b/modules/logger/src/index.ts @@ -1,10 +1,14 @@ +import type { Writable, TransformOptions } from 'node:stream'; +import { Transform } from 'node:stream'; +import restoreCursorDefault from 'restore-cursor'; import { Logger } from './Logger'; -import type { LogStep } from './LogStep'; import { destroyCurrent, getCurrent, setCurrent } from './global'; -import { LogBuffer } from './LogBuffer'; +import type { LogStep } from './LogStep'; +import { prefix } from './transforms/LogStepToString'; export * from './Logger'; export * from './LogStep'; +export * from './types'; /** * This gets the logger singleton for use across all of oneRepo and its commands. @@ -39,6 +43,7 @@ export function getLogger(opts: Partial[0]> */ export function destroyLogger() { destroyCurrent(); + restoreCursor(); } /** @@ -68,7 +73,7 @@ export async function stepWrapper( const out = await fn(step); if (!inputStep) { - await step.end(); + step.end(); } return out; @@ -91,35 +96,64 @@ export async function stepWrapper( * @group Logger */ export function bufferSubLogger(step: LogStep): { logger: Logger; end: () => Promise } { - const logger = getLogger(); - const buffer = new LogBuffer(); - const subLogger = new Logger({ verbosity: logger.verbosity, stream: buffer }); - function proxyChunks(chunk: Buffer) { - if (subLogger.hasError) { - step.error(() => chunk.toString().trimEnd()); - } else if (subLogger.hasInfo) { - step.info(() => chunk.toString().trimEnd()); - } else if (subLogger.hasWarning) { - step.warn(() => chunk.toString().trimEnd()); - } else if (subLogger.hasLog) { - step.log(() => chunk.toString().trimEnd()); - } else { - step.debug(() => chunk.toString().trimEnd()); - } - } - buffer.on('data', proxyChunks); + const stream = new Buffered() as Transform; + stream.pipe(step as Writable); + + const subLogger = new Logger({ verbosity: step.verbosity, stream, captureAll: true }); return { logger: subLogger, async end() { - buffer.off('data', proxyChunks); + // TODO: this promise/immediate may not be necessary await new Promise((resolve) => { setImmediate(async () => { - await subLogger.end(); - buffer.destroy(); - resolve(); + stream.unpipe(); + stream.destroy(); + if (subLogger.hasError) { + step.hasError = true; + } + return resolve(); }); }); }, }; } + +export const restoreCursor = restoreCursorDefault; + +class Buffered extends Transform { + constructor(opts?: TransformOptions) { + super(opts); + // We're going to be adding many listeners to this transform. This prevents warnings about _potential_ memory leaks + // However, we're (hopefully) managing piping, unpiping, and destroying the streams correctly in the Logger + this.setMaxListeners(0); + } + + // TODO: The buffered logger seems to have its `end()` method automatically called after the first step + // is ended. This is a complete mystery, but hacking it to not end saves it from prematurely closing + // before unpipe+destroy is called. + // @ts-expect-error + end() {} + + _transform( + chunk: Buffer, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + encoding = 'utf8', + callback: () => void, + ) { + this.push( + `${chunk + .toString() + .trim() + .split('\n') + .map((line) => `│ ${line.trim()}`) + .join('\n')}\n`, + ); + callback(); + } + + _final(callback: () => void) { + this.push(null); + callback(); + } +} diff --git a/modules/logger/src/transforms/LogProgress.ts b/modules/logger/src/transforms/LogProgress.ts new file mode 100644 index 00000000..51181df7 --- /dev/null +++ b/modules/logger/src/transforms/LogProgress.ts @@ -0,0 +1,51 @@ +import { Transform } from 'node:stream'; + +export class LogProgress extends Transform { + #updaterTimeout?: NodeJS.Timeout; + #frame: number; + #written: boolean = false; + + constructor() { + super(); + // this.#updater = createLogUpdate(this); + this.#frame = 0; + this.#runUpdater(); + } + + #runUpdater() { + clearTimeout(this.#updaterTimeout); + this.#updaterTimeout = setTimeout(() => { + // this.push(new Error().stack); + this.write(`└ ${frames[this.#frame % frames.length]}`); + this.#written = true; + this.#frame += 1; + // this.#runUpdater(); + }, 100); + } + + _destroy() { + clearTimeout(this.#updaterTimeout); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _transform(chunk: string | Buffer, encoding = 'utf8', callback: () => void) { + clearTimeout(this.#updaterTimeout); + if (this.#written) { + // Erase the last line + this.push('\u001B[2K\u001B[G'); + this.#written = false; + } + + this.push(chunk); + callback(); + this.#runUpdater(); + } + + _final(callback: () => void) { + clearTimeout(this.#updaterTimeout); + this.push(null); + callback(); + } +} + +export const frames: Array = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; diff --git a/modules/logger/src/transforms/LogStepToString.ts b/modules/logger/src/transforms/LogStepToString.ts new file mode 100644 index 00000000..adb9a54c --- /dev/null +++ b/modules/logger/src/transforms/LogStepToString.ts @@ -0,0 +1,70 @@ +import { Transform } from 'node:stream'; +import pc from 'picocolors'; +import type { LineType, LoggedBuffer } from '../types'; +import { ensureNewline, stringify } from '../utils/string'; + +export class LogStepToString extends Transform { + constructor() { + super({ decodeStrings: false, objectMode: true }); + } + + _transform( + chunk: LoggedBuffer | string | Buffer, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + encoding = 'utf8', + callback: () => void, + ) { + try { + if (typeof chunk === 'string' || Buffer.isBuffer(chunk)) { + this.push(chunk); + } else { + const data = chunk as LoggedBuffer; + if (typeMinVerbosity[data.type] <= data.verbosity) { + this.push(ensureNewline(`${this.#prefix(data.type, data.group, stringify(data.contents), data.hasError)}`)); + } + } + } catch { + this.push(JSON.stringify(chunk)); + } + callback(); + } + + _final(callback: () => void) { + this.push(null); + callback(); + } + + #prefix(type: LineType, group: string | undefined, output: string, hasError?: boolean) { + if (type === 'end') { + return `${!group ? pc.bold(pc.dim('◼︎ ')) : prefix[type]}${hasError ? pc.red('✘') : pc.green('✔')} ${output}`; + } + if (type === 'start') { + return `${!group ? '' : prefix[type]}${output}`; + } + return output + .split('\n') + .map((line) => `${group ? '│ ' : ''}${prefix[type]}${line}`) + .join('\n'); + } +} +const typeMinVerbosity: Record = { + start: 1, + end: 1, + error: 1, + info: 1, + warn: 2, + log: 3, + debug: 5, + timing: 6, +}; + +export const prefix: Record = { + timing: pc.red('⏳'), + start: '┌ ', + end: '└ ', + error: pc.red(pc.bold('ERR ')), + warn: pc.yellow(pc.bold('WRN ')), + log: pc.cyan(pc.bold('LOG ')), + debug: pc.magenta(pc.bold('DBG ')), + info: pc.blue(pc.bold('INFO ')), +}; diff --git a/modules/logger/src/types.ts b/modules/logger/src/types.ts new file mode 100644 index 00000000..5275a746 --- /dev/null +++ b/modules/logger/src/types.ts @@ -0,0 +1,31 @@ +/** + * Control the verbosity of the log output + * + * | Value | What | Description | + * | ------ | -------------- | ------------------------------------------------ | + * | `<= 0` | Silent | No output will be read or written. | + * | `>= 1` | Error, Info | | + * | `>= 2` | Warnings | | + * | `>= 3` | Log | | + * | `>= 4` | Debug | `logger.debug()` will be included | + * | `>= 5` | Timing | Extra performance timing metrics will be written | + * + * @group Logger + */ +export type Verbosity = 0 | 1 | 2 | 3 | 4 | 5; + +/** + * @internal + */ +export type LineType = 'start' | 'end' | 'error' | 'warn' | 'info' | 'log' | 'debug' | 'timing'; + +/** + * @internal + */ +export type LoggedBuffer = { + type: LineType; + contents: string; + group?: string; + hasError?: boolean; + verbosity: Verbosity; +}; diff --git a/modules/logger/src/utils/cursor.ts b/modules/logger/src/utils/cursor.ts new file mode 100644 index 00000000..266ea25f --- /dev/null +++ b/modules/logger/src/utils/cursor.ts @@ -0,0 +1,19 @@ +import restoreCursor from 'restore-cursor'; + +export function hideCursor() { + if (!process.stderr.isTTY) { + return; + } + + restoreCursor(); + + process.stderr.write('\u001B[?25l'); +} + +export function showCursor() { + if (!process.stderr.isTTY) { + return; + } + + process.stderr.write('\u001B[?25h'); +} diff --git a/modules/logger/src/utils/string.ts b/modules/logger/src/utils/string.ts new file mode 100644 index 00000000..3b4c3186 --- /dev/null +++ b/modules/logger/src/utils/string.ts @@ -0,0 +1,30 @@ +export function stringify(item: unknown): string { + if (typeof item === 'string') { + return item.replace(/^\n+/, '').replace(/\n*$/g, ''); + } + + if ( + Array.isArray(item) || + (typeof item === 'object' && item !== null && item.constructor === Object) || + item === null + ) { + return JSON.stringify(item, null, 2); + } + + if (item instanceof Date) { + return item.toISOString(); + } + + if (typeof item === 'function' && item.length === 0) { + return stringify(item()); + } + + return `${String(item)}`; +} + +export function ensureNewline(str: string): string { + if (/^\S*$/.test(str)) { + return ''; + } + return str.endsWith('\n') ? str : str.replace(/\n*$/g, '\n'); +} diff --git a/modules/onerepo/src/core/graph/__tests__/verify.test.ts b/modules/onerepo/src/core/graph/__tests__/verify.test.ts index 61b82aeb..f1699fc4 100644 --- a/modules/onerepo/src/core/graph/__tests__/verify.test.ts +++ b/modules/onerepo/src/core/graph/__tests__/verify.test.ts @@ -38,7 +38,7 @@ describe('verify', () => { const graph = getGraph(path.join(__dirname, '__fixtures__', 'repo')); const schema = require.resolve('./__fixtures__/tsconfig-schema.ts'); - await expect(run(`--custom-schema ${schema}`, { graph })).rejects.toMatch('must be equal to constant'); + await expect(run(`--custom-schema ${schema}`, { graph })).rejects.toEqual('must be equal to constant'); }); test('can verify js (eg jest.config, etc)', async () => { diff --git a/modules/onerepo/src/core/graph/verify.ts b/modules/onerepo/src/core/graph/verify.ts index 0ec73e3a..ac3f239d 100644 --- a/modules/onerepo/src/core/graph/verify.ts +++ b/modules/onerepo/src/core/graph/verify.ts @@ -9,6 +9,7 @@ import type { AnySchema } from 'ajv'; import ajvErrors from 'ajv-errors'; import type { Builder, Handler } from '@onerepo/yargs'; import type { Graph, Workspace } from '@onerepo/graph'; +import type { LogStep } from '@onerepo/logger'; import { verifyDependencies } from '../dependencies/utils/verify-dependencies'; import { epilogue } from '../dependencies/verify'; import { defaultValidators } from './schema'; @@ -87,7 +88,7 @@ export const handler: Handler = async function handler(argv, { graph, logg if (required && files.length === 0) { const msg = `❓ Missing required file matching pattern "${schemaKey.split(splitChar)[1]}"`; schemaStep.error(msg); - writeGithubError(msg); + writeGithubError(schemaStep, msg); } for (const file of files) { if (!(file in map)) { @@ -119,12 +120,12 @@ export const handler: Handler = async function handler(argv, { graph, logg schemaStep.error(`Errors in ${workspace.resolve(file)}:`); ajv.errors?.forEach((err) => { schemaStep.error(` ↳ ${err.message}`); - writeGithubError(err.message, graph.root.relative(workspace.resolve(file))); + writeGithubError(schemaStep, err.message, graph.root.relative(workspace.resolve(file))); }); } } } - await schemaStep.end(); + schemaStep.end(); } }; @@ -146,8 +147,8 @@ const splitChar = '\u200b'; type ExtendedSchema = AnySchema & { $required?: boolean }; -function writeGithubError(message: string | undefined, filename?: string) { +function writeGithubError(step: LogStep, message: string | undefined, filename?: string) { if (process.env.GITHUB_RUN_ID) { - process.stderr.write(`::error${filename ? ` file=${filename}` : ''}::${message}\n`); + step.write(`::error${filename ? ` file=${filename}` : ''}::${message}\n`); } } diff --git a/modules/onerepo/src/core/install/index.ts b/modules/onerepo/src/core/install/index.ts index afd850d4..09f943b5 100644 --- a/modules/onerepo/src/core/install/index.ts +++ b/modules/onerepo/src/core/install/index.ts @@ -31,9 +31,7 @@ export const install: Plugin = function install() { } const logger = getLogger(); - const oVerbosity = logger.verbosity; - logger.verbosity = 2; - const step = logger.createStep('Version mismatch detected!'); + const step = logger.createStep('Version mismatch detected!', { verbosity: 2 }); const bar = '⎯'.repeat(Math.min(process.stderr.columns, 70)); if ( @@ -61,8 +59,7 @@ ${bar} ${bar}`); } - await step.end(); - logger.verbosity = oVerbosity; + step.end(); }, }; }; diff --git a/modules/onerepo/src/core/tasks/__tests__/tasks.test.ts b/modules/onerepo/src/core/tasks/__tests__/tasks.test.ts index e5284c51..26c47d3d 100644 --- a/modules/onerepo/src/core/tasks/__tests__/tasks.test.ts +++ b/modules/onerepo/src/core/tasks/__tests__/tasks.test.ts @@ -36,7 +36,7 @@ describe('handler', () => { beforeEach(() => { out = ''; vi.spyOn(process.stdout, 'write').mockImplementation((content) => { - out += content.toString(); + out += (content as string).toString(); return true; }); vi.spyOn(subprocess, 'run').mockResolvedValue(['', '']); @@ -245,7 +245,7 @@ describe('handler', () => { args: ['"deployroot"'], cmd: 'echo', meta: { name: 'fixture-root', slug: 'fixture-root' }, - name: 'echo "deployroot" (fixture-root)', + name: 'echo "deployroot"', opts: { cwd: '.' }, }, ], @@ -284,7 +284,7 @@ describe('handler', () => { args: ['"deployroot"'], cmd: 'echo', meta: { name: 'fixture-root', slug: 'fixture-root' }, - name: 'echo "deployroot" (fixture-root)', + name: 'echo "deployroot"', opts: { cwd: '.' }, }, ], diff --git a/modules/onerepo/src/core/tasks/tasks.ts b/modules/onerepo/src/core/tasks/tasks.ts index ba3a1501..d3d1a6bf 100644 --- a/modules/onerepo/src/core/tasks/tasks.ts +++ b/modules/onerepo/src/core/tasks/tasks.ts @@ -134,7 +134,7 @@ export const handler: Handler = async (argv, { getWorkspaces, graph, logge if (list) { process.stdout.write(JSON.stringify({ parallel: [], serial: [] })); } - await setupStep.end(); + setupStep.end(); if (staged) { await stagingWorkflow.restoreUnstaged(); } @@ -180,7 +180,7 @@ export const handler: Handler = async (argv, { getWorkspaces, graph, logge } if (list) { - await setupStep.end(); + setupStep.end(); const step = logger.createStep('Listing tasks'); const all = { parallel: parallelTasks, @@ -196,19 +196,20 @@ export const handler: Handler = async (argv, { getWorkspaces, graph, logge return value; }), ); - await step.end(); + step.end(); return; } if (!hasTasks) { setupStep.info(`No tasks to run`); - await setupStep.end(); + setupStep.end(); if (staged) { await stagingWorkflow.restoreUnstaged(); } return; + } else { + setupStep.end(); } - await setupStep.end(); try { await batch(parallelTasks.flat(1).map((task) => task.fn ?? task)); @@ -280,7 +281,7 @@ function singleTaskToSpec( cmd === '$0' && logger.verbosity ? `-${'v'.repeat(logger.verbosity)}` : '', ].filter(Boolean) as Array; - const name = `${command.replace(/^\$0 /, `${cliName} `)} (${workspace.name})`; + const name = `${command.replace(/^\$0 /, `${cliName} `)}${!workspace.isRoot ? ` (${workspace.name})` : ''}`; let fn: PromiseFn | undefined; if (cmd === '$0') { @@ -298,7 +299,7 @@ function singleTaskToSpec( await yargs.parse(); await subLogger.end(); - await step.end(); + step.end(); return ['', '']; }; } diff --git a/modules/onerepo/src/setup/setup.ts b/modules/onerepo/src/setup/setup.ts index 82c3f57c..16d01c7e 100644 --- a/modules/onerepo/src/setup/setup.ts +++ b/modules/onerepo/src/setup/setup.ts @@ -9,7 +9,8 @@ import { globSync } from 'glob'; import { commandDirOptions, setupYargs } from '@onerepo/yargs'; import type { Graph } from '@onerepo/graph'; import { getGraph } from '@onerepo/graph'; -import { Logger, getLogger } from '@onerepo/logger'; +import type { Verbosity, Logger } from '@onerepo/logger'; +import { getLogger } from '@onerepo/logger'; import type { RequireDirectoryOptions, Argv as Yargv } from 'yargs'; import type { Argv, DefaultArgv, Yargs } from '@onerepo/yargs'; import { flushUpdateIndex } from '@onerepo/git'; @@ -112,7 +113,7 @@ export async function setup({ process.env.ONEREPO_HEAD_BRANCH = head; process.env.ONEREPO_DRY_RUN = 'false'; - const graph = await (inputGraph || getGraph(process.env.ONEREPO_ROOT)); + const graph = inputGraph || getGraph(process.env.ONEREPO_ROOT); const yargs = setupYargs(yargsInstance.scriptName('one'), { graph, logger }); yargs @@ -195,20 +196,15 @@ export async function setup({ }); const logger = getLogger(); + // Enforce the initial verbosity, in case it was modified + logger.verbosity = argv.verbosity as Verbosity; - // allow the last performance mark to propagate to observers. Super hacky. - await new Promise((resolve) => { - setImmediate(() => { - resolve(); - }); - }); - + await shutdown(argv); await logger.end(); - // Register a new logger on the top of the stack to silence output so that shutdown handlers to not write any output - const silencedLogger = new Logger({ verbosity: 0 }); - await shutdown(argv); - await silencedLogger.end(); + if (logger.hasError) { + process.exitCode = 1; + } }, }; } diff --git a/modules/subprocess/src/index.ts b/modules/subprocess/src/index.ts index e9217383..6f51ff77 100644 --- a/modules/subprocess/src/index.ts +++ b/modules/subprocess/src/index.ts @@ -115,7 +115,7 @@ export async function run(options: RunSpec): Promise<[string, string]> { detail: { description: 'Spawned subprocess', subprocess: { ...withoutLogger, opts } }, }); - if (options.opts?.stdio === 'inherit') { + if (!logger.captureAll && options.opts?.stdio === 'inherit') { logger.pause(); } @@ -132,13 +132,11 @@ export async function run(options: RunSpec): Promise<[string, string]> { ${JSON.stringify(withoutLogger, null, 2)}\n`, ); - return Promise.resolve() - .then(() => { - return !inputStep ? step.end() : Promise.resolve(); - }) - .then(() => { - resolve([out.trim(), err.trim()]); - }); + if (!inputStep) { + step.end(); + } + + return resolve([out.trim(), err.trim()]); } if (inputStep) { @@ -150,20 +148,24 @@ export async function run(options: RunSpec): Promise<[string, string]> { ${JSON.stringify(withoutLogger, null, 2)}\n${process.env.ONEREPO_ROOT ?? process.cwd()}\n`, ); - const subprocess = start(options); + const subprocess = start({ + ...options, + opts: { + ...options.opts, + env: { ...options.opts?.env, FORCE_COLOR: !process.env.NO_COLOR ? '1' : undefined }, + stdio: logger.captureAll ? 'pipe' : options.opts?.stdio, + }, + }); subprocess.on('error', (error) => { if (!options.skipFailures) { step.error(error); } - return Promise.resolve() - .then(() => { - logger.unpause(); - return !inputStep ? step.end() : Promise.resolve(); - }) - .then(() => { - reject(error); - }); + logger.unpause(); + if (!inputStep) { + step.end(); + } + return reject(error); }); if (subprocess.stdout && subprocess.stderr) { @@ -195,23 +197,24 @@ ${JSON.stringify(withoutLogger, null, 2)}\n${process.env.ONEREPO_ROOT ?? process const error = new SubprocessError(`${sortedOut || code}`); step.error(sortedOut.trim()); step.error(`Process exited with code ${code}`); - return (!inputStep ? step.end() : Promise.resolve()).then(() => { - logger.unpause(); - reject(error); - }); + if (!inputStep) { + step.end(); + } + + logger.unpause(); + reject(error); + return; } if (inputStep) { step.timing(`onerepo_start_Subprocess: ${options.name}`, `onerepo_end_Subprocess: ${options.name}`); } - return Promise.resolve() - .then(() => { - return !inputStep ? step.end() : Promise.resolve(); - }) - .then(() => { - resolve([out.trim(), err.trim()]); - }); + if (!inputStep) { + step.end(); + } + + return resolve([out.trim(), err.trim()]); }); }); } @@ -272,7 +275,7 @@ export async function sudo(options: Omit & { reason?: string }) `DRY-RUN command: sudo ${commandString}\n`, ); - await step.end(); + step.end(); return ['', '']; } @@ -304,12 +307,9 @@ export async function sudo(options: Omit & { reason?: string }) logger.log(stdout); logger.log(stderr); - return Promise.resolve() - .then(() => { - logger.unpause(); - return step.end(); - }) - .then(() => resolve([stdout, stderr])); + logger.unpause(); + step.end(); + resolve([stdout, stderr]); }, ); }); @@ -380,19 +380,17 @@ export async function batch( return new Promise((resolve, reject) => { const logger = getLogger(); logger.debug(`Running ${tasks.length} processes with max parallelism ${maxParallel}`); - function runTask(runner: () => Promise<[string, string]>, index: number): Promise { - return runner() - .then((output) => { - results[index] = output; - }) - .catch((e) => { - failing = true; - results[index] = e; - }) - .finally(() => { - completed += 1; - runNextTask(); - }); + async function runTask(runner: () => Promise<[string, string]>, index: number): Promise { + try { + const output = await runner(); + results[index] = output; + } catch (e) { + failing = true; + results[index] = e as Error; + } finally { + completed += 1; + runNextTask(); + } } function runNextTask() { diff --git a/modules/test-cli/src/index.ts b/modules/test-cli/src/index.ts index 620ffc08..7b82f306 100644 --- a/modules/test-cli/src/index.ts +++ b/modules/test-cli/src/index.ts @@ -100,6 +100,12 @@ async function runHandler>( }); const logger = new Logger({ verbosity: 5, stream }); + await new Promise((resolve) => { + setImmediate(() => { + resolve(); + }); + }); + const { builderExtras, graph = getGraph(path.join(dirname, 'fixtures', 'repo')) } = extras; const argv = await runBuilder(builder, cmd, graph, builderExtras); @@ -109,7 +115,6 @@ async function runHandler>( const wrappedGetFilepaths = (opts?: Parameters[2]) => builders.getFilepaths(graph, argv as builders.Argv, opts); - const error: unknown = undefined; await handler(argv, { logger, getAffected: wrappedGetAffected, @@ -121,8 +126,14 @@ async function runHandler>( await logger.end(); - if (logger.hasError || error) { - return Promise.reject(out || error); + await new Promise((resolve) => { + setImmediate(() => { + resolve(); + }); + }); + + if (logger.hasError) { + return Promise.reject(out); } return out; diff --git a/plugins/performance-writer/src/index.ts b/plugins/performance-writer/src/index.ts index b820875a..f95a3641 100644 --- a/plugins/performance-writer/src/index.ts +++ b/plugins/performance-writer/src/index.ts @@ -97,7 +97,7 @@ export function performanceWriter(opts: Options = {}): PluginObject { shutdown: async () => { const logger = getLogger(); - const step = logger.createStep('Report metrics'); + const step = logger.createStep('Report metrics', { verbosity: 0 }); observer.disconnect(); const measures = performance.getEntriesByType('measure'); @@ -111,7 +111,7 @@ export function performanceWriter(opts: Options = {}): PluginObject { } await file.write(outFile, JSON.stringify(measures), { step }); - await step.end(); + step.end(); }, }; } diff --git a/plugins/vitest/src/commands/vitest.ts b/plugins/vitest/src/commands/vitest.ts index 158dceaa..1369ee45 100644 --- a/plugins/vitest/src/commands/vitest.ts +++ b/plugins/vitest/src/commands/vitest.ts @@ -46,7 +46,7 @@ export const builder: Builder = (yargs) => export const handler: Handler = async function handler(argv, { getWorkspaces, graph }) { const { '--': other = [], affected, config, inspect, watch, workspaces } = argv; - const args: Array = ['--config', config]; + const args: Array = ['--config', config, '--clearScreen=false']; const wOther = other.indexOf('-w'); const watchOther = other.indexOf('--watch'); diff --git a/yarn.lock b/yarn.lock index ac155bea..2af41bff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2132,8 +2132,8 @@ __metadata: "@internal/jest-config": "workspace:^" "@internal/tsconfig": "workspace:^" "@internal/vitest-config": "workspace:^" - log-update: ^5.0.1 picocolors: ^1.0.0 + restore-cursor: ^5.0.0 typescript: ^5.7.2 languageName: unknown linkType: soft @@ -3804,15 +3804,6 @@ __metadata: languageName: node linkType: hard -"ansi-escapes@npm:^5.0.0": - version: 5.0.0 - resolution: "ansi-escapes@npm:5.0.0" - dependencies: - type-fest: ^1.0.2 - checksum: d4b5eb8207df38367945f5dd2ef41e08c28edc192dc766ef18af6b53736682f49d8bfcfa4e4d6ecbc2e2f97c258fda084fb29a9e43b69170b71090f771afccac - languageName: node - linkType: hard - "ansi-regex@npm:^5.0.1": version: 5.0.1 resolution: "ansi-regex@npm:5.0.1" @@ -3843,7 +3834,7 @@ __metadata: languageName: node linkType: hard -"ansi-styles@npm:^6.0.0, ansi-styles@npm:^6.1.0, ansi-styles@npm:^6.2.1": +"ansi-styles@npm:^6.1.0, ansi-styles@npm:^6.2.1": version: 6.2.1 resolution: "ansi-styles@npm:6.2.1" checksum: ef940f2f0ced1a6347398da88a91da7930c33ecac3c77b72c5905f8b8fe402c52e6fde304ff5347f616e27a742da3f1dc76de98f6866c69251ad0b07a66776d9 @@ -4841,15 +4832,6 @@ __metadata: languageName: node linkType: hard -"cli-cursor@npm:^4.0.0": - version: 4.0.0 - resolution: "cli-cursor@npm:4.0.0" - dependencies: - restore-cursor: ^4.0.0 - checksum: ab3f3ea2076e2176a1da29f9d64f72ec3efad51c0960898b56c8a17671365c26e67b735920530eaf7328d61f8bd41c27f46b9cf6e4e10fe2fa44b5e8c0e392cc - languageName: node - linkType: hard - "cli-cursor@npm:^5.0.0": version: 5.0.0 resolution: "cli-cursor@npm:5.0.0" @@ -8409,13 +8391,6 @@ __metadata: languageName: node linkType: hard -"is-fullwidth-code-point@npm:^4.0.0": - version: 4.0.0 - resolution: "is-fullwidth-code-point@npm:4.0.0" - checksum: 8ae89bf5057bdf4f57b346fb6c55e9c3dd2549983d54191d722d5c739397a903012cc41a04ee3403fd872e811243ef91a7c5196da7b5841dc6b6aae31a264a8d - languageName: node - linkType: hard - "is-generator-fn@npm:^2.0.0": version: 2.1.0 resolution: "is-generator-fn@npm:2.1.0" @@ -9683,19 +9658,6 @@ __metadata: languageName: node linkType: hard -"log-update@npm:^5.0.1": - version: 5.0.1 - resolution: "log-update@npm:5.0.1" - dependencies: - ansi-escapes: ^5.0.0 - cli-cursor: ^4.0.0 - slice-ansi: ^5.0.0 - strip-ansi: ^7.0.1 - wrap-ansi: ^8.0.1 - checksum: 2c6b47dcce6f9233df6d232a37d9834cb3657a0749ef6398f1706118de74c55f158587d4128c225297ea66803f35c5ac3db4f3f617046d817233c45eedc32ef1 - languageName: node - linkType: hard - "longest-streak@npm:^3.0.0": version: 3.1.0 resolution: "longest-streak@npm:3.1.0" @@ -10726,10 +10688,10 @@ __metadata: languageName: node linkType: hard -"mimic-function@npm:^5.0.0": - version: 5.0.1 - resolution: "mimic-function@npm:5.0.1" - checksum: eb5893c99e902ccebbc267c6c6b83092966af84682957f79313311edb95e8bb5f39fb048d77132b700474d1c86d90ccc211e99bae0935447a4834eb4c882982c +"mimic-fn@npm:^4.0.0": + version: 4.0.0 + resolution: "mimic-fn@npm:4.0.0" + checksum: 995dcece15ee29aa16e188de6633d43a3db4611bcf93620e7e62109ec41c79c0f34277165b8ce5e361205049766e371851264c21ac64ca35499acb5421c2ba56 languageName: node linkType: hard @@ -11273,12 +11235,12 @@ __metadata: languageName: node linkType: hard -"onetime@npm:^7.0.0": - version: 7.0.0 - resolution: "onetime@npm:7.0.0" +"onetime@npm:^6.0.0": + version: 6.0.0 + resolution: "onetime@npm:6.0.0" dependencies: - mimic-function: ^5.0.0 - checksum: eb08d2da9339819e2f9d52cab9caf2557d80e9af8c7d1ae86e1a0fef027d00a88e9f5bd67494d350df360f7c559fbb44e800b32f310fb989c860214eacbb561c + mimic-fn: ^4.0.0 + checksum: 0846ce78e440841335d4e9182ef69d5762e9f38aa7499b19f42ea1c4cd40f0b4446094c455c713f9adac3f4ae86f613bb5e30c99e52652764d06a89f709b3788 languageName: node linkType: hard @@ -12425,23 +12387,13 @@ __metadata: languageName: node linkType: hard -"restore-cursor@npm:^4.0.0": - version: 4.0.0 - resolution: "restore-cursor@npm:4.0.0" - dependencies: - onetime: ^5.1.0 - signal-exit: ^3.0.2 - checksum: 5b675c5a59763bf26e604289eab35711525f11388d77f409453904e1e69c0d37ae5889295706b2c81d23bd780165084d040f9b68fffc32cc921519031c4fa4af - languageName: node - linkType: hard - "restore-cursor@npm:^5.0.0": - version: 5.1.0 - resolution: "restore-cursor@npm:5.1.0" + version: 5.0.0 + resolution: "restore-cursor@npm:5.0.0" dependencies: - onetime: ^7.0.0 + onetime: ^6.0.0 signal-exit: ^4.1.0 - checksum: 838dd54e458d89cfbc1a923b343c1b0f170a04100b4ce1733e97531842d7b440463967e521216e8ab6c6f8e89df877acc7b7f4c18ec76e99fb9bf5a60d358d2c + checksum: e7c29a44105e877cf1836dd9901261519941caa3db2c7340d41a3cd1e6527a4522831dd1dc205eb638a026e6033940fb6b07d9326f8890d1bf0acdce41858786 languageName: node linkType: hard @@ -13012,16 +12964,6 @@ __metadata: languageName: node linkType: hard -"slice-ansi@npm:^5.0.0": - version: 5.0.0 - resolution: "slice-ansi@npm:5.0.0" - dependencies: - ansi-styles: ^6.0.0 - is-fullwidth-code-point: ^4.0.0 - checksum: 7e600a2a55e333a21ef5214b987c8358fe28bfb03c2867ff2cbf919d62143d1812ac27b4297a077fdaf27a03da3678e49551c93e35f9498a3d90221908a1180e - languageName: node - linkType: hard - "smart-buffer@npm:^4.2.0": version: 4.2.0 resolution: "smart-buffer@npm:4.2.0" @@ -13778,13 +13720,6 @@ __metadata: languageName: node linkType: hard -"type-fest@npm:^1.0.2": - version: 1.4.0 - resolution: "type-fest@npm:1.4.0" - checksum: b011c3388665b097ae6a109a437a04d6f61d81b7357f74cbcb02246f2f5bd72b888ae33631b99871388122ba0a87f4ff1c94078e7119ff22c70e52c0ff828201 - languageName: node - linkType: hard - "type-fest@npm:^4.21.0": version: 4.30.0 resolution: "type-fest@npm:4.30.0" @@ -14798,7 +14733,7 @@ __metadata: languageName: node linkType: hard -"wrap-ansi@npm:^8.0.1, wrap-ansi@npm:^8.1.0": +"wrap-ansi@npm:^8.1.0": version: 8.1.0 resolution: "wrap-ansi@npm:8.1.0" dependencies: