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

@sticklightapp/sandbox-logger

v0.0.23

Published

The log record shape every sandbox type emits, plus the emitters for the React/Vite sandbox.

Readme

@sticklightapp/sandbox-logger

The log record shape every sandbox type emits, plus the emitters for the React/Vite sandbox.

Why

Sandboxes used to write logs like this — valid JSON, but every field worth querying concatenated into one string:

{ "level": "error", "timestamp": 1785655783509, "pid": 132, "hostname": "f3022ba4-…",
  "message": "7:29:43 AM [vite] Internal server error: `@custom-variant` cannot be nested.\n  Plugin: @tailwindcss/vite:generate:serve\n  File: /home/sandbox/src/index.css\n      at …" }

You cannot filter by file, group by error type, or count distinct failures. Now:

{ "ts": 1785655783509, "level": "error", "source": "devserver",
  "errorType": "Internal server error",
  "message": "`@custom-variant` cannot be nested.",
  "plugin": "@tailwindcss/vite:generate:serve",
  "file": "src/index.css",
  "stack": "at …" }

The contract

A sandbox type is conformant if every line of /tmp/dev.log parses as JSON against SandboxLogRecord. That is the whole contract. A new sandbox type — different framework, different runtime, not even Node — writes its own emitter and stays compatible, because consumers read the schema rather than any particular emitter.

See src/schema.ts for the field list. Three fields carry the weight:

  • source — devserver | browser | command | build. Lets all streams in a sandbox be read as one timeline.
  • event — stable machine name for lifecycle records (build-error, build-success, hmr-update, page-reload). Consumers match on these, so treat them as API.
  • raw — set only when parsing was partial. Nothing is ever dropped, and parser gaps are findable by querying for its presence.

Paths are always repo-relative (src/pages/Index.tsx), never absolute sandbox paths.

Getting fields, not strings

Vite hands logger.error(msg, { error }) a RollupError carrying plugin, id, loc and frame as real values, then formats them into a human-readable string. Read them off the error and nothing is lost; parse the formatted string afterwards and you are guessing.

createViteLogger takes the first path. parseViteMessage is the fallback for messages that never reach vite's logger — anything a plugin or dependency writes straight to console.error.

Its fixtures are verbatim lines from /tmp/dev.log on running production sandboxes. Keep them that way: a hand-written fixture stops the test proving we can parse what vite actually emits.

Usage

import { createSandboxLogger, createViteLogger, writeRecord } from '@sticklightapp/sandbox-logger';

const sink = createSandboxLogger();
const emit = (record) => writeRecord(sink, record);

export default defineConfig({ customLogger: createViteLogger(createLogger(), emit) });

Adding a sandbox type

  1. Emit SandboxLogRecord NDJSON to /tmp/dev.log, one record per line.
  2. Set source correctly and reuse the existing event names where the concept matches.
  3. If the runtime formats errors before you see them, write a parser like parseViteMessage and test it against real captured output — not invented samples.

Publishing - required before the template can use it

The template depends on this package by exact version ("@sticklightapp/sandbox-logger": "0.0.9"), and the snapshot image runs a bare npm install inside the sandbox (libs/snapshot-tools/src/lib/build-snapshot-image.ts) with no .npmrc and no token. It can only reach the public npm registry, so this package must be published there. Unpublished, that install fails and no snapshot can be built.

Three things make that work, and all are easy to get wrong:

  1. It lives in packages/, not libs/. .github/workflows/deploy-package.yaml decides whether to publish by looking for packages/<name>/project.json carrying a deploy:npm tag. A package under libs/ sets should_deploy=false and is skipped silently - no error, no publish.
  2. The nx project name in project.json must be the bare directory name, not the scoped package name. The release tag is {projectName}/{version}, and the publish gate derives the directory with cut -d'/' -f1. Name the project @sticklightapp/sandbox-logger and the tag becomes @sticklightapp/sandbox-logger/<version>, so the gate looks for packages/@sticklightapp/ and skips - silently again. package.json keeps the scoped name; only project.json is bare, same as sandbox-internal-sdk and sticklight-cli.
  3. publishConfig.registry must be npmjs. The same workflow routes anything else to GitHub Packages, which is private and unreachable from a sandbox.
  4. dist/ must be a valid package root. The publish job runs pnpm version and npm publish inside DIST_DIRECTORY, so dist/package.json has to exist with dist-relative paths - tsc alone does not produce one, and the job dies on Cannot find module './package.json'. That is what scripts/prepare-package-dist.mjs is for, mirroring sandbox-internal-sdk. Note files there cannot be ["dist"] as it is in the repo-root manifest, or the tarball would be empty.

Rollout - three ordered steps

All three steps are done as of 2026-09-14 - 0.0.9 on npm, baked into supabase-react-template:1.12.0, and pinned. Keep the order if you ever redo them.

  1. nx release for this project -> GitHub release sandbox-logger/<version> -> Deploy Package publishes it. release.yaml only releases affected projects, and its nx release step pushes to master, so a concurrent release can lose the race and drop the tag. If no sandbox-logger/<version> release appears, re-run the release on master. Do not reach for _Manual Package to get a version the template can pin. It publishes <latest-tag>-<branch>-<sha> (microservice-pre-job.yaml), so you get a branch-named prerelease like 0.0.7-my-branch-6e25132 - fine for unblocking or testing, wrong to bake into a production image. Only the release path produces a clean semver. Verify it landed: curl -s -o /dev/null -w "%{http_code}" https://registry.npmjs.org/@sticklightapp%2Fsandbox-logger should print 200. Query the registry directly: a local .npmrc scoping to GitHub Packages makes npm view report a false 404.
  2. pnpm nx run snapshot-tools:create-snapshot - runs npm install in the image, which is why step 1 comes first.
  3. Bump DEFAULT_DAYTONA_TEMPLATE in apps/api-platform/src/core/services/sandbox/provider/daytona/daytona-sandbox-provider.ts to the new snapshot version. It is pinned, so new sandboxes keep the old image until it is raised.

Note vite.config.ts imports this package at config load time, so Node - not vite - resolves it. The resolve.alias block does not apply to the config file's own imports, only to app code. An alias cannot substitute for publishing.

Rollout note

Template changes only reach new sandboxes, via a new snapshot. Existing sandboxes keep the vite.config.ts they were created with, so both record shapes will coexist in the wild until they cycle. Consumers must tolerate a record with no source field.

Status

As of 2026-09-16 structured logging is on for new sandboxes: the template pins 0.0.12, supabase-react-template:1.13.5 bakes it, and DEFAULT_DAYTONA_TEMPLATE points there. AI Lab pins its own image (SNAPSHOT_NAME in api-ai-lab) and is still on 1.10.8, so it emits the legacy shape.

Two things this rollout learned the hard way. The pin here and the published version drift silently: 1.13.0 through 1.13.4 were all baked while the template still pinned 0.0.9, so they shipped without the field fix in 0.0.12 and nothing in the bake path complained. And versions above the one we pin can already exist in Daytona unpinned, so check the registry before you pick a number - create-snapshot's patch bump is safe (real semver), but check-snapshot's "(latest)" label is not: it sorts by string, so it calls 1.9.3 newer than 1.13.4.

Never pin a version at or below 0.0.7. All of them, plus 0.0.8, write {,"ts":...} on every line - pino's envelope separator with an empty envelope - which fails JSON.parse. toSandboxLogRecords skips lines it cannot parse, so a sandbox on those versions reports no devserver records at all, silently, with nothing in any error log to explain it. Every check short of booting a sandbox and reading /tmp/dev.log passes, check-snapshot included, since it only inspects baked files.

So when you bake, verify by booting the image, starting the dev server and confirming every line parses - not just that the file has content. Break a file and request it over HTTP too: vite only transforms a module when something asks for it, so the error path stays silent otherwise.

One thing to know about that path if you change buildRecord: vite hands customLogger.error both a RollupError and its own formatted message, and neither is complete on its own. The error object has real loc and frame for plugin errors but nothing at all for swc, which throws a plain Error with its position inside the message text. The message carries vite's Internal server error / Pre-transform error label, which the error object has no equivalent for. So the record reads the error first and backfills from the parsed message - reading only one of the two silently empties fields, which is exactly how errorType, line, col and frame went missing before.

Note the message has no 7:33:40 AM [vite] prefix on this path: vite's own logger adds that only when it writes to the console. parseViteMessage therefore treats the prefix as optional.

The template imports this package in vite.config.ts at config load time, so if it misbehaves the dev server does not start at all.