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

@native-surface/playground

v0.1.0-alpha.4

Published

Component playground for native-surface: serve any React Native app's stories on the canvas engine - real Yoga layout, Skia paint, shareable URLs, no device.

Readme

@native-surface/playground

A component playground whose preview pane is a <canvas>. The chrome — sidebar, controls, actions — is ordinary DOM; the preview is exclusively NativeSurface from native-surface, which lays out with Yoga (WASM) and paints with Skia (CanvasKit). No React Native Web anywhere: inspect the preview and you find one canvas element, not a div tree.

It runs in two modes:

  • Inside a host app (the product): npx native-surface playground serves the playground UI against that app's story files, on that app's dependencies.
  • Standalone (this repo's demo): pnpm --filter @native-surface/playground dev serves the built-in src/stories/ demo set.

CLI

cd your-react-native-app
npm i -D @native-surface/playground   # native-surface provides the `native-surface` bin
npx native-surface playground         # http://localhost:5170
Options:
  --root <dir>       Host app directory (default: cwd)
  --port <n>         Dev server port (default: 5170)
  --stories <glob>   Story file glob relative to --root; repeatable
  --platform <os>    ios | android — file-extension resolution (default: ios)
  --open             Open the browser

Story discovery priority: --stories.native-surface/playground.config.mjs stories → the host's .storybook/main.{js,mjs,cjs} stories globs (resolved relative to the .storybook dir; @(a|b) alternation supported; a TS-only main.ts is not executed — you get a warning and the defaults) → the defaults src/**/*.stories.{tsx,ts,jsx,js} and **/*.play.{tsx,jsx}.

Adding or deleting a matching file live-reloads the story list; edits ride normal HMR.

shoot — headless screenshots / CI visual regression

npx native-surface playground shoot                      # PNGs into .native-surface/shots
npx native-surface playground shoot --diff shots-baseline            # compare
npx native-surface playground shoot --diff shots-baseline --update   # (re)create baseline

Renders every story in a headless Chromium against the same server the serve command runs, waits for fonts + the engine's flush, and captures the CANVAS ONLY — one <out>/<storyId>.png per story plus <out>/report.json. A story that hits the "not bridged yet" boundary is reported as skipped with the reason; a story that throws is an error. --diff <dir> compares each shot with <dir>/<storyId>.png via a built-in PNG decoder (no extra deps) and fails (exit 1) on any mismatch or missing baseline unless --update copies the current shots over. Deterministic by construction — fixed viewport (--viewport WxH, default 390x720), dpr pinned to 1, SwiftShader rendering — so --tolerance (differing-pixel fraction, default 0.001) rarely matters.

Requirements: a Chromium/Chrome binary (--browser flag, else $CHROME_PATH, else /usr/bin/chromium) and puppeteer-core resolvable from $NS_SHOOT_PUPPETEER_DIR, the host app, or the playground package (usually: npm i -D puppeteer-core in the app). Programmatic use: import { shoot } from '@native-surface/playground/src/shoot.mjs'.

Config file

.native-surface/playground.config.mjs (or .js) in the host root:

export default {
  stories: ['src/**/*.stories.tsx'], // optional; CLI --stories wins
  port: 5170,                        // optional; CLI --port wins
  storyPadding: 16,                  // inset between device chrome and canvas (0 = flush)
  decorators: 'none',                // sets storyPadding to 0 unless storyPadding is also set
  optimizeDeps: {                    // optional Vite escape hatch:
    exclude: ['react-native-paper'], //   host UI kits importing 'react-native'
    include: ['@callstack/react-theme-provider'], // their bare-CJS leaves
  },
  aliases: {                         // optional; exact-specifier overrides that
    'react-native': './rn-plus.tsx', //   OUTRANK the engine preset's aliases —
  },                                 //   patch a missing engine export locally
};

optimizeDeps.exclude keeps a UI kit that imports the aliased react-native out of Vite's prebundle (a dep chunk would freeze a private engine copy); include entries that only resolve from the host get an automatic exact alias so the optimizer finds them from the playground's Vite root. aliases values resolve against the host root when they start with . or /.

What the server adopts from the host

  • Dependency resolution: the whole nativeSurface() preset is re-rooted at the host (resolveFrom), so reanimated real-vs-shim detection, optimizeDeps and the compat aliases all follow the host's node_modules.
  • tsconfig paths → Vite aliases. Limits: one relative extends hop; single wildcard per key; first target of a multi-target array; bare baseUrl resolution is not emulated.
  • Reanimated's Babel plugin is applied to host story source when react-native-reanimated/plugin resolves from the host root (Metro parity for worklets in story files).

Import audit ("not bridged yet")

Any bare import that fails to resolve — typically a native module without a web bridge — is stubbed with a Proxy that lets the import succeed and throws <pkg> has no web bridge yet (native-surface) on first use. The story shows an amber "Not bridged yet" boundary instead of taking down the server; every stub is listed in the Audit panel and served as JSON at /__ns_audit. Caveats: a dependency probed with try/catch-import will see a successful import and fail later on use; unresolvable imports inside prebundled CJS deps are beyond the stub's reach.

Story format

CSF-shaped, close to CSF3. One file per component; export default or export const meta is the meta, every other export is a story.

import { Button } from './components/Button';
import type { Meta, Story } from '../story-types';

export const meta: Meta = {
  title: 'Button',                 // optional; defaults to the file name
  component: Button,               // rendered as <Button {...args} />
  args: { label: 'Save', onPress: () => {} },
  argTypes: { variant: { options: ['primary', 'secondary'] } },
  decorators: [centered],
  order: ['Primary', 'Disabled'],  // optional; see below
};

export const Primary: Story = { args: { variant: 'primary' } };
export const Disabled: Story = { args: { disabled: true } };

Supported CSF3 forms:

  • story objects with args (rendered as <meta.component {...args} />)
  • story-level render(args), which wins over meta.render, which wins over meta.component
  • a plain function export is a story (function = render); its storyName property overrides the display name, as name does for object stories
  • decorators on meta and story, composed story-first (innermost), each receiving (Story, { id, title, name, args, theme })
  • parameters on meta and story: merged (story wins) and stored on the entry; unknown keys are ignored
  • title-less meta and *.play.tsx files: the group title falls back to the file name

Not supported (deliberate v1 line, see docs/plays/playground-in-existing-app.md): play functions, loaders, globals, includeStories/excludeStories.

Args merge meta.args then story.args; the same for argTypes.

meta.order lists export names in sidebar order. It exists because ES module namespace objects are always key-sorted, so a file's declaration order is not recoverable at runtime; unlisted stories follow alphabetically.

Controls

Knobs are inferred from each arg's runtime value: string → text, number → number, boolean → checkbox, object/array → JSON textarea (applied as you type, invalid JSON just marks the field), function → a read-only "logged to actions" row. argTypes[name] overrides that:

| Field | Effect | | --- | --- | | options | Renders a <select>; values keep their original type | | labels | Display names for options, keyed by String(option) | | control | Forces a kind ('none' hides the knob) | | name | Label shown instead of the arg name | | description | Tooltip on the label |

Edits are per-story and live for the session; Reset restores the story's declared args.

Actions

Every function-valued arg is wrapped so calls log with name, a depth-capped JSON-ish payload, and a timestamp (arg tag). The surface's own onAction hook is wired too, so the engine reports presses it dispatches even for handlers the story did not pass (surface tag). Consecutive identical calls collapse into one row with a count.

Sharing

The selected story lives in the URL hash (#/story/<group>--<story>), so reloads and shared links reopen the same story; the toolbar's Copy link button copies the current address.

Layout

src/stories/components/ holds real React Native source — those files import from 'react-native' only, and vite.config.ts aliases that to the canvas renderer via reactNativeAlias(). tsconfig.json mirrors the alias with a paths entry so typechecking resolves the same module.

The CLI/server half lives in bin/cli.mjs + src/server/*.mjs (plain Node, no build step); createPlaygroundServer() in src/server/create-server.mjs returns an unlistened Vite dev server — the seam a future headless shoot (screenshot) mode plugs into.

Engine bugs and contract gaps found while building this live in ENGINE-ISSUES.md.