Skip to content

Commit 6ac45af

Browse files
authored
feat(tools-formatting): create @rnx-kit/tools-formatting package and move the table formatter to that package (#4097)
1 parent 09b6f03 commit 6ac45af

17 files changed

Lines changed: 331 additions & 49 deletions

File tree

.changeset/brown-mugs-drive.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---

incubator/tools-babel/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"@rnx-kit/reporter": "*",
4040
"@rnx-kit/scripts": "*",
4141
"@rnx-kit/test-fixtures": "*",
42+
"@rnx-kit/tools-formatting": "^0.0.1",
4243
"@rnx-kit/tsconfig": "*",
4344
"@swc/core": "^1.15.24",
4445
"@types/babel__core": "^7.20.0",

incubator/tools-babel/test/ast.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
* Diagnostic test that finds structural AST differences between OXC and Babel
33
* on a single simple non-comment JS fixture, reporting all differences.
44
*/
5+
import { formatAsTable } from "@rnx-kit/tools-formatting";
56
import { ok } from "node:assert/strict";
67
import { describe, it } from "node:test";
7-
import { formatAsTable } from "../../tools-performance/src/table";
88
import type { AnyNode } from "./analysis";
99
import { diffAst } from "./analysis";
1010
import type { FileData } from "./fixtures";
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# @rnx-kit/tools-formatting
2+
3+
[![Build](https://github.com/microsoft/rnx-kit/actions/workflows/build.yml/badge.svg)](https://github.com/microsoft/rnx-kit/actions/workflows/build.yml)
4+
[![npm version](https://img.shields.io/npm/v/@rnx-kit/tools-formatting)](https://www.npmjs.com/package/@rnx-kit/tools-formatting)
5+
6+
🚧🚧🚧🚧🚧🚧🚧🚧🚧🚧🚧
7+
8+
### THIS TOOL IS EXPERIMENTAL — USE WITH CAUTION
9+
10+
🚧🚧🚧🚧🚧🚧🚧🚧🚧🚧🚧
11+
12+
Provides light-weight, zero-dependency, formatting utilities for console (or log-file) output.
13+
14+
## Motivation
15+
16+
Lightweight and centralized formatting utilities.
17+
18+
## Installation
19+
20+
```sh
21+
yarn add @rnx-kit/tools-formatting --dev
22+
```
23+
24+
or if you're using npm
25+
26+
```sh
27+
npm add --save-dev @rnx-kit/tools-formatting
28+
```
29+
30+
## Usage
31+
32+
### Table formatting
33+
34+
The `formatAsTable` utility can format any 2D data array into a bordered table:
35+
36+
```typescript
37+
import { formatAsTable } from "@rnx-kit/tools-formatting";
38+
39+
const table = formatAsTable(
40+
[
41+
["parse", 12, 1],
42+
["bundle", 450, 1],
43+
],
44+
{
45+
columns: [
46+
{ label: "operation", align: "left" },
47+
{ label: "total (ms)", align: "right", digits: 0, localeFmt: true },
48+
{ label: "calls", align: "right" },
49+
],
50+
sort: [1],
51+
}
52+
);
53+
console.log(table);
54+
```
55+
56+
### Path shortening
57+
58+
`shortenPath` truncates file paths for display, keeping the most significant
59+
trailing segments and replacing the rest with an ellipsis. This is useful for
60+
tables or logs where long absolute paths waste space.
61+
62+
```typescript
63+
import { shortenPath } from "@rnx-kit/tools-formatting";
64+
65+
shortenPath(
66+
"/Users/me/dev/rnx-kit/packages/metro-resolver-symlinks/src/resolver.ts"
67+
);
68+
// => ".../metro-resolver-symlinks/src/resolver.ts"
69+
```
70+
71+
By default it keeps 3 path segments. If the segment at the cut boundary is a
72+
known source directory (`src`, `lib`, `dist`, `bin`), it keeps one extra segment
73+
so the parent package name stays visible:
74+
75+
```typescript
76+
shortenPath("/Users/me/dev/rnx-kit/packages/my-package/src/utils/helpers.ts");
77+
// => ".../my-package/src/utils/helpers.ts" (4 segments)
78+
```
79+
80+
Short paths are returned unchanged when shortening would not save space. The
81+
segment count can be customized:
82+
83+
```typescript
84+
shortenPath("/a/b/c/d/e.ts", 2);
85+
// => ".../d/e.ts"
86+
```
87+
88+
## API Reference
89+
90+
### Functions
91+
92+
| Function | Description |
93+
| ---------------------------- | ------------------------------------------------------------------------------- |
94+
| `formatAsTable(data, opts?)` | Format a 2D data array into a bordered ASCII table. |
95+
| `shortenPath(path, segs?)` | Shorten a file path to the last _segs_ segments (default 3), with `...` prefix. |
96+
97+
### TableOptions
98+
99+
| Field | Type | Default | Description |
100+
| ----------- | ----------------------------- | ------- | ----------------------------------------------- |
101+
| `columns` | `(string \| ColumnOptions)[]` | auto | Column labels or configuration objects. |
102+
| `sort` | `number[]` | none | Column indices to sort by, in precedence order. |
103+
| `showIndex` | `boolean` | `false` | Show a row index column. |
104+
| `noColors` | `boolean` | `false` | Strip ANSI styling from output. |
105+
106+
### ColumnOptions
107+
108+
| Field | Type | Default | Description |
109+
| ----------- | --------------------------- | -------- | -------------------------------------------- |
110+
| `label` | `string` | auto | Column header label. |
111+
| `digits` | `number` | -- | Fixed decimal places for numeric values. |
112+
| `localeFmt` | `boolean` | `false` | Use locale number formatting. |
113+
| `align` | `"left"\|"right"\|"center"` | `"left"` | Cell text alignment. |
114+
| `maxWidth` | `number` | -- | Maximum column width (truncates with `...`). |
115+
| `style` | `StyleValue \| function` | -- | ANSI style or custom formatter. |
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"name": "@rnx-kit/tools-formatting",
3+
"version": "0.0.1",
4+
"description": "EXPERIMENTAL - USE WITH CAUTION - tools-formatting",
5+
"homepage": "https://github.com/microsoft/rnx-kit/tree/main/incubator/tools-formatting#readme",
6+
"license": "MIT",
7+
"author": {
8+
"name": "Microsoft Open Source",
9+
"email": "microsoftopensource@users.noreply.github.com"
10+
},
11+
"repository": {
12+
"type": "git",
13+
"url": "https://github.com/microsoft/rnx-kit",
14+
"directory": "incubator/tools-formatting"
15+
},
16+
"files": [
17+
"lib/**/*.d.ts",
18+
"lib/**/*.js"
19+
],
20+
"type": "module",
21+
"sideEffects": false,
22+
"main": "lib/index.js",
23+
"types": "lib/index.d.ts",
24+
"scripts": {
25+
"build": "rnx-kit-scripts build",
26+
"format": "rnx-kit-scripts format",
27+
"lint": "rnx-kit-scripts lint",
28+
"test": "rnx-kit-scripts test"
29+
},
30+
"devDependencies": {
31+
"@rnx-kit/scripts": "*",
32+
"@rnx-kit/tsconfig": "*"
33+
},
34+
"engines": {
35+
"node": ">=22.11"
36+
},
37+
"experimental": true
38+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const ELLIPSIS = "...";
2+
export const SRC_DIRS = ["src", "lib", "dist", "bin"];
3+
export const SEPARATORS = ["/", "\\"];
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export { shortenPath } from "./paths.ts";
2+
3+
export type { TableOptions, ColumnOptions } from "./table.ts";
4+
export { formatAsTable } from "./table.ts";
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { ELLIPSIS, SEPARATORS, SRC_DIRS } from "./const.ts";
2+
3+
/**
4+
* Utility function for formatting file paths in a short form for display purposes. In particular,
5+
* this will shorten to the path to the last 3 segments, and replace the rest with an ellipsis. For example:
6+
* /Users/someuser/dev/rnx-kit/packages/metro-resolver-symlinks/src/metro-resolver.ts
7+
* would be shortened to:
8+
* .../metro-resolver-symlinks/src/metro-resolver.ts
9+
*
10+
* If the last path segment is a known source directory (e.g. "src", "lib", "dist", "bin"), then it will
11+
* return the last 4 segments instead.
12+
*
13+
* This is useful for displaying file paths in a table format where space is limited and focusing attention
14+
* on the most significant parts of the path.
15+
* @param path The file path to shorten
16+
* @param segments The number of path segments to include in the shortened path (default is 3)
17+
* @returns The shortened file path
18+
*/
19+
export function shortenPath(path: string, segments = 3): string {
20+
let last = 0;
21+
for (let i = path.length - 1; i > ELLIPSIS.length; i--) {
22+
if (SEPARATORS.includes(path[i])) {
23+
segments--;
24+
if (segments === 0) {
25+
// check the last slice to see if it starts with a known source dir, if so keep iterating.
26+
if (last > i && SRC_DIRS.includes(path.slice(i + 1, last))) {
27+
segments++;
28+
last = 0;
29+
} else {
30+
return ELLIPSIS + path.slice(i);
31+
}
32+
} else {
33+
last = i;
34+
}
35+
}
36+
}
37+
return path;
38+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { equal } from "node:assert/strict";
2+
import { describe, it } from "node:test";
3+
import { shortenPath } from "../src/paths.ts";
4+
5+
describe("shortenPath", () => {
6+
it("shortens a long unix path to 3 segments", () => {
7+
equal(
8+
shortenPath(
9+
"/Users/someuser/dev/rnx-kit/packages/metro-resolver-symlinks/src/metro-resolver.ts"
10+
),
11+
".../metro-resolver-symlinks/src/metro-resolver.ts"
12+
);
13+
});
14+
15+
it("keeps 4 segments when the boundary falls on a src dir", () => {
16+
equal(
17+
shortenPath(
18+
"/Users/someuser/dev/rnx-kit/packages/my-package/src/utils/helpers.ts"
19+
),
20+
".../my-package/src/utils/helpers.ts"
21+
);
22+
});
23+
24+
it("detects lib as a src dir", () => {
25+
equal(
26+
shortenPath("/a/b/c/d/lib/utils/index.ts"),
27+
".../d/lib/utils/index.ts"
28+
);
29+
});
30+
31+
it("detects dist as a src dir", () => {
32+
equal(
33+
shortenPath("/a/b/c/d/dist/utils/index.js"),
34+
".../d/dist/utils/index.js"
35+
);
36+
});
37+
38+
it("detects bin as a src dir", () => {
39+
equal(shortenPath("/a/b/c/d/bin/utils/cli.js"), ".../d/bin/utils/cli.js");
40+
});
41+
42+
it("returns path unchanged when it has fewer segments than requested", () => {
43+
equal(shortenPath("foo/bar.ts"), "foo/bar.ts");
44+
});
45+
46+
it("does not shorten when result would be longer than original", () => {
47+
// /a/b/c.ts is short enough that prepending "..." would not save space
48+
equal(shortenPath("/a/b/c.ts"), "/a/b/c.ts");
49+
});
50+
51+
it("returns path unchanged when separators are fewer than segments", () => {
52+
equal(shortenPath("a/b/c.ts"), "a/b/c.ts");
53+
});
54+
55+
it("returns empty string for empty input", () => {
56+
equal(shortenPath(""), "");
57+
});
58+
59+
it("returns bare filename unchanged", () => {
60+
equal(shortenPath("file.ts"), "file.ts");
61+
});
62+
63+
it("handles windows-style backslash separators", () => {
64+
equal(
65+
shortenPath("C:\\Users\\me\\dev\\packages\\my-pkg\\src\\index.ts"),
66+
"...\\my-pkg\\src\\index.ts"
67+
);
68+
});
69+
70+
it("respects custom segment count", () => {
71+
equal(shortenPath("/a/b/c/d/e.ts", 2), ".../d/e.ts");
72+
});
73+
74+
it("custom segments=1 returns just the filename with ellipsis", () => {
75+
equal(shortenPath("/a/b/c/d/e.ts", 1), ".../e.ts");
76+
});
77+
78+
it("src dir check still applies with custom segment count", () => {
79+
equal(shortenPath("/a/b/c/src/e.ts", 2), ".../c/src/e.ts");
80+
});
81+
82+
it("does not trigger src dir check for non-boundary segments", () => {
83+
equal(shortenPath("/a/b/c/d/src/file.ts"), ".../d/src/file.ts");
84+
});
85+
86+
it("handles trailing separator", () => {
87+
// trailing / creates an empty segment, so 3 segments = empty + "e" + "d"
88+
equal(shortenPath("/a/b/c/d/e/"), ".../d/e/");
89+
});
90+
91+
it("src dir check only extends by one extra segment", () => {
92+
// Even if the 4th segment is also a src dir, we only get one extra
93+
equal(
94+
shortenPath("/longer/lib/src/utils/helpers.ts"),
95+
".../lib/src/utils/helpers.ts"
96+
);
97+
});
98+
99+
it("does not shorten when src dir extension would exceed original length", () => {
100+
// /a/lib/src/utils/helpers.ts — the src dir extension would place
101+
// the cut at index 2, inside the ellipsis guard, so it returns unchanged
102+
equal(
103+
shortenPath("/a/lib/src/utils/helpers.ts"),
104+
"/a/lib/src/utils/helpers.ts"
105+
);
106+
});
107+
});

0 commit comments

Comments
 (0)