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

create-fronton

v0.7.0

Published

Scaffolds a new frontend project on fronton — an opinionated Vite + React + TypeScript + Tailwind setup with build-time Tailwind class obfuscation, gzip/brotli compression, and backon backend wiring baked in. Runs via bunx/bun create or npx/npm create.

Readme

create-fronton

Scaffolds a new frontend project on @mednours/fronton — like bun create vite@latest, but opinionated by default: Vite + React 19 + TypeScript + Tailwind v4, with build-time Tailwind class obfuscation and gzip/brotli compression already wired in, and a backon backend's dev-server proxy set up automatically if one is found next to it.

Usage

bun create fronton my-app

Or, without the bun create shorthand:

bunx create-fronton my-app

Leave the name off (or use . to scaffold into the current directory, same as bun create vite@latest .) to be prompted for one:

bunx create-fronton

Either way, you'll always be asked two questions: where's this deploying? (Vercel / Hostinger or another static/shared host / other — not sure yet), since that changes which file gets written, and a one-line site description, used to fill in index.html's SEO/social-preview meta tags (leave it blank and a generic placeholder goes in instead — nothing blocks on it):

my-app/
  package.json
  tsconfig.json
  index.html
  vite.config.ts
  vercel.json          <- Vercel only
  public/
    .htaccess           <- Hostinger (or another static/shared host) only
  .env.example
  src/
    main.tsx
    App.tsx
    index.css

Picking "other / not sure yet" skips both — no host-specific file, just a README pointer to build dist/ and deploy it wherever you land.

To skip every question and take the defaults (Vercel, no description, no native wrapper), pass --yes:

bunx create-fronton my-app --yes

The prompts need a real TTY — @clack/prompts throws ERR_TTY_INIT_FAILED without one — so --yes is what makes this usable from CI, a container build, or any other script. A project name is required in that mode, since there's nothing to prompt.

index.html always gets the full title/description/Open Graph/Twitter card block, not just Vercel/Hostinger-conditional files — checked against six real fronton-adjacent projects, five of which hand-write 13-16 of these tags with no helper library, so it's the one piece of boilerplate worth generating regardless of deploy target. og:image/twitter:image point at /og-image.png, which doesn't exist yet — add a real 1200x630 image at public/og-image.png once you have one.

What's opinionated, and why there's barely a prompt for it

Unlike create-backon (which asks about framework, database, and deploy target), create-fronton asks about almost nothing else — the entire point is not re-deciding the same handful of things per project:

  • React only, on Vite. No framework prompt.
  • Tailwind class obfuscation and gzip/brotli compression are always on in the generated vite.config.ts. Both are one line to disable if you don't want them (obfuscateTailwind({ enabled: false }), or remove the frontonCompression() line) — see @mednours/fronton's README for exactly what each one does and its limitations.

None of this is hard-locked into the generated code — it's all just the starting vite.config.ts, plain and editable. The opinion is about what you get without asking, not about taking anything away.

Not wired in by default: @mednours/fronton also ships compressImages() and compressVideos() (sharp and ffmpeg-based build-time media compression). They're left out of the generated vite.config.ts on purpose — their dependencies are large native binaries most projects have no use for — see @mednours/fronton's README for how to add either one.

Installable web app (PWA)

Answer yes to "Make it installable as a PWA?" and the generated project gets vite-plugin-pwa wired in: a web manifest built from the project name and description you gave, placeholder icons at public/icon-192.png and public/icon-512.png, and a service worker that precaches the built app shell. bun run build produces sw.js and manifest.webmanifest alongside the usual output; nothing else to configure.

What it deliberately doesn't do: push notifications, background sync, or runtime caching of API responses. The last one especially is a decision about your backend's data — cache the wrong endpoint and you serve stale responses with no obvious cause — so vite.config.ts is left as the place to add a workbox.runtimeCaching rule when you actually know what you want.

Updates use registerType: "prompt", so a newly deployed version waits rather than reloading the page under someone mid-task. The generated README shows how to surface that prompt.

This question is only asked when you haven't picked a native wrapper below: Capacitor and Tauri already produce an installed app loading its assets locally, so a service worker precaching the same shell is a second caching layer that buys nothing.

Native app wrappers (Capacitor / Tauri)

A fourth question — "Add a native app wrapper?" — is always asked too: none (the default), Capacitor, or Tauri. The two aren't offered as a combined "pick several" choice on purpose — see templates/mobile.ts and templates/tauri.ts's own header comments for why each is scoped the way it is, and pick whichever fits: Capacitor if you only need Android and want the smaller, JS-only footprint; Tauri if you want desktop too (macOS/Linux/Windows) from the same codebase, and don't mind Rust being part of the project.

Capacitor (Android only)

my-app/
  capacitor.config.ts
  .github/
    workflows/
      mobile.yml

plus a relative base: "./" in vite.config.ts and safe-area CSS in src/index.css (a Capacitor WebView loads the built app from a file:// origin and draws edge-to-edge under the status bar/gesture nav by default — both are inert in a normal browser tab), @capacitor/core/@capacitor/android/@capacitor/cli in package.json, and cap:add:android/cap:sync/cap:open:android scripts.

What it deliberately doesn't do: write a real android/ directory to disk. That step needs network access and @capacitor/android already resolved, so rather than risk scaffolding failing for reasons that have nothing to do with writing files, it's a documented bun run cap:add:android you run once, when you're ready. .github/workflows/mobile.yml doesn't wait on that either — it runs cap add android itself if the directory isn't already committed, builds a debug APK, and uploads it as a workflow artifact, so a brand-new scaffold gets a working CI build on its first push.

Two things worth doing before this is a real app rather than a placeholder: swap capacitor.config.ts's appId away from the com.example.* placeholder (Android treats it as the app's permanent identity, not something safe to change after the fact), and add real signing once you need a release build — the CI workflow only produces an unsigned debug APK. iOS isn't wired in yet; that needs a Mac to build, which this template's CI doesn't have.

Tauri (Android + desktop, one codebase)

my-app/
  src-tauri/
    Cargo.toml
    build.rs
    src/
      main.rs
      lib.rs
    tauri.conf.json
    capabilities/
      default.json
    icons/
      icon.png
  .github/
    workflows/
      tauri.yml

src-tauri/ is a real Rust crate — one run() in src/lib.rs, shared by both the desktop build and the Android build via a #[cfg_attr(mobile, tauri::mobile_entry_point)] attribute, which is the actual "one codebase" difference from Capacitor's mobile-only wrapper. vite.config.ts also gets Tauri's own requirements: a fixed dev-server port (1420), clearScreen: false, src-tauri/ excluded from Vite's file watcher, and TAURI_DEV_HOST support for testing on a physical Android device — merged with the backon dev-proxy config when one applies, not replacing it. @tauri-apps/api, @tauri-apps/plugin-opener, and @tauri-apps/cli get added to package.json, plus a tauri script.

icons/icon.png is the one binary file in an otherwise pure-text scaffold — a solid-color placeholder, but a real, valid PNG, because tauri::generate_context!() (a compile-time macro) requires an actual icon file on disk just to cargo check, independent of whatever tauri.conf.json's bundle.icon says. Swap it with bun run tauri icon /path/to/art.png once you have real artwork.

Same "don't shell out during scaffold" discipline as Capacitor: nothing here runs cargo or tauri android init, so src-tauri/gen/android isn't written to disk yet either. .github/workflows/tauri.yml builds desktop bundles for macOS (Intel + Apple Silicon), Linux, and Windows, plus a debug Android APK — both jobs generate what they need fresh in CI if it isn't committed, so a brand-new scaffold gets a working CI build on its first push, same as Capacitor's.

Requires Rust locally for bun run tauri dev/build (see rust-lang.org/tools/install); Android additionally needs the Android SDK/NDK (see Tauri's prerequisites guide) — CI needs none of this set up locally first. Two things worth doing before this is a real app: swap tauri.conf.json's identifier away from the com.example.* placeholder (same permanent-identity caveat as Capacitor's appId), and add code signing before a release build — the CI workflow only produces unsigned/debug output.

Backend detection

If a sibling backend/, server/, or api/ folder exists next to where you're scaffolding, and its package.json depends on @mednours/backon, create-fronton wires up the dev-server proxy automatically:

workspace/
  backend/    <- a backon project, package.json depends on @mednours/backon
  frontend/   <- scaffolded here — proxy to ../backend is wired in automatically

The generated vite.config.ts gets a server.proxy entry (via backonProxy() from @mednours/fronton) pointed at the backend's port — read from its .env/.env.example, defaulting to 3000 (create-backon's own default) if neither declares one. PORT=3000, PORT="3000", PORT = 3000 and export PORT=3000 all parse; a commented-out # PORT= correctly doesn't, and falls through to the next file. WebSocket upgrades on the same prefix are forwarded too, so a backon defineWebSocket() route works through bun run dev without extra setup. src/App.tsx also gets a small real example: a fetch through the proxy on mount, showing whether the backend actually responded, instead of a placeholder claiming it works.

No backend detected? The project generates exactly the same way, just without that piece — the README it writes explains how to add the proxy by hand later.

This is intentionally the extent of the backon integration for now — wiring (proxy, env, CORS guidance), not generated data-fetching hooks around backon's own generate-client output. See the monorepo README for where that might go next.

Routing and pages

Answer yes to "Add routing?" and the generated project gets react-router, an explicit route table at src/routes.tsx, and pages under src/pages/:

bun run add-page about        # -> src/pages/About.tsx at /about
bun run add-page users/:id    # -> src/pages/UsersId.tsx at /users/:id
bun run add-component button  # -> src/components/Button.tsx

add-page writes the component and inserts its lazy import and route entry into src/routes.tsx, above the catch-all, at the fronton:pages / fronton:routes markers. That second edit is the reason the command exists — it's the step that gets forgotten, and forgetting the lazy() wrapper specifically is how code-splitting quietly stops happening.

Components are grouped by the page they belong to, or shared/ for anything used by more than one:

src/components/
  shared/NavBar.tsx     # used across pages
  home/Hero.tsx         # only on /
  about/Faq.tsx         # only on /about

add-component hero goes to shared/; add-component home/hero goes to home/. The grouping is the point — a component's location says where it's used, so deleting a page tells you exactly which components can go with it, which a flat src/components/ stops answering about ten components in. If the group matches no page in src/routes.tsx, you get a warning rather than a refusal: hom/hero is usually a typo, but grouping by a feature instead of a route is a legitimate choice.

Generated components have the imports already there (clsx, ReactNode), a named export, and an exported props interface — named rather than default because components aren't lazy-loaded the way pages are, and a named export is what makes a rename propagate through editor tooling.

What they deliberately don't have is memo/useCallback/useMemo. See below.

React Compiler

Opt-in at scaffold time. It wires react({ compiler: true }) in vite.config.ts and adds oxc-transform-react, the Rust port of React Compiler that @vitejs/plugin-react 6 loads as an optional peer dependency. The compiler auto-memoizes, so hand-written memo/useCallback/useMemo stop being something to think about — which is why generated components carry none.

Two honest caveats. @vitejs/plugin-react still labels native compiler support experimental, so this is off by default. And it isn't free: it emits a small compiler-runtime chunk and grows each compiled component (a 450-byte component became 977 bytes in a real build here), so what it buys is fewer re-renders, not a smaller bundle.

Without it, the advice in the generated components still holds — reach for memo when a profile says to, not by reflex. memo on a component taking an object or callback prop re-renders anyway, since a fresh reference fails the shallow compare.

Why an explicit table, not file-based routing

File-based routing (vite-plugin-pages, generouted) makes a file in src/pages/ into a route with no table at all, which is genuinely less to maintain — and for a long-lived project it's probably the better call. It loses on three counts here: it's another build plugin that can misbehave, routing moves into filename conventions you have to remember, and nothing answers "what routes exist" except the file tree.

Its real advantage — a lazy chunk per page, so a first visit downloads only the route it landed on — isn't exclusive to it, and isn't given up here: every generated route is React.lazy(), and add-page writes new ones the same way. Verified on a real build: four pages come out as four separate chunks, and a first paint loads the landed route's chunk alone.

Routing also adds an SPA fallback rewrite to vercel.json, without which a refresh or a shared deep link on /about 404s. (Vercel applies rewrites after the filesystem check, so it can't shadow the hashed assets.) The Hostinger .htaccess already did this job.

Typed API client (sync-api)

bunx create-fronton sync-api     # or `bun run sync-api` inside a generated project

backon already generates a typed client — bun run generate-client reads its own src/endpoints/ and writes client/api.ts, one function per mounted method. The problem is that it lands inside the backend project, gitignored there as a build artifact, with no route to the frontend but copy-paste, which reintroduces exactly the drift the generator existed to prevent.

sync-api re-runs that generator in the sibling backend, then copies the result to src/lib/api.ts. Calling the API becomes usersApi.getUser(id) instead of a hand-written fetch you keep in step by remembering to.

Copying it verbatim doesn't work, which is the reason this is a command and not a documentation note. Three things are rewritten:

  • process.env.API_BASE_URL becomes resolveApiBaseUrl(). In a Vite bundle process doesn't exist, and backon's guarded version doesn't throw — it falls through to "", so a split-origin deploy quietly calls its own origin.
  • The base URL gains the /api prefix in dev. The generated paths are the backend's real ones (/users), and the dev proxy is what owns /api and strips it again before forwarding. Verbatim, the request never matches the proxy, Vite serves index.html as an SPA fallback, and the call fails with Unexpected token '<', "<!doctype "... is not valid JSON — verified against a real dev server, which is how this was found.
  • An unused import type { z } from "zod" is dropped. backon emits it either way, and an unused import still has to resolve, so it fails bun run typecheck on a frontend with no zod.

The synced file is committed rather than gitignored, unlike backon's copy: a deploy builds the frontend on its own, with no backend checkout next to it to regenerate from.

Requirements

Bun >= 1.3.

Part of the fronton monorepo.