Storybook translation

Statically inspect supported React CSF and produce reviewable shadcn registry artifacts.

Storybook is the upstream source for component implementation, stories, tests, and documentation. Compify translates a supported story source bundle into a shadcn registry item; it does not replace Storybook or promise faithful capture of Storybook runtime behavior.

The CLI and @compify/storybook addon described here are present in the current repository source candidate. The current CLI 0.2.0 and addon 0.1.0 manifests are not published releases. The repository's v0.1.0 GitHub source tag already contains a CLI 0.2.0 manifest; npm's older @compify/cli@0.1.0 came from different source history, so the two historical artifacts must not be conflated. The project-operated API is a public alpha rather than a generally available managed service or SLA.

CLI workflow

Build and link the CLI from source, then run it from the React package containing the stories:

compify storybook inspect src/components/Button.stories.tsx
compify storybook inspect src/components/Button.stories.tsx --json

compify storybook export src/components/Button.stories.tsx \
  --name button --output .compify/button.registry.json

COMPIFY_API_URL=https://api.example.com compify login -t <token>
COMPIFY_API_URL=https://api.example.com compify storybook publish \
  src/components/Button.stories.tsx \
  --publishing-name button --visibility private

The story-file argument is optional only when exactly one *.stories.* file can be inferred under the working directory. When several exist, specify one. Use --cwd <path> to choose another working directory. The CLI resolves the component source from the default meta component import (including the common const meta = { component: Button } satisfies Meta; export default meta form). Use --component-entry <path> to override or supply that source when it cannot be inferred:

compify storybook inspect src/components/Button.stories.tsx \
  --component-entry src/components/Button.tsx

See the CLI reference for other options.

inspect reports all named stories discovered in the selected file by default. Pass --story <export-name> to inspect, export, or publish one exact named story:

compify storybook inspect src/components/Button.stories.tsx --story Primary
compify storybook export src/components/Button.stories.tsx \
  --story Primary --output .compify/button.registry.json

Without --story, every named story in the file is selected and must be portable for export or publish. With it, portability diagnostics belonging only to other named stories do not block the selected story. The component source bundle is still resolved from the file's default meta component (or --component-entry).

Supported source boundary

The implementation is intentionally constrained to React CSF source files named *.stories.tsx, *.stories.ts, *.stories.jsx, *.stories.js, *.stories.mjs, or *.stories.cjs.

It also recognizes the narrow CSF Next factory form const meta = preview.meta({ component }) where preview is imported, followed by export const Name = meta.story({ ...static metadata... }). It never imports or executes that preview. Story.extend, Story.input, Story.composed, computed factory inputs and imported/dynamic configuration remain unsupported; real Storybook 9/10 fixtures are still required before this is a broad claim.

It currently recognizes:

  • a classic CSF default export or the narrow factory meta above;
  • CSF3 named variable exports;
  • CSF2 named function exports, including static .args and .storyName assignments;
  • JSON-static story args and static story names, including bounded local const aliases and static object/array spreads;
  • static includeStories / excludeStories arrays or regular expressions;
  • local export-list aliases such as export { primary as Primary };
  • a local component import referenced by default meta component, including a default-meta variable alias, or an explicit --component-entry; and
  • static local imports/re-exports, string-literal import() and require() targets, and bare dependencies reachable from that component entry; and
  • exact, package-local aliases from the nearest in-package tsconfig.json (compilerOptions.baseUrl and one-target paths entries) or the package root's exact string-valued package.json#imports entries.

Alias configuration is parsed as bounded JSON/JSONC text and is never imported or executed. Compify does not follow tsconfig extends. Wildcard keys or targets, multiple fallback targets, conditional/array package imports, absolute or external targets, symlink targets, and targets outside the story's package root fail closed when relevant (an extends entry always fails closed). The resolver only searches from an explicitly configured baseUrl; merely having a tsconfig.json does not turn bare package imports into local paths.

Because shadcn consumers do not inherit the publisher's TypeScript or package alias configuration, every accepted JavaScript/TypeScript alias edge is rewritten to a deterministic relative specifier in the emitted files. Alias syntax in stylesheets is rejected rather than emitted broken. Ordinary relative imports are preserved unchanged.

Static literals include strings, numbers, booleans, null, literal arrays and objects, expression-free template literals, and simple unary literal values. Dynamic expressions are not evaluated. Dynamic args make that story non-portable and produce a warning; missing recognized meta or no named story exports is an error. Inspection reports non-portability; export and publish fail instead of emitting or uploading a partially portable artifact.

The exported registry item contains the component entry and its transitive text files and runtime dependencies, plus story metadata, source provenance when available, and a stable SHA-256 digest. Conventional entry/component, lib, hook, and components/ui paths receive native shadcn file types so the pinned client maps helpers to the corresponding consumer aliases instead of flattening them into the component directory. Arbitrary target policy remains unsupported. Review this derived output before publishing.

inspect --json includes a deterministic source-graph sidecar with normalized file hashes, media kinds, import edges, original specifiers, resolution reasons, and unresolved edges. --explain <path> prints the inclusion chain for one file. This evidence is deliberately outside the registry/publish digest contract so adding explanations cannot silently change the install artifact.

For a real cross-app proof, storybook handoff exports locally, invokes pinned native shadcn without a shell, optionally runs an explicit consumer build, and writes a digest-verifiable receipt. A receipt is built evidence only when that build succeeds; no publish request is made. Snapshot traversal is bounded and never follows symlinks. A native-command failure writes no success receipt, removes only Compify's unchanged temporary artifact, and may leave consumer changes for the operator to review before retrying.

Static parsing and security

Inspection parses the abstract syntax tree; it never imports or executes the story or component modules. In particular, it does not run decorators, loaders, play functions, render functions, hooks, environment reads, or build plugins.

The bundler also:

  • confines the entry and local imports to the working/package roots, including resolution through symbolic links;
  • rejects absolute local imports and unresolved local imports;
  • includes only supported text extensions;
  • refuses likely credential/key files, private-key content, high-confidence provider token formats, and binary content;
  • limits a bundle to 500 files, 256 KiB per file, and 5 MiB total UTF-8 component source;
  • rejects case-colliding paths; and
  • normalizes line endings and sorts output for deterministic digests.

These controls reduce exposure; they do not certify source as safe or suitable for publication. Static text can still contain proprietary code, tokens in unrecognized formats, personal information, or undesired files. Inspect the file list and artifact, use least-privilege credentials, and publish privately until review is complete.

Limitations

Compify does not currently promise:

  • Vue, Svelte, Angular, Web Components, or other renderer conversion;
  • evaluation of variables, spreads, functions, computed keys, or imported data to recover dynamic args/names;
  • semantic capture of decorators, loaders, play functions, parameters, global configuration, Storybook/bundler aliases, wildcard or inherited TypeScript aliases, stylesheet aliases, or Storybook preview runtime state;
  • asset/binary bundling or arbitrary file types;
  • automatic conversion of a story render function into a reusable component;
  • perfect dependency discovery for runtime or dynamically loaded dependencies;
  • combining named stories from different story files into one artifact; or
  • automatic synchronization when upstream source changes.

An inspection with no error diagnostics means the supported static bundle can be produced. It does not mean the installed component will visually or behaviorally match every Storybook configuration. Test the resulting source in a consumer.

@compify/storybook addon

The addon is a thin manager-only panel and toolbar indicator for explicit parameters.compify metadata. Build it from repository source:

cd packages/storybook
bun install --frozen-lockfile
bun run build

When the package is available in your Storybook environment, add it to .storybook/main.ts:

export default {
  addons: ["@compify/storybook"],
};

Provide author-owned metadata on a story:

import type { CompifyParameters } from "@compify/storybook/parameters";

const compify = {
  status: "portable",
  component: "@acme/button",
  registry: "https://registry.example.com/r/acme/button.json",
  installCommand:
    "bunx shadcn@4.16.2 add https://registry.example.com/r/acme/button.json",
  previewUrl: "https://storybook.example.com/?path=/story/button--primary",
  reasons: ["Reviewed static bundle"],
} satisfies CompifyParameters;

export const Primary = {
  parameters: { compify },
};

Supported statuses are portable, partial, not-portable, and unknown. The addon reads only this explicit metadata. It does not inspect source, read credentials or environment variables, make network requests, execute displayed commands, publish files, or create a local bridge. Only HTTP(S) preview URLs are clickable. Its status is an author assertion, not the result of CLI inspection.

Use the CLI separately for static inspection, export, and authenticated publish. The default https://api.compify.app currently includes POST /cli/publish-story as a project-operated public-alpha surface. Set COMPIFY_API_URL or the global --api-url to use an operator-controlled self-hosted API implementing the same current-source contract. Neither option turns the unpublished CLI/addon candidates into releases; the alpha has no managed-service SLA, while a self-hosted operator owns its service obligations. public items are indexed, unlisted items are direct-address only, and private items are owner-only. The addon and CLI do not currently synchronize metadata automatically.