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

theme-forge

v1.0.0

Published

A powerful, database-free visual content editor for any frontend project. Scans source code with AST parsing to extract every text string and image, lets you edit them in a modern web UI, writes changes back to the exact source location, and rebuilds the

Readme

🔨 Theme Forge

An embeddable, database-free content/theme editor for any frontend project.

Install it into your app and get a full visual "Theme Customizer" inside your own admin panel. Click any text or image in a live preview and edit it in place, change theme colors, upload images, then save back to source and rebuildno database required. Your source code stays the single source of truth.

  • Visual / WYSIWYG editor — click text to edit inline, drag-and-drop images, pick colors, with undo/redo and keyboard shortcuts.
  • AST-based (Babel) — understands JSX/TSX and edits only real text, string attributes, image paths and colors, never your logic.
  • Offset-precise, auto-escaping writes straight back to your source files (correct escaping for JSX text, JSX attributes and JS/TS string literals).
  • Image uploads saved into your public/ folder + an image library browser.
  • Live rebuild via a child process, streamed to a build console (SSE).
  • Ships as two drop-in pieces: a Next.js API handler and a React panel.

Built for React / Next.js (App Router). Works on any project whose content lives in .js / .jsx / .ts / .tsx files.


Table of contents

  1. Requirements
  2. Install
  3. Quick start (Next.js App Router)
  4. Full configuration reference
  5. <ThemeForgePanel /> props
  6. Using the visual editor
  7. Per-theme / per-page preview
  8. Production: make changes appear without a manual restart
  9. What gets detected
  10. CLI helpers
  11. Programmatic API
  12. Troubleshooting
  13. How it works
  14. License

Requirements

  • A Next.js app using the App Router (app/ directory).
  • React 18+ and react-dom 18+ (peer dependencies — you already have them).
  • Node.js 18+.
  • Your editable content lives in .js / .jsx / .ts / .tsx files.

Not on Next.js? You can still use the programmatic API and the CLI to scan, edit and rebuild from your own server/scripts.


Install

npm install theme-forge
# or
pnpm add theme-forge
# or
yarn add theme-forge

Quick start (Next.js App Router)

Three small steps. After this you have a working customizer page in your admin.

Step 1 — Mount the API handlers

Create a catch-all route. The folder name [...tf] matters (it captures the sub-actions like scan, save, upload, images, build).

app/api/theme-forge/[...tf]/route.ts

import { createThemeForgeHandlers } from "theme-forge/next";

export const { GET, POST } = createThemeForgeHandlers({
  root: "src",                  // source dir to scan (use "." if no src/)
  publicDir: "public",          // your static folder
  imageUploadDir: "public/uploads",
  buildCommand: "npm run build", // run on "Save & Rebuild" (use your package manager)
});

// Theme Forge reads/writes files and spawns a build — keep it on the Node runtime.
export const dynamic = "force-dynamic";
export const runtime = "nodejs";

Step 2 — Externalize the Node-only dependencies

Theme Forge uses Babel + fast-glob on the server. Tell Next.js not to bundle them so the API route runs them natively.

next.config.ts (or next.config.js)

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  serverExternalPackages: [
    "@babel/parser",
    "@babel/traverse",
    "@babel/types",
    "fast-glob",
  ],
};

export default nextConfig;

⚠️ Do not put theme-forge itself in serverExternalPackages — it contains a React client component and externalizing it causes a duplicate-React (useContext is null) error. Only externalize the four packages above.

Step 3 — Drop the panel into an admin page

app/admin/theme-customizer/page.tsx

"use client";

import { ThemeForgePanel } from "theme-forge/react";

export default function Page() {
  return <ThemeForgePanel basePath="/api/theme-forge" />;
}

That's it. Open /admin/theme-customizer, click text/images in the live preview, edit them, then hit Save changes or Save & Rebuild.

The panel is fully self-styled (scoped CSS) — it looks correct with no Tailwind configuration required.


Full configuration reference

Pass options to createThemeForgeHandlers({...}), or create a theme-forge.config.json in your project root (npx theme-forge init). Inline options override the config file.

{
  "root": "src",
  "include": ["**/*.{js,jsx,ts,tsx}"],
  "exclude": ["**/node_modules/**", "**/.next/**", "**/*.test.*"],
  "textAttributes": ["alt", "title", "placeholder", "label", "aria-label", "content", "value"],
  "imageAttributes": ["src", "poster", "srcSet"],
  "publicDir": "public",
  "imageUploadDir": "public/uploads",
  "publicUrlBase": "/",
  "buildCommand": "npm run build",
  "minTextLength": 1,
  "skipNonAlphaText": true
}

| Option | Default | Description | | ------------------ | ---------------------------------------- | -------------------------------------------------------- | | root | "src" | Source directory to scan (relative to project root). | | include | ["**/*.{js,jsx,ts,tsx}"] | Glob(s) of files to scan. | | exclude | node_modules / .next / dist / tests … | Glob(s) to ignore. | | textAttributes | alt, title, placeholder, … | JSX attributes treated as editable text. | | imageAttributes | src, poster, srcSet | JSX attributes treated as images. | | publicDir | "public" | Static dir that serves images (Next.js: public). | | imageUploadDir | "public/uploads" | Where uploaded images are written. | | publicUrlBase | "/" | URL prefix that maps to publicDir. | | buildCommand | "npm run build" | Command run by Save & Rebuild. | | restartCommand | "" | Optional command run (detached) after a successful build to restart the server in production. See Production. | | minTextLength | 1 | Ignore text shorter than this. | | skipNonAlphaText | true | Skip strings with no letters (numbers/symbols only). |

Project without a src/ folder? Set root: "." and add an exclude for app/api/** if you don't want the API route itself scanned.


<ThemeForgePanel /> props

| Prop | Default | Description | | ------------------ | -------------------- | ---------------------------------------------------- | | basePath | "/api/theme-forge" | Where the API handlers are mounted. | | initialPreviewUrl| "/" | Page URL loaded in the live preview iframe. | | hideHeader | false | Hide the built-in heading (use your own page title). | | className | — | Extra class on the outer wrapper. |

basePath must match where you mounted the route in Step 1.


Using the visual editor

  • Edit text — click any text in the preview, type, press Enter (or click away) to apply. Esc cancels.
  • Replace an image — click an image to open its panel (upload a file, pick one from the image library, or paste a URL), or drag-and-drop an image file straight onto it.
  • Theme colors — open the colors panel to edit detected color tokens with a color picker.
  • Devices — preview at desktop / tablet / mobile widths.
  • List view — toggle to a searchable/filterable list of every item.

Keyboard shortcuts

| Shortcut | Action | | ------------------------------ | --------------- | | Ctrl/Cmd + S | Save changes | | Ctrl/Cmd + Z | Undo | | Ctrl/Cmd + Shift + Z / + Y | Redo | | Ctrl/Cmd + Shift + B | Save & Rebuild |


Per-theme / per-page preview

To preview/edit a specific theme or route, point the panel at any URL via initialPreviewUrl. For example, drive it from a query param so a "Customize" button on a theme card can deep-link into the editor:

"use client";

import { Suspense } from "react";
import { useSearchParams } from "next/navigation";
import { ThemeForgePanel } from "theme-forge/react";

function Customizer() {
  const theme = useSearchParams().get("theme");
  const previewUrl = theme ? `/?theme=${encodeURIComponent(theme)}` : "/";
  return <ThemeForgePanel basePath="/api/theme-forge" initialPreviewUrl={previewUrl} hideHeader />;
}

export default function Page() {
  // useSearchParams must be wrapped in <Suspense> in the App Router.
  return (
    <Suspense fallback={null}>
      <Customizer />
    </Suspense>
  );
}

Then link to /admin/theme-customizer?theme=my-theme from anywhere.


Production: make changes appear without a manual restart

In development (next dev) edits show up immediately. In production you run a long-lived server (next start). next build writes a new .next build, but the already-running next start process keeps serving the old build until it is restarted — so your saved changes won't appear until you restart the server.

Theme Forge solves this with restartCommand: after a successful build it runs that command detached (so it survives the server being torn down), letting your process manager bring the server back up on the fresh build.

Run your server under a process manager and point restartCommand at it:

pm2

// app/api/theme-forge/[...tf]/route.ts
export const { GET, POST } = createThemeForgeHandlers({
  root: "src",
  buildCommand: "npm run build",
  restartCommand: "pm2 restart my-app", // <-- restarts after build
});
pm2 start "npm run start" --name my-app

systemd

restartCommand: "sudo systemctl restart my-app"

Docker / Kubernetes — trigger a rolling restart, e.g. restartCommand: "kubectl rollout restart deployment/my-app", or restart the container via your orchestrator.

Without a process manager, a bare next start that is killed will simply exit and stay down. Always run production behind pm2 / systemd / a container so the restart brings it back automatically. Leave restartCommand empty ("") to disable auto-restart and restart manually.


What gets detected

Theme Forge parses the AST and extracts editable items as one of three kinds:

  • text — JSX text between tags, plus strings in data structures (object properties, arrays, JSX expressions) that look like human-readable copy.
  • image — string values that resolve to an image (file extensions like .png/.jpg/.webp/.svg, /images|/assets|/uploads/… paths, data:image/…, remote image URLs), in attributes or in data.
  • color#hex, rgb()/rgba(), hsl()/hsla() literals.

To avoid breaking your app it skips: imports, type/enum literals, strings used in comparisons/logic (e.g. if (x === "foo")), and known non-text keys (href, className, id, slug, config keys, etc.). Each item is written back with the correct escaping for its location, so quotes, <, >, {, } and newlines never corrupt your source.


CLI helpers

npx theme-forge scan      # print a summary of editable content
npx theme-forge build     # run the build command with live output
npx theme-forge init      # write a starter theme-forge.config.json

Programmatic API

Use the core directly from your own server or scripts (no Next.js required):

import { loadConfig, scanProject, applyEdits, Builder } from "theme-forge";

const config = loadConfig(process.cwd(), { root: "src" });

// 1) Scan
const { items } = await scanProject(config);

// 2) Edit (by stable id)
applyEdits(config, [{ id: items[0].id, value: "New text" }]);

// 3) Rebuild
const builder = new Builder(config);
builder.on("event", (e) => console.log(e));
builder.start();

Also exported: extractFromCode, isColor, isImageSource, listImages, saveUpload, and types (ContentItem, ResolvedConfig, …).


Troubleshooting

Module not found: Can't resolve 'theme-forge/next' (Turbopack with a local symlink) — install the packed tarball instead of a file: link so it is physically copied into node_modules:

cd theme-forge && npm run build && npm pack
cd ../your-app && pnpm add ../theme-forge/theme-forge-1.0.0.tgz

TypeError: Cannot read properties of null (reading 'useContext') — you put theme-forge into serverExternalPackages. Remove it; only externalize @babel/* and fast-glob (see Step 2).

"use client" / hooks errors — make sure the page that renders <ThemeForgePanel /> is itself a client component ("use client" at the top) or imported into one.

Build doesn't start / wrong command — set buildCommand to match your package manager (pnpm build, yarn build, npm run build).

Some text isn't clickable in the preview — text that renders differently from source (e.g. HTML entities like &apos;) may not match in the visual layer; edit those from the List view instead.


How it works

 source (.tsx) ─▶ Babel AST ─▶ ContentItem[] (text / image / color, with offsets)
                                   │
                    <ThemeForgePanel/>  (edit values in your admin)
                                   │  /api/theme-forge/*
        save ─▶ re-parse file ─▶ match by id ─▶ escape ─▶ splice offsets ─▶ write
                                   │
        rebuild ─▶ child_process spawn(buildCommand) ─▶ live SSE log stream

No database, no lock-in. Your code stays the single source of truth.


License

MIT