npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

inspect-utils

v1.0.1

Published

Utilities for adding custom inspect information to objects

Downloads

63

Readme

🌺 inspect-utils 🌸

standard-readme compliant

🌅🌸 Gorgeous inspect output for your custom classes. 🌺🌄

Table of Contents

Install

$ pnpm i inspect-utils

Motivation

Let's say you write a class that uses getters to define its main public interface:

class Point {
  #x: number;
  #y: number;

  constructor(x: number, y: number) {
    this.#x = x;
    this.#y = y;
  }

  get x() {
    return this.#x;
  }

  get y() {
    return this.#y;
  }
}

Since x and y are not data properties, the default Node inspect output is:

console.log(new Point(1, 2));

Point {}

This is not very useful. Let's fix that:

import { DisplayStruct } from "inspect-utils";

class Point {
  #x: number;
  #y: number;

  constructor(x: number, y: number) {
    this.#x = x;
    this.#y = y;
  }

  [Symbol.for("nodejs.util.inspect.custom")]() {
    return DisplayStruct("Point", {
      x: this.#x,
      y: this.#y,
    });
  }

  get x() {
    return this.#x;
  }

  get y() {
    return this.#y;
  }
}

Now you get the inspect output you were expecting:

console.log(new Point(1, 2));

Point { x: 1, y: 2 }

Features

In addition to DisplayStruct, which creates inspect output with labelled values, there are multiple other styles of inspect output.

Tuples: Unlabeled Instances

If you have a class that represents a single internal value, representing the value as { label: value } is too noisy.

In this case, you can use DisplayTuple to create less verbose inspect output:

class SafeString {
  #value: string;

  [Symbol.for("nodejs.util.inspect.custom")]() {
    return DisplayTuple("SafeString", this.#value);
  }
}

Now, the inspect output is:

SafeString('hello')

You can pass multiple values to DisplayTuple as an array, and they will be comma-separated in the output.

class SafeString {
  #value: string;
  #verified: "checked" | "unchecked";

  constructor(value: string, verified: "checked" | "unchecked") {
    this.#value = value;
    this.#verified = verified;
  }

  [Symbol.for("nodejs.util.inspect.custom")]() {
    return DisplayTuple("SafeString", [this.#value, this.#verified]);
  }
}

SafeString('hello', 'checked')

Units: Valueless Instances

If you have an instance that represents a singleton value, you can use DisplayUnit to create even less verbose inspect output.

You can use descriptions with unit-style inspect output. You can also use unit-style inspect output for certain instances and more verbose inspect output for others.

import { DisplayStruct } from "inspect-utils";

type CheckResult =
  | { verification: "unsafe" }
  | { verification: "safe"; value: string };

class CheckedString {
  static UNSAFE = new CheckedString({ verification: "unsafe" });

  static safe(value: string): CheckedString {
    return new CheckedString({ verification: "safe", value });
  }

  #value: CheckResult;

  constructor(value: CheckResult) {
    this.#value = value;
  }

  [Symbol.for("nodejs.util.inspect.custom")]() {
    switch (this.#value.verification) {
      case "unsafe":
        return DisplayUnit("CheckedString", { description: "unsafe" });
      case "safe":
        return DisplayTuple("CheckedString", this.#value.value);
    }
  }
}

CheckedString[unsafe] and CheckedString('hello')

Descriptions

If you have a single class with multiple logical sub-types, you can add a description to the inspect output:

import { DisplayStruct } from "inspect-utils";

class Async<T> {
  #value:
    | { status: "pending" }
    | { status: "fulfilled"; value: T }
    | { status: "rejected"; reason: Error };

  constructor(value: Promise<T>) {
    this.#value = { status: "pending" };

    value
      .then((value) => {
        this.#value = { status: "fulfilled", value };
      })
      .catch((reason) => {
        this.#value = { status: "rejected", reason };
      });
  }

  [Symbol.for("nodejs.util.inspect.custom")]() {
    switch (this.#value.status) {
      case "pending":
        return DisplayUnit("Async", { description: "pending" });
      case "fulfilled":
        return DisplayTuple("Async", this.#value.value, {
          description: "fulfilled",
        });
      case "rejected":
        return DisplayTuple("Async", this.#value.reason, {
          description: "rejected",
        });
    }
  }
}

SafeString('hello', 'checked')

Annotations

Descriptions are useful to communicate that the different sub-types are almost like different classes, so they appear as labels alongside the class name itself.

Annotations, on the other hand, provide additional context for the value.

Let's see what would happen if we used annotations instead of descriptions for the async example.

import { DisplayStruct } from "inspect-utils";

class Async<T> {
  #value:
    | { status: "pending" }
    | { status: "fulfilled"; value: T }
    | { status: "rejected"; reason: Error };

  constructor(value: Promise<T>) {
    this.#value = { status: "pending" };

    value
      .then((value) => {
        this.#value = { status: "fulfilled", value };
      })
      .catch((reason) => {
        this.#value = { status: "rejected", reason };
      });
  }

  [Symbol.for("nodejs.util.inspect.custom")]() {
    switch (this.#value.status) {
      case "pending":
        return DisplayUnit("Async", { description: "pending" });
      case "fulfilled":
        return DisplayTuple("Async", this.#value.value, {
-         description: "fulfilled",
+         annotation: "@fulfilled",
        });
      case "rejected":
        return DisplayTuple("Async", this.#value.reason, {
-         description: "rejected",
+         annotation: "@rejected",
        });
    }
  }
}

In this case, the inspect output would be

SafeString('hello', 'checked')

📒 The unit style does not support annotations because annotations appear alongside the structure's value and the unit style doesn't have a value.

The decision to use descriptions or annotations is stylistic. Descriptions are presented as important information alongside the class name, while annotations are presented in a dimmer font alongside the value.

Display: Custom Formatting

You can also use the Display function to control the output format even more directly.

Declarative Use (The Whole Enchilada)

import { inspect } from "inspect-utils";

class Point {
  static {
    inspect(this, (point) =>
      DisplayStruct("Point", {
        x: point.#x,
        y: point.#y,
      }),
    );
  }

  #x: number;
  #y: number;

  constructor(x: number, y: number) {
    this.#x = x;
    this.#y = y;
  }
}

This does two things:

  • Automatically installs the Symbol.for("nodejs.util.inspect.custom") on instances of Point.
  • Sets Symbol.toStringTag to Point on instances of Point.

Production-Friendly Builds by Default (Using Conditional Exports)

If you are using a tool that understands conditional exports, using the declarative API above will automatically strip out the custom display logic when the "production" condition is defined.

Vite directly supports the "production" condition, and enables it whenever the Vite mode is "production"

The default condition includes import.meta.env.DEV checks, and is suitable for builds that know how to replace import.meta.env.DEV but don't resolve conditional exports properly.

Why Stripping Works: A Bit of a Deep Dive

This strategy assumes that you are using a minifier like terser in a mode that strips out no-op functions.

When the production export is resolved, the inspect function looks like this:

export function inspect(Class, inspect) {}

When using a function like that in standard minifiers with default compression settings, the call to the function, including callback parameters, is eliminated.

Check out this example in the swc playground.

Pasting the same code into the [terser playground] with default settings yields this output:

export class Point {
  static {}
  #t;
  #s;
  constructor(t, s) {
    (this.#t = t), (this.#s = s);
  }
}

Unfortunately, both terser and swc leave in empty static blocks at the moment. Hopefully this will be fixed in the future. In the meantime, the default behavior of this library with a minifier is to completely remove all custom inspect logic, which is the meat of the matter.

A reasonable bundler (such as rollup) should also avoid including any of inspect-utils's display logic in production, since you only use the inspect function directly, and the inspect function doesn't use any of the rest of inspect-utils's code in the production export.

The debug Condition

inspect-utils also provides an export for the debug-symbols condition, which does not strip out the custom display logic and is intended to be compatible with the production condition.

To use this, you will need to configure your environment with a "debug-symbols" condition that is higher priority than the "production" condition.

Maintainers

The Starbeam team.

Contributing

See the contributing file!

License

MIT © 2023 Yehuda Katz