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

@pygmalionjs/pygmalion

v0.6.34

Published

Code-backed DOM design sandbox and visual QA editor

Readme

Pygmalion

English | Korean

Pygmalion is a code-backed visual editor for React applications. It renders real DOM and registered React components on a canvas, keeps shared component instances synchronized, and connects visual edits to reviewable source changes.

Why Pygmalion? The name comes from the myth of a sculptor whose creation came to life.

Behind every design is your React code.

What you can do

  • Browse registered screens, components, assets, and design tokens.
  • Navigate large screen sets from the sidebar or canvas.
  • Edit layout, styles, text, props, and supported component structure.
  • Choose between a shared-source edit and a single-screen override.
  • See shared edits update every affected frame immediately.
  • Inspect the source diff and affected screens before applying a change.
  • Compare baseline and changed screenshots with DOM and pixel QA.
  • Undo or redo canvas edits, or reset every uncommitted editor change.

Install

Pygmalion is distributed as the public npm package @pygmalionjs/pygmalion. No registry configuration or authentication token is required.

npm install --save-dev @pygmalionjs/pygmalion

Configure a host project

Create pygmalion.config.ts in the host application:

import path from "node:path";
import { definePygmalionProject } from "@pygmalionjs/pygmalion/vite";

const appRoot = import.meta.dirname;
const projectRoot = path.resolve(appRoot, "..");

export default definePygmalionProject({
  configRoot: appRoot,
  projectRoot,
  appRoot,
  appDirectory: "frontend",
  sourceDirectory: "src",
  source: { remote: "origin", branch: "dev" },
  mirrorRoot: path.resolve(projectRoot, "..", "app-dev-view"),
  inventory: {
    script: path.join(appRoot, "scripts", "generate-pygmalion-inventory.mjs"),
    outputRoot: appRoot,
    outputs: [
      "src/generated/design-registry.ts",
      "src/generated/design-inventory.ts",
    ],
    runtimeInputs: [".env.local"],
  },
  inspect: {
    normalizeValue(property, value) {
      return value;
    },
  },
});

inventory.outputs lists generator-owned paths relative to the application root. Pygmalion restores these paths in its dedicated mirror before switching revisions and checks again after generation; any change outside the declared outputs stops synchronization. Git does not report ignored files, so list ignored runtime or configuration files a generator could affect in inventory.runtimeInputs. Each entry is one file relative to the application root, with at most 64 files and 16 MiB total. Pygmalion compares their content directly before and after generation; it does not recursively scan .git, node_modules, or other unrelated directories.

Add the integration plugins to the host Vite configuration:

import { createPygmalionVitePlugins } from "@pygmalionjs/pygmalion/vite";
import pygmalionConfig from "./pygmalion.config";

export default defineConfig({
  plugins: [react(), ...createPygmalionVitePlugins(pygmalionConfig)],
});

Frameworks that expose webpack loaders through a JavaScript-only pipeline can ask the inspect loader to strip TypeScript after source instrumentation. A Turbopack rule should set transpile: true while keeping the original file extension, so relative TSX module resolution keeps the host's source identity:

{
  './src/**/*.tsx': {
    loaders: [{
      loader: require.resolve('@pygmalionjs/pygmalion/webpack-loader'),
      options: { root: process.cwd(), sourceDirectory: 'src', transpile: true },
    }],
  },
}

When the mirror preview reloads the host Vite configuration, PYGMALION_PREVIEW_MODE=1 prevents the integration plugins from starting recursively.

Mirror and session previews run as their own Vite processes, so the mode you pass to the editor does not reach them. Set preview.mode when a screen only exists under a specific env file — a feature flag that has to be off, a mock profile a recipe depends on:

export default definePygmalionProject({
  // ...
  preview: { mode: "e2e" },
});

Switch what the mirror renders

The mirror follows source.branch unless source.ref pins it. Either can be repointed at runtime, so comparing two revisions no longer means restarting the dev server:

const { switchSource, mirror } = usePygmalionProject();

await switchSource("origin/dev");
await switchSource(PYGMALION_WORKTREE_SOURCE_REF); // uncommitted work
mirror.sourceRef; // what it is tracking now

PYGMALION_WORKTREE_SOURCE_REF renders the current working tree by resolving it to a throwaway commit, leaving your branch, HEAD, and index untouched. It covers tracked changes only; untracked files raise a warning on the returned status.

Bind screen declarations to the selected revision

An editor host is normally built once, while its mirror can switch among many checkouts. Bundling routed pages into the host therefore lets a new preview run under an old screen catalog. Generate a JSON catalog inside every mirror checkout and let the exact-source endpoint serve it under a read lease:

export default definePygmalionProject({
  // ...
  inventory: {
    script: './scripts/generate-pygmalion-inventory.mjs',
    outputs: ['artifacts/pygmalion-revision-catalog.json'],
  },
  preview: {
    catalog: { file: 'artifacts/pygmalion-revision-catalog.json' },
  },
});

With the default inventory.script invocation, Pygmalion passes --source-root <path> --out-root <path> --commit <sha>. A custom inventory.args(context) receives the same exact context.commit when the generator uses another CLI shape.

The generated file uses the versioned, JSON-only contract exported as PygmalionRevisionCatalog. Stamp sourceRevision from the inventory generator's commit context and include routed screens.pages, screens.assets, tokens, and every screen declaration that can differ by revision. Put serializable registry metadata in componentRegistry; the editor binds it by name to the React implementations already bundled by the host and rejects a catalog whose implementation is unavailable. This keeps props, defaults, adoption, and code-generation metadata revision-bound without putting functions in JSON. Repository-owned generated-code intent belongs in codegenProject so its frame contracts and prompt provenance switch with the same catalog. Then opt the editor into the endpoint:

const { appOrigin, previewRevision } = usePygmalionProject();

<PygmalionEditor
  previewCacheNamespace="my-product"
  previewRevision={previewRevision}
  appOrigin={appOrigin ?? undefined}
  previewCatalogEndpoint="/__pygmalion-route-preview/artifact/catalog"
/>

Pass the runtime's full previewRevision; the editor derives its source SHA for the catalog request while retaining the session mutation counter for screen cache invalidation.

The editor validates both namespace and source revision before installation. While another checkout is preparing, it keeps the last complete catalog, preview origin, and interaction declarations together. A late response from an older dev → qa → dev request cannot overwrite the current selection. Invalid, permanently unavailable, or catalogs that produce no installable page fail closed on a dedicated error surface; the editor never relabels the previous canvas as the requested revision. A catalog with no routed screens remains installable when the host contributes non-screen library pages.

When onDesignChange is connected, each payload carries the exact sourceRevision that owned the edited batch. Its non-enumerable signal is aborted when that catalog is retired, allowing an in-process host adapter to cancel work without changing an existing JSON request body.

To offer a choice instead of asking for a ref string, list the revisions and render the shipped control in your toolbar:

const { sourceRefs, switchSource, mirror } = usePygmalionProject();

<SourceRefControl
  value={mirror.sourceRef}
  currentCommit={mirror.shortCommit}
  refs={sourceRefs.refs}
  defaultRef={sourceRefs.defaultRef}
  worktreeRef={sourceRefs.worktreeRef}
  busy={mirror.state === 'syncing'}
  refreshLabel="Refresh this branch"
  onChange={(ref) => void switchSource(ref)}
>
  {/* Optional: your own trigger, so the control reads as part of the toolbar. */}
  <span>{mirror.sourceRef ?? 'dev'}@{mirror.shortCommit}</span>
</SourceRefControl>;

The menu is a native select layered over that trigger, so keyboard access, mobile behavior, and placement stay with the platform while the painted trigger stays yours. Its first item explicitly reloads the selected revision, including when a branch tip moved while that same branch remained selected. Passing currentCommit marks the control with [data-update-available] and [data-latest-commit] when the listed ref is newer. [data-pygmalion-source-ref] and [data-busy] are also available for styling.

GET /__pygmalion-dev-control/refs backs this list with local branches, remote tracking branches, the default revision, and the worktree ref. loadSourceRefs() reloads it after branches change.

Preview artifacts are cached per source SHA, so switching invalidates that cache and the next capture runs again from scratch. A capture already running for the previous revision is discarded instead of published, so it cannot overwrite the artifact the editor moved to.

A design session's worktree is branched from one source SHA. Once it holds unapplied edits, switchSource refuses rather than leave the canvas on a revision those edits were never written against — apply or revert them first, or pass { discardEdits: true }.

Check a frame at the widths you support

A frame resizes by corner drag or by the W/H boxes. Name the widths your application actually guarantees and they become one-click buttons next to those boxes, so moving between them costs nothing to remember:

<PygmalionEditor
  viewportPresets={[
    { label: '480', width: 480, height: 800, description: 'Minimum supported width' },
    { label: 'Default', width: 1280, height: 800 },
  ]}
/>

Omit height to change width alone and leave the frame's height as it is. The button reads as active while the frame sits at that size, and Reset to default size still returns the frame to the viewport its manifest authored.

If the first declared canvas is a large asset catalog, open the working canvas directly and avoid rendering that catalog during startup:

<PygmalionEditor initialCanvas="Screens" />

Screens that never stop moving

A capture waits for the document to hold still. A screen with an elapsed clock, a level meter, or a marquee never does, so it fails on stabilize no matter how long it waits. Declare those regions and their mutations stop counting as churn:

captureStoryboardCase({
  // ...
  stability: {
    volatileSelectors: ['[data-testid="elapsed-time"]', '[data-live-meter]'],
  },
});

The wait itself samples every 100ms and needs three identical samples, up to a 4s ceiling — wide enough for a screen that repaints on a one-second beat. attempts, requiredStableSamples, intervalMs, and minimumWaitMs are adjustable for anything slower.

Register screens and scenarios

The host owns application-specific routes, fixtures, authentication, mock data, and screen names. Pygmalion owns preview scheduling, DOM capture, editing, source sessions, and QA.

Use createDesignScreenCollection to classify every discovered scenario as either:

  • a visual screen with a route or registered component fixture; or
  • a non-visual behavior with an explicit reason.

A scenario must appear in exactly one category. Invalid references, missing coverage, and duplicate classifications fail before the editor opens.

Screen recipes support click, focus, hover, focus-visible, active, fill, check, press, scroll, wait, and storage steps. Use deterministic recipes and assertions so each frame represents a repeatable application state. The screen state contract defines when a variation is a frame, an interaction state, a condition state, an editable parameter, a motion preview, or behavior-only QA. Rendered CSS and Web Animations targets are automatically available in the right-panel Motion controls with play, pause, reset, target isolation, and phase scrubbing.

Keep product routes separate from preview entry routes

Authentication and fixture gateways belong to the host application. When a screen needs one, keep route as the canonical product address and declare the routed gateway as previewRoute:

{
  id: 'screen:account-security',
  name: 'Account security',
  section: 'Account',
  route: '/account/security',
  previewRoute: '/__preview__/account/security',
}

Pygmalion boots, captures, caches, and interacts through previewRoute, while inventory, coverage, and source identity continue to use route. The gateway should be development-only, keep credentials out of URLs and client manifests, and render or redirect to the real routed screen rather than a separately assembled component fixture. Changing the gateway invalidates that frame's preview cache automatically.

Declared screenFlows support the same split. Put the canonical starting address in route and the host gateway in previewRoute; the flow boots the gateway and still reports the canonical route to capture preparation.

Keep user journeys explicit instead of relying only on source navigation inference. Paths expand into adjacent graph edges, and source-discovered edges are still merged underneath them:

const storyboard = {
  startScreenIds: ['screen:entry'],
  paths: [
    {
      id: 'checkout',
      label: 'Checkout',
      screenIds: ['screen:entry', 'screen:cart', 'screen:confirmation'],
    },
  ],
};

<PygmalionEditor storyboard={storyboard} />

Every id is checked against the resolved screen catalog. Unknown endpoints and unreachable screens stay visible as graph diagnostics instead of disappearing from the sidebar. Code-only redirects or fixtures can be classified through nonVisualScenarioIds with ids created by createStoryboardRouteScenarioId(route).

The editor shell and frame geometry mount before storyboard discovery finishes. Cached or live route content remains gated until baseline environment and source digests settle, so immediate canvas paint cannot consume a provisional preview identity. Selecting an already visible frame in the sidebar preserves the camera; an offscreen frame is revealed with the smallest pan possible and never changes the user's zoom.

Capture artifact lifecycle

Import the reusable capture primitives from @pygmalionjs/pygmalion/storyboard. Pygmalion owns isolated browser contexts, bounded scheduling, inert DOM snapshots, screenshots, QA diagnostics, compact versioned artifacts, exact revision validation, and atomic artifact publication. The Vite integration serves only an artifact whose namespace and source revision match the active editor.

The host application supplies routes, environment presets, mock adapters, interaction recipes, and assertions. A host hook may install domain-specific network fixtures, but it must not fork the browser or artifact lifecycle.

Artifact v3 preserves every registered frame with one explicit outcome: ready, rendered-with-qa-failure, or capture-error. QA failures can retain their last stable DOM and screenshot while CI remains red. A failed strict capture therefore cannot silently leave an older artifact active.

When previewArtifactEndpoint is configured, an embedded artifact with another namespace or source revision is treated only as a visibly outdated, last-known preview. It does not count toward data-preview-warm. Each frame exposes its exact-resolution lifecycle through data-artifact-resolution: checking, outdated, generating, error, or exact. Transport, validation, and generation failures stay visible on the frame with a retry action. A current live snapshot or exact endpoint response clears the stale state.

Editing workflow

For Icons, Atoms, and Components, select an item in Assets to open its catalog frame and select the source implementation automatically. The trailing + action remains insertion-only, so opening an asset never mutates another frame.

  1. Open Screens. Preview warm-up starts without requiring frame clicks.
  2. Select a frame from the sidebar to center it on the canvas.
  3. Select a DOM layer or registered component.
  4. Choose Shared component or This screen as the edit scope.
  5. Change layout, content, props, or supported structure.
  6. Review the affected frame list and generated source diff.
  7. Run visual QA, then apply the change to the isolated design session.
  8. Use Reset changes to discard all unapplied edits and editor history.

Shared edits update detailed active frames and lightweight inactive frames in the same render cycle. A frame does not need to be activated before it reflects a shared change.

Local development

npm install
npm run dev

Validation commands:

npm test
npm run typecheck
npm run build:lib
npm run lint:language

Project documentation