Skip to content

Commit

Permalink
Add @nx.js/inspect package (#147)
Browse files Browse the repository at this point in the history
  • Loading branch information
TooTallNate authored Sep 2, 2024
1 parent 72173eb commit e7e769e
Show file tree
Hide file tree
Showing 14 changed files with 393 additions and 210 deletions.
5 changes: 5 additions & 0 deletions .changeset/early-countries-smoke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nx.js/inspect": patch
---

Add `@nx.js/inspect` package
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ jobs:
- name: Install Dependencies
run: pnpm install --frozen-lockfile

- name: Build dependent packages
run: pnpm build --filter nxjs-runtime

- name: Bundle `runtime.js`
run: pnpm bundle

Expand Down
7 changes: 7 additions & 0 deletions docs/app/toggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
TbBrowser,
TbBraces,
TbBrandPowershell,
TbZoomQuestion,
} from 'react-icons/tb';
import { RootToggle } from 'fumadocs-ui/components/layout/root-toggle';

Expand All @@ -28,6 +29,12 @@ export function Toggle() {
url: '/http',
icon: <TbBrowser size={24} />,
},
{
title: '@nx.js/inspect',
description: 'Inspection utility for nx.js',
url: '/inspect',
icon: <TbZoomQuestion size={24} />,
},
{
title: '@nx.js/repl',
description: 'Read-Eval-Print Loop for nx.js',
Expand Down
35 changes: 35 additions & 0 deletions docs/content/inspect/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
title: Installation
---

<InstallTabs items={['npm', 'pnpm', 'yarn', 'bun']}>
<Tab value='npm'>

```bash
npm install @nx.js/inspect
```

</Tab>
<Tab value="pnpm">

```bash
pnpm add @nx.js/inspect
```

</Tab>
<Tab value='yarn'>

```bash
yarn add @nx.js/inspect
```

</Tab>
<Tab value='bun'>

```bash
bun add @nx.js/inspect
```

</Tab>

</InstallTabs>
4 changes: 4 additions & 0 deletions docs/content/inspect/meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"root": true,
"pages": ["--- Usage ---", "index", "--- API Reference ---", "...api"]
}
8 changes: 8 additions & 0 deletions packages/inspect/node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { createInspect } from './src/index';

export const inspect = createInspect();

console.log(inspect(Promise.resolve(42)));
console.log(inspect(/test/g));
console.log(inspect({ a: 1, b: 2, 'c d': 3 }));
console.log(inspect(async () => 42));
38 changes: 38 additions & 0 deletions packages/inspect/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"name": "@nx.js/inspect",
"version": "0.0.0",
"description": "JavaScript value inspect utility for nx.js",
"repository": {
"type": "git",
"url": "https://github.com/TooTallNate/nx.js.git",
"directory": "packages/inspect"
},
"type": "module",
"homepage": "https://nxjs.n8.io/inspect",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"test": "vitest",
"docs": "typedoc && ../../type-aliases-meta.sh"
},
"files": [
"dist"
],
"publishConfig": {
"access": "public"
},
"dependencies": {
"kleur": "github:TooTallNate/kleur#rgb"
},
"devDependencies": {
"vite": "^5.4.2",
"vitest": "^2.0.5"
},
"keywords": [
"nx.js",
"switch",
"inspect"
],
"author": "Nathan Rajlich <[email protected]>",
"license": "MIT"
}
245 changes: 245 additions & 0 deletions packages/inspect/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
import { bold, cyan, green, magenta, red, rgb, yellow } from 'kleur/colors';

const grey = rgb(100, 100, 100);

export const PROMISE_STATE_UNKNOWN = -1 as const;
export const PROMISE_STATE_PENDING = 0 as const;
export const PROMISE_STATE_FULFILLED = 1 as const;
export const PROMISE_STATE_REJECTED = 2 as const;

export type PromiseState =
| typeof PROMISE_STATE_UNKNOWN
| typeof PROMISE_STATE_PENDING
| typeof PROMISE_STATE_FULFILLED
| typeof PROMISE_STATE_REJECTED;

export interface CreateInspectOptions {
getPromiseState?: (p: Promise<unknown>) => [PromiseState, unknown];
}

export interface InspectOptions {
depth?: number;
refs?: Map<{}, number>;
}

export function createInspect(opts?: CreateInspectOptions) {
const { getPromiseState } = opts ?? {};

/**
* Inspects a given value and returns a string representation of it.
* The function uses ANSI color codes to highlight different parts of the output.
* It can handle and correctly output different types of values including primitives, functions, arrays, and objects.
*
* @param v The value to inspect.
* @param opts Options which may modify the generated string representation of the value.
* @returns A string representation of `v` with ANSI color codes.
*/
function inspect(v: unknown, opts: InspectOptions = {}): string {
const { depth = 1 } = opts;

// Primitives
if (typeof v === 'boolean') {
return bold(yellow(v));
}
if (typeof v === 'number') {
return bold(yellow(isNegZ(v) ? '-0' : v));
}
if (typeof v === 'bigint') {
return bold(yellow(`${v}n`));
}
if (typeof v === 'symbol') {
return green(String(v));
}
if (typeof v === 'string') {
return green(JSON.stringify(v));
}
if (typeof v === 'undefined') {
return grey(String(v));
}
if (v === null) {
return bold(String(v));
}

// After this point, all should be typeof "object" or
// "function", so they can have additional properties
// which may be circular references.
const refs = opts.refs ?? new Map();
const refIndex = refs.get(v);
if (typeof refIndex === 'number') {
return cyan(`[Circular *${refIndex}]`);
}

refs.set(v, refs.size + 1);

// If the value defines the `inspect.custom` symbol, then invoke
// the function. If the return value is a string, return that.
// Otherwise, treat the return value as the value to inspect
if (v && typeof (v as any)[inspect.custom] === 'function') {
const c = (v as any)[inspect.custom]({ ...opts, depth });
if (typeof c === 'string') {
return c;
}
v = c;
}

if (typeof v === 'function') {
const { name } = v;
if (String(v).startsWith('class')) {
return cyan(`[class${name ? `: ${name}` : ''}]`);
}
return cyan(`[Function${name ? `: ${name}` : ' (anonymous)'}]`);
}
if (Array.isArray(v)) {
const contents =
v.length === 0
? ''
: ` ${v
.map((e) => inspect(e, { ...opts, refs, depth }))
.join(', ')} `;
return `[${contents}]`;
}
if (isRegExp(v)) {
return bold(red(String(v)));
}
if (isDate(v)) {
return magenta(v.toISOString());
}
if (isPromise(v)) {
let val = '';
const [state, result] = getPromiseState?.(v) ?? [
PROMISE_STATE_UNKNOWN,
undefined,
];
if (state === PROMISE_STATE_PENDING) {
val = cyan('<pending>');
} else if (state === PROMISE_STATE_UNKNOWN) {
val = grey('<unknown>');
} else {
if (state === PROMISE_STATE_REJECTED) {
val = `${red('<rejected>')} `;
}
val += inspect(result, { ...opts, refs, depth });
}
return `Promise { ${val} }`;
}
if (isError(v)) {
const { stack } = v;
const obj =
Object.keys(v).length > 0
? ` ${printObject(v, { ...opts, refs, depth })}`
: '';
return stack ? `${v}\n${stack.trimEnd()}${obj}` : `[${v}]${obj}`;
}
if (isArrayBuffer(v)) {
const contents = new Uint8Array(v);
const b: string[] = [];
for (let i = 0; i < Math.min(50, v.byteLength); i++) {
b.push(contents[i].toString(16).padStart(2, '0'));
}
let c = b.join(' ');
if (contents.length > 50) c += '...';
const len = inspect(v.byteLength);
return `ArrayBuffer { ${cyan(
'[Uint8Contents]',
)}: <${c}>, byteLength: ${len} }`;
}
if (isTypedArray(v)) {
const b: string[] = [];
for (let i = 0; i < Math.min(50, v.length); i++) {
b.push(inspect(v[i]));
}
let c = b.length === 0 ? '' : ` ${b.join(', ')} `;
if (v.length > 50) c += '...';
return `${getClass(v)}(${v.length}) [${c}]`;
}
if (typeof v === 'object') {
return printObject(v, { ...opts, refs, depth });
}
return `? ${v}`;
}

inspect.custom = Symbol('inspect.custom');
inspect.keys = Symbol('inspect.keys');
inspect.values = Symbol('inspect.values');
inspect.entries = Symbol('inspect.entries');

function printObject(v: any, opts: InspectOptions) {
const { depth = 1 } = opts;
const keys = new Set([...(v[inspect.keys]?.() || []), ...Object.keys(v)]);
const ctor = v.constructor;
const className =
ctor && ctor !== Object && typeof ctor.name === 'string'
? `${ctor.name} `
: '';
let contents = '';
let endSpace = '';
let len = 0;
const parts: string[] = [];
if (typeof v[inspect.values] === 'function') {
for (const val of v[inspect.values]()) {
const l = inspect(val, { ...opts, depth: depth + 1 });
len += l.length;
parts.push(l);
}
}
if (typeof v[inspect.entries] === 'function') {
for (const [k, val] of v[inspect.entries]()) {
const l = `${inspect(k, {
...opts,
depth: depth + 1,
})} => ${inspect(val, { ...opts, depth: depth + 1 })}`;
len += l.length;
parts.push(l);
}
}
for (const k of keys) {
const l = `${k}: ${inspect(v[k], { ...opts, depth: depth + 1 })}`;
len += l.length;
parts.push(l);
}
if (parts.length > 0) {
if (len > 60) {
const space = ' '.repeat(depth);
contents = parts.map((p) => `\n${space}${p},`).join('') + '\n';
endSpace = ' '.repeat(depth - 1);
} else {
contents = ` ${parts.join(', ')} `;
}
}
return `${className}{${contents}${endSpace}}`;
}

return inspect;
}

function isNegZ(n: number) {
return 1 / n === -Infinity;
}

function getClass(v: unknown) {
return Object.prototype.toString.call(v).slice(8, -1);
}

function isDate(v: unknown): v is Date {
return getClass(v) === 'Date';
}

function isError(v: unknown): v is Error {
return getClass(v) === 'Error';
}

function isRegExp(v: unknown): v is RegExp {
return getClass(v) === 'RegExp';
}

function isArrayBuffer(v: unknown): v is ArrayBuffer {
return getClass(v) === 'ArrayBuffer';
}

function isTypedArray(v: unknown): v is ArrayLike<number> {
return getClass(v).endsWith('Array');
}

function isPromise(v: unknown): v is Promise<unknown> {
return getClass(v) === 'Promise';
}
15 changes: 15 additions & 0 deletions packages/inspect/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"outDir": "dist",
"target": "es2022",
"declaration": true,
"sourceMap": true,
"moduleResolution": "node",
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
},
"include": [
"src/**/*.ts"
]
}
7 changes: 7 additions & 0 deletions packages/inspect/typedoc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"$schema": "https://typedoc.org/schema.json",
"extends": ["../../typedoc.json"],
"name": "@nx.js/inspect",
"entryPoints": ["./src/index.ts"],
"out": "docs"
}
Loading

0 comments on commit e7e769e

Please sign in to comment.