Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
422adfb
add basic ansi color support to console package
JasonVMo Aug 15, 2025
d79e010
reduce implementations in console to just provide the base color cons…
JasonVMo Aug 18, 2025
28c83c2
remove color exports from console package
JasonVMo Aug 19, 2025
cf3fe79
rework reporter to be aligned with office and self-contained
JasonVMo Sep 3, 2025
02e7472
more updates to ensure reporter is tree-shakeable
JasonVMo Sep 4, 2025
129978d
use overloaded timing and add some tests for new shape
JasonVMo Sep 6, 2025
125868b
updated reporter implementation
JasonVMo Sep 19, 2025
ff0b1e6
update unit tests for the reporter
JasonVMo Sep 19, 2025
90f8dcb
add some fixes and test fixes
JasonVMo Sep 19, 2025
2962bf3
lockfile changes
JasonVMo Sep 19, 2025
c4e39b6
Merge remote-tracking branch 'origin/main' into jasonvmo/reporter
JasonVMo Sep 19, 2025
d44c886
fix new linting error
JasonVMo Sep 19, 2025
55a2ac6
updated reporter implementation
JasonVMo Sep 19, 2025
5d62a82
docs(changeset): rework of the reporter package to be better organize…
JasonVMo Sep 19, 2025
40b71d2
remove console changes
JasonVMo Sep 19, 2025
307df97
Apply suggestions from code review
JasonVMo Sep 26, 2025
b8a340c
remove performance and cascade modules and address some review feedback
JasonVMo Sep 29, 2025
c0937fe
add missing changes to add dynamic prefix support
JasonVMo Sep 29, 2025
012a17c
address remaining code review changes
JasonVMo Sep 29, 2025
38e4d70
Apply suggestions from code review
JasonVMo Sep 30, 2025
764506d
Merge remote-tracking branch 'origin/main' into jasonvmo/reporter
JasonVMo Sep 30, 2025
3c39bad
add control character stripping to file streams
JasonVMo Sep 30, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/bright-melons-attend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@rnx-kit/reporter": minor
---

rework of the reporter package to be better organized, self-contained, and have
additional functionality
335 changes: 252 additions & 83 deletions incubator/reporter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,101 +13,270 @@ This is a common package for logging output to the console and/or logfiles,
timing tasks and operations, and listening for events for task start, finish,
and errors.

It also contains a performance tracker that can be enabled to dump performance
information from execution of tasks. Enabling performance tracing will chain to
child processes as well to handle script tracking.
It is written as esm, side-effect free, with the functionality separated so that
it will only bring in the portions that are used. The core logger and reporter
functionality can be used on its own, with additional modules provided for
colors and formatting.

## Motivation
All code is self-contained and this has no dependencies.

Standardizing the reporter used in our packages allows for easier high level
perf analysis of how our tools are behaving. By adding the various events it
also gives a common framework for people to add listeners for telemetry if
desired.
## Core Components

## Installation
### 🎯 **Logger (`createLogger`)**

```sh
yarn add @rnx-kit/reporter --dev
A flexible logging system that supports multiple log levels and custom output
destinations.

```typescript
import { createLogger } from "@rnx-kit/reporter";

const logger = createLogger({
output: "verbose", // or custom OutputWriter
prefix: { error: "🚨", warn: "⚠️" },
onError: (args) => console.error("Error occurred:", args),
});

logger.error("Something went wrong");
logger.warn("This is a warning");
logger.log("General information");
logger.verbose("Detailed debugging info:", myCustomObject);
logger.fatalError("Critical error"); // logs and throws
```

or if you're using npm
**Features:**

- **Log Levels**: `error`, `warn`, `log`, `verbose` with hierarchical filtering
- **Custom Prefixes**: Add emoji, text, or styling to log messages
- **Error Callbacks**: Handle errors with custom logic
- **Multiple Outputs**: Console, files, or custom destinations

### πŸ“Š **Reporter (`createReporter`)**

A hierarchical task and performance tracking system built on top of the logger.

```typescript
import { createReporter } from "@rnx-kit/reporter";

const reporter = createReporter({
name: "build-system",
output: "log",
reportTimers: true,
});

// Hierarchical task execution
await reporter.task("build", async (buildTask) => {
buildTask.log("Starting build process...");

```sh
npm add --save-dev @rnx-kit/reporter
// async functions will time and execute asynchronously
const result1 = await buildTask.task("compile", async (compileTask) => {
compileTask.log("Compiling TypeScript...");
// compilation logic
});

// sync functions will execute without yielding to the event loop
const result2 = buildTask.task("bundle", (bundleTask) => {
bundleTask.log("Creating bundle...");
// bundling logic
});
});

// Operation timing
const result = await reporter.measure("file-processing", async () => {
// time-sensitive operation
return processFiles();
});
```

## Usage

Reporters have two roles, the base Reporter role, and a Task role. Reporters are
effectively at the root level, whereas Tasks are parented to a reporter or
another Task.

Reporters are created by calling
`createReporter<T>(options: ReporterOptions<T>)` with any options specified.

- The generic parameter sets the type of the `data` property.
- This allows passing additional information through reporters and tasks and
will be surfaced in events.

The reporter interface is comprised of several parts:

### Logging Functions

These include `error`, `warn`, `log`, and `verbose`. These are structured like
the console logging functions in that they have variable parameters, which will
be serialized into a single message string by using node's `inspect`. This is
the same internal mechanism used by console.log, at least in the node
implementation.

- The `LogLevel` set in either the global settings, or overridden in the
reporter settings dictates whether anything is output from the functions.
- `"log"` is the default level, which will enable the `error`, `warn`, and `log`
functions. The `verbose` function will do nothing.
- File logging can be enabled by configuring `OutputOptions` when creating a
reporter or by calling `updateDefaultOutput`.
- File logging will share the same log level as the console, unless the level is
set specifically in the file settings.
- A prefix for a type of message can be set in settings. This is prepended to
all messages of this type. For instance the default error prefix contains
`"Error:"`
- A label can be set for the reporter, which will prepend all output for all log
types.

An additional `throwError` function is provided which will log the error and
then throw with that message. It will send an event for the error, and log it
under the reporter/task.

### Timing Functions

The task functions wrap a function call, either async or synchronous, creating a
new sub reporter in the Task role which is passed as a parameter. The task data
type, output, and formatting options are inherited from the parent reporter.
Events will be sent when the task is started and when it completes, with errors
and sub-operation timing recorded within.

```ts
task<T>(
name: string | TaskOptions<TData>,
fn: (reporter: Reporter<TData>) => T
): T;
taskAsync<T>(
name: string | TaskOptions<TData>,
fn: (reporter: Reporter<TData>) => Promise<T>
): Promise<T>;
**Features:**

- **Hierarchical Tasks**: Nested task execution with automatic timing
- **Performance Measurement**: Track operation durations and call counts
- **Error Tracking**: Automatic error collection and reporting
- **Event Publishing**: Start/finish/error events via Node.js diagnostics
channels

### 🎨 **Formatting & Colors**

Rich text formatting with ANSI colors and semantic highlighting.

```typescript
import { getFormatter, createFormatter } from "@rnx-kit/reporter";

const fmt = getFormatter();

// Basic colors
console.log(fmt.red("Error message"));
console.log(fmt.green("Success message"));
console.log(fmt.blue("Info message"));

// Semantic formatting
console.log(fmt.package("@my-scope/package-name"));
console.log(fmt.duration(1250)); // "1.25s"
console.log(fmt.pad("text", 10, "center")); // " text "

// Custom formatter
const customFmt = createFormatter({
highlight1: fmt.magenta,
durationValue: fmt.yellowBright,
});
```

**Features:**

- **ANSI Colors**: Full 16-color and 256-color support
- **Font Styles**: Bold, dim, italic, underline, strikethrough
- **Semantic Colors**: Package names, durations, highlights, paths
- **Smart Padding**: VT control character-aware text alignment
- **Auto-detection**: Respects terminal color capabilities

### πŸ“‘ **Event System**

Type-safe event handling using Node.js diagnostics channels.

```typescript
import {
subscribeToStart,
subscribeToFinish,
subscribeToError,
} from "@rnx-kit/reporter";

// Listen for task start events
const unsubscribeStart = subscribeToStart((session) => {
console.log(`Task started: ${session.name} (depth: ${session.depth})`);
});

// Listen for task completion
const unsubscribeFinish = subscribeToFinish((session) => {
console.log(`Task finished: ${session.name} in ${session.elapsed}ms`);
console.log(`Operations:`, session.operations);
});

// Listen for errors
const unsubscribeError = subscribeToError((event) => {
console.log(`Error in ${event.session.name}:`, event.args);
});

// Cleanup when done
unsubscribeStart();
unsubscribeFinish();
unsubscribeError();
```

The `time` and `timeAsync` functions are helpers for high frequency operation
timing. The results of these operations will be aggregated within the given
reporter or task, and will record the total elapsed time and number of calls.
These will be available in the complete event.
## Output Destinations

```ts
time<T>(label: string, fn: () => T): T;
timeAsync<T>(label: string, fn: () => Promise<T>): Promise<T>;
### πŸ“€ **Console Output**

Default output to stdout/stderr with proper log level routing.

```typescript
import { createOutput } from "@rnx-kit/reporter";

// Console output with specific log level
const output = createOutput("warn"); // Only error and warn messages
```

### Formatting Functions
### πŸ“ **File Output**

Write logs to files with automatic directory creation.

These functions are part of the `ReporterFormatting` interface and provide
helpers which will format or color text using the settings for the given
reporter.
```typescript
import { openFileWrite } from "@rnx-kit/reporter";

const fileOutput = createOutput(
"verbose",
openFileWrite("./logs/app.log", true), // append mode
openFileWrite("./logs/errors.log", true)
);
```

### πŸ”€ **Multiple Outputs**

Combine multiple output destinations.

```typescript
import { mergeOutput, createOutput } from "@rnx-kit/reporter";

const consoleOut = createOutput("warn");
const fileOut = createOutput("verbose", fileWriter);
const combined = mergeOutput(consoleOut, fileOut);
```

## Architecture Principles

### 🧩 **Modular Design**

Each component can be used independently:

- Use just the logger for simple logging needs
- Add the reporter for task tracking
- Include formatting for rich output
- Enable events for monitoring

### 🎯 **Zero Dependencies**

Completely self-contained with no external dependencies, using only Node.js
built-ins.

### πŸ“ **Type Safety**

Comprehensive TypeScript definitions with full type inference and safety.

### πŸ”„ **Side-Effect Free**

ESM modules with no global state pollution - safe for library use.

### ⚑ **Performance Focused**

- Lazy initialization of heavy components
- Efficient string handling
- Minimal allocation in hot paths
- Optional features don't impact performance when unused

## Common Patterns

### πŸ—οΈ **Build Tool Integration**

```typescript
const build = createReporter({ name: "webpack-build", reportTimers: true });

await build.task("compile", async (task) => {
const stats = await task.measure("typescript", () => compileTypeScript());
const bundle = await task.measure("webpack", () => runWebpack());
task.log(`Compilation complete: ${stats.files} files, ${bundle.size} bytes`);
});
```

### πŸ§ͺ **Test Runner Integration**

```typescript
const test = createReporter({ name: "test-runner", output: "verbose" });

for (const suite of testSuites) {
await test.task(suite.name, async (suiteTask) => {
for (const testCase of suite.tests) {
try {
await suiteTask.measure(testCase.name, () => testCase.run());
suiteTask.log(`βœ… ${testCase.name}`);
} catch (error) {
suiteTask.error(`❌ ${testCase.name}:`, error);
}
}
});
}
```

### πŸš€ **CLI Application Logging**

```typescript
const app = createCascadingReporter("MY_CLI_APP", {
level: process.env.VERBOSE ? "verbose" : "log",
file: process.env.LOG_FILE ? { out: process.env.LOG_FILE } : undefined,
});

if (app) {
await app.task("main", async (task) => {
task.log("Application started");
// CLI logic
});
}
```
4 changes: 3 additions & 1 deletion incubator/reporter/eslint.config.js
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
module.exports = require("@rnx-kit/eslint-config");
import config from "@rnx-kit/eslint-config";
// eslint-disable-next-line no-restricted-exports
export default config;
9 changes: 2 additions & 7 deletions incubator/reporter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
"license": "MIT",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"type": "module",
"sideEffects": false,
Comment thread
JasonVMo marked this conversation as resolved.
"author": {
"name": "Microsoft Open Source",
"email": "microsoftopensource@users.noreply.github.com"
Expand Down Expand Up @@ -35,17 +37,10 @@
"lint": "rnx-kit-scripts lint",
"test": "rnx-kit-scripts test"
},
"dependencies": {
"chalk": "^4.1.0"
},
"devDependencies": {
"@rnx-kit/eslint-config": "*",
"@rnx-kit/jest-preset": "*",
"@rnx-kit/scripts": "*",
"@rnx-kit/tsconfig": "*"
},
"jest": {
"preset": "@rnx-kit/jest-preset/private"
},
"experimental": true
}
Loading