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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ui-organized/ui-inspect-plugin

v0.1.1

Published

Bundler-agnostic dev-server plugin for UI.Inspect — discovers design tokens from disk, injects the inspector in dev only, and writes copy edits back to source.

Readme

@ui-organized/ui-inspect-plugin

The bundler-agnostic half of plugin mode (SPEC §5). @ui-organized/ui-inspect-vite is a ten-line wrapper around this; Webpack, Rspack, Rollup and esbuild wrappers hang off the same factory when their targets are proven.

What it does

discover ──▶ read ──▶ push ──────────ws──────────▶ parse ──▶ Engine
config.json  disk     uiinspect:tokens             core       setRawTokens()
  ↑                                                             │
  └── watch the file, re-push on change      uiinspect:tokens-loaded
                                             (count for the log line)

locate ◀── write ◀──────────────ws──────────────── edit copy in the panel
src/App.tsx:42   uiinspect:apply                   │
  │                                                │
  └──▶ uiinspect:applied ────────────────────▶ overlay dropped; HMR
       (one result per change)                 renders from the file
  1. apply: "serve" — dev only. The plugin does not participate in a production build at all (§0.7 #1).
  2. Discovers configuiinspect.config.json at the project root; every key optional, the file itself optional.
  3. Discovers tokens — an explicit tokens.path, else sniffing in §5.2's order: tailwind.config.*tokens.json / *.tokens.jsontheme.ts → a stylesheet that actually declares custom properties. Nothing found degrades to the page's own :root properties, the same thing the bookmarklet does.
  4. Reads from disk — the capability that separates plugin mode from the bookmarklet. DTCG $description/$extensions, alias references, and SCSS variables the compiler folds away all exist in the source and never survive into the runtime DOM. .ts/.js themes are evaluated through the bundler's own module loader, so a tailwind.config.ts works.
  5. Injects the panel via a virtual module appended in transformIndexHtml. The app imports nothing (§0.7 corollary).
  6. Watches the token file and re-pushes on change, so editing a token updates drift and the pickers live, without a reload.

Division of labour

The plugin locates and reads; core parses. Every adapter therefore has exactly one implementation — the one core already tests — instead of a second copy living in a node process. What crosses the wire is the file's content plus enough context to parse it:

interface ProjectTokens {
  path: string;                                  // project-relative, for display
  kind: "css" | "dtcg" | "json" | "tailwind";
  text?: string;                                 // stylesheet or JSON, verbatim
  data?: unknown;                                // an evaluated theme module
  root?: string;                                 // which selector (css only)
}

The client calls parseProjectTokens(payload) and hands the result to engine.setRawTokens(tokens, origin). The Engine's source becomes project, and the panel names the real file.

Tailwind

The theme surface is theme with theme.extend merged over it — deliberately not resolveConfig. Resolving would pull in Tailwind's entire default palette, making nearly every colour on the page read as "on-token" and inflating the drift score (§8). The project's own declared theme is the honest answer.

Writing copy back to source

Edit an element's text in the panel, then Apply to source in the Changes tab. The plugin finds the string in your project and rewrites it, the dev server reloads, and the overlay disappears — the edit stops being a preview and becomes the file.

Finding the string is the whole problem, and it is solved in three steps:

  1. Source location, free. React's dev JSX transform already records __source, which React hangs off the fiber as _debugSource. §5.3 says to check for that before writing a Babel plugin, and it is there — so a text edit carries src: "src/App.tsx:42:6" with no extra tooling. Vue's __file works the same way. React ≥19 dropped _debugSource; those projects fall through to step 3.
  2. Whitespace-tolerant matching. JSX collapses a wrapped text child into one line, so the string the browser reports is often nowhere in the file literally. Matching happens on a whitespace-collapsed copy with an index map back to the original offsets, so copy written across three indented lines is found and replaced as one span.
  3. Widening search. A <h2>{title}</h2> reports the h2's line while the copy lives in a title="Counter" prop in a different file, so a miss at the hint widens to the whole file, then to the project.

It writes only when exactly one place could have produced the string. Two matches is a refusal, not a coin flip:

✕ "Save" appears in more than one file — too ambiguous to write
✕ "Clicks: 42" is not in any source file — it may be interpolated or come from data
✕ new text contains < or >, which would break the JSX it sits in

Refused changes stay staged and still export, so nothing is lost. Give ambiguous copy a unique edit, or apply it by hand.

Replacements are validated against the syntax they land in: JSX text rejects <, >, {, }; a quoted attribute rejects its own delimiter and newlines; a template literal rejects ${. The writer refuses rather than escaping, because escaping means generating code you did not write in a file you own.

Safety

Never reaching production:

  • apply: "serve", so the plugin never runs in a production build.
  • The injected client sits inside if (process.env.NODE_ENV !== "production"), which a bundler folds away — defence in depth (§0.7 #2).
  • The panel import is dynamic and inside that guard, so there is never a static edge from app code into the tool.
  • npm run check:prod builds examples/vite-react for production and fails on any occurrence of __uii / uiinspect / mountInspector in the output.

Writing to your repo (§5.6). Every write must satisfy all of:

  • the target resolves inside the project root after symlinks — a traversal, an absolute path elsewhere, and a symlink escaping the root are all refused;
  • the file is an editable source type, and outside node_modules;
  • it matches write.allowPaths, when you configure one;
  • the old text matches exactly one place in the searched region;
  • the replacement is legal in the syntax it lands in;
  • the file is replaced via temp file + rename, so a crash cannot truncate it;
  • every write is printed to the dev server console: [uiinspect] wrote src/App.tsx:69 — "Plugin mode" → "Plugin mode works".

Use dryRun: true to locate and validate without writing anything.

Copy is all this writes. Token writers, staging and the PR flow are Phase 4b; write.format / tokensPath and the github keys are parsed and carried, unused.

Options

uiInspect({
  enabled: true,        // false turns the panel off without removing the plugin
  expanded: false,      // start open instead of collapsed to the launcher
  tokens: { path: "./src/tokens.css", root: ":root", include: "", exclude: "" },
  allowPaths: ["src/**"],   // restrict what copy edits may touch
  dryRun: false,            // locate + validate, write nothing
  clientEntry: "@ui-organized/ui-inspect",  // advanced
});

Anything set in uiinspect.config.json is the default; these options override it.