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

@scayle/storefront-build

v0.1.0-alpha.1

Published

Vite plugin setup for the SCAYLE Storefront Application

Readme

scayle-logo-cr

Overview

The Storefront Application uses a custom Vite setup via the @scayle/storefront-build package. It provides a single dev server (Hono + Vite with HMR) and a split build (client bundle, SSR bundle, and a generated Node server entry). You run vite dev for development and .output/server/index.mjs for production.

Configuration is passed as storefrontBuild({ serverEntry, ssrEntry, indexEntry }).

Installation

# Install via PNPM
pnpm add @scayle/storefront-build

# Install with YARN
yarn add @scayle/storefront-build

# Install with NPM
npm i @scayle/storefront-build

How It Works

Development

Running pnpm dev (or vite dev) starts the Vite dev server. The @hono/vite-dev-server plugin loads your Hono app from the configured server entry and forwards non-excluded requests to it. The app and Vite run in the same process on server.port (default 3000). No separate Node process is required.

Client-side code (Vue, Tailwind, assets) is served and transformed by Vite with HMR. Server-side rendering uses the same Vue components. In dev the SSR entry is loaded through Vite's ssrLoadModule and is not cached, so changes to components or Tailwind classes apply on the next request and stay in sync with the client (no hydration mismatch).

Production Build and Runtime

The build runs two Vite builds:

  1. Client: Entry is the HTML index (e.g. ./src/client/index.html). Output is a static bundle in .output/public with assets in .output/public/assets/<BUILD_ID>/.
  2. SSR: Entry is the SSR module (e.g. ./src/client/ssr.ts) plus a virtual module that generates the runtime server file. Output is in .output/server: index.mjs (server entry), ssr.js (SSR bundle), and chunks/[name]-[hash].js (shared chunks).

Production runtime is node .output/server/index.mjs. That script imports the built Hono app and the SSR bundle, serves the app, and serves static assets from .output/public. The SSR module is loaded once from disk and cached in memory.

Technical Architecture

Plugin Order and Roles

The @scayle/storefront-build default export returns a Plugin[] (a nested plugin array that Vite flattens) containing these plugins in order:

  1. @hono/vite-dev-server: Loads the Hono app from serverEntry, forwards requests to it, and injects the Vite dev server as c.env.vite (as of v0.25.0) so handlers can use it for SSR.
  2. storefront-build-ssr: Applies only when command === 'build' and isSsrBuild === true. Configures the SSR build and a virtual module that emits index.mjs.
  3. storefront-build-client: Applies only when command === 'build' and isSsrBuild === false. Sets client entry and outDir, and moves the emitted index.html to .output/server/index.html.

Dev Server Request Handling

For each request the dev-server plugin:

  1. Checks if the request URL matches a file in Vite's publicDir. If yes, calls next() so Vite serves it.
  2. Checks the request URL against the exclude list. If it matches, calls next() so Vite (or later middleware) handles it.
  3. Otherwise loads the Hono app (if not already loaded) and calls app.fetch(request, env) with env including vite (the Vite dev server).

The app uses c.env.vite only in development. The Inertia middleware passes it into ServerRenderer.render(..., { viteServer }). When viteServer is present, the renderer loads the SSR entry via viteServer.ssrLoadModule() and does not cache it, so each request can see updated modules after HMR.

How the index.html shell works in dev

The app does not run the index through vite.transformIndexHtml(). The only place that loads the HTML template is ServerRenderer.resolveHTMLTemplate(): it returns readFile(this.config.indexEntrypoint, 'utf8') with no Vite involvement. There is no hook or override that runs the template through Vite's HTML transform in dev. For each SSR response the flow is:

  1. Read the template from disk: ServerRenderer.resolveHTMLTemplate() reads the file at the dev index path (e.g. src/client/index.html) with readFile(). That file is the raw source: it contains placeholders like <!-- @inertia --> / <!-- @inertiaHead --> and source URLs such as <script type="module" src="/src/client/main.ts"></script> and <link rel="stylesheet" href="/src/client/index.css" />.
  2. Replace placeholders and send HTML: Inertia replaces the placeholders with the SSR-rendered head and body and returns that HTML as the response.
  3. HMR client injection: The @hono/vite-dev-server plugin sees that the response is Content-Type: text/html and appends a script that loads the Vite client: <script>import("/@vite/client")</script>. So the browser receives the full HTML (your shell + SSR content + HMR script) without the app ever calling transformIndexHtml.
  4. Script and style requests: When the browser requests /src/client/main.ts or /src/client/index.css, those URLs match the dev-server exclude list (e.g. .*\.ts$, .*\.css$ from defaultOptions). So the dev-server does not pass them to Hono. It calls next() and the request is handled by Vite's middleware. Vite then transforms the module (TypeScript, Vue, etc.) or serves the CSS and returns the response. So the "transform" of the client app happens when the browser requests those script/style URLs, not when we serve the HTML. The index.html itself is intentionally left as a static template with source paths so that in dev all client assets go through Vite and get HMR.

Exclude Rules

The dev-server is configured with custom exclude patterns so that asset and source-file requests are handled by Vite, not by Hono:

  • .svg (any path ending in .svg). Imported SVGs (e.g. from @assets or src/client/assets) are served by Vite.
  • /src/.../*.json. JSON under src/ (e.g. i18n locale files) is served by Vite when requested as URLs (e.g. /src/i18n/locales/en_US.json?import). Paths that do not start with /src/ (e.g. /api/spec.json) are not excluded and still reach the Hono app.
  • defaultOptions.exclude. The plugin's default list (e.g. .css, .ts, .vue, .js, favicon, node_modules, etc.) is spread after the custom rules.

SSR Build Details

The SSR build plugin:

  • Sets build.entry to serverEntry, build.outDir to .output/server, and build.ssr to true.
  • Adds a virtual module virtual:build-entry-module that generates the runtime server source. That source imports the app from serverEntry, calls serve({ fetch: app.fetch, port, hostname }) from @hono/node-server with a listen callback that logs a startup banner via createLogger('server') from @scayle/storefront/shared, and registers SIGINT/SIGTERM handlers with a 5s shutdown timeout.
  • Sets Rollup input to the virtual module and ssrEntry, and external to Node built-in and node: modules.
  • Uses entryFileNames so the virtual module output is index.mjs and the SSR entry output is ssr.js.
  • Sets ssr.noExternal: true so dependencies are bundled into the SSR bundle.

The client build plugin sets the client entry to indexEntry and outDir to .output/public, with assets in assets/<BUILD_ID>/. In writeBundle it moves the emitted index.html from the nested path (e.g. .output/public/src/client/index.html) to .output/server/index.html so the server renderer can use it.

Configuration

Required Options

Pass a config object into storefrontBuild() with:

| Option | Description | | ------------- | --------------------------------------------------------------- | | serverEntry | Path to the Hono app entry (e.g. ./src/server/index.ts). | | ssrEntry | Path to the SSR entry module (e.g. ./src/client/ssr.ts). | | indexEntry | Path to the client HTML entry (e.g. ./src/client/index.html). |

Example

// vite.config.mts
import { defineConfig } from 'vite'
import storefrontBuild from '@scayle/storefront-build'

export default defineConfig({
  server: {
    port: 3000,
  },
  plugins: [
    // ... other plugins (vue, tailwind, etc.)
    storefrontBuild({
      serverEntry: './src/server/index.ts',
      ssrEntry: './src/client/ssr.ts',
      indexEntry: './src/client/index.html',
    }),
  ],
})

Scripts

  • pnpm dev: Start the dev server (Hono + Vite, HMR).
  • pnpm build:client: Builds client only. The template uses pnpm build:ssr then pnpm build:client so both SSR and client are built.
  • pnpm build:ssr: Builds the SSR bundle and generates .output/server/index.mjs and .output/server/ssr.js.
  • pnpm build: Builds both the client and SSR bundles.

Production run: node .output/server/index.mjs (or pnpm start / pnpm preview with env).

Build ID

The build generates a unique build identifier that is embedded in both client and SSR production builds via the import.meta.env.SCAYLE_BUILD_ID macro. This allows app code and templates to access the current build ID for cache busting, asset URLs, and runtime checks.

Note: SCAYLE_BUILD_ID is only available during production builds. It is not defined in development mode.

Output Structure with Build ID

The build ID affects the output directory structure:

.output/
├── server/
│   ├── index.mjs          # Server entry point
│   ├── ssr.js             # SSR renderer bundle
│   ├── index.html         # HTML template (moved from public)
│   └── chunks/
│       └── [name]-[hash].js   # Shared chunks
└── public/
    └── assets/
        └── <BUILD_ID>/    # Assets namespaced by build ID
            ├── [name]-[hash].js
            ├── [name]-[hash].css
            └── ...

The build ID namespacing lets platforms upload assets to S3 and serve them at /assets/<BUILD_ID>/*, enabling immutable caching and atomic deployments.

SSR Externals

By default the SSR build bundles all npm dependencies into the output (ssr.noExternal: true). Some packages cannot be bundled, for example packages with native bindings, packages that rely on runtime module loading (auto-instrumentation, monkey-patching), or packages whose module format is incompatible with bundlers.

These packages must be externalized: kept out of the bundle and resolved from node_modules at runtime. The storefront-externals plugin traces the files needed by external packages using @vercel/nft (via nf3) and copies them into .output/server/node_modules/. The result is a fully self-contained .output/ directory with real file copies instead of symlinks.

Adding External Packages

Use the standard Vite ssr.external field in vite.config.ts:

export default defineConfig({
  ssr: {
    // These packages are externalized from the SSR bundle
    // and automatically traced + copied into .output/server/node_modules/
    external: ['isomorphic-dompurify', 'my-native-addon'],
  },
})

Packages listed in ssr.external are automatically picked up by the externals plugin. No additional configuration is needed for the common case.

Plugin API (Advanced)

SDK authors building Vite plugins that need regex patterns, full-trace, or explicit trace paths can use the StorefrontExternals API. The externals plugin attaches a registry to the Vite config during the config phase. Downstream plugins (typically with enforce: 'post') read this registry and add their packages:

import { EXTERNALS_CONFIG_KEY } from '@scayle/storefront-build'
import type { StorefrontExternals } from '@scayle/storefront-build'
import type { Plugin } from 'vite'

export function myPlugin(): Plugin {
  return {
    name: 'my-plugin',
    enforce: 'post',
    config(config) {
      const externals = config[EXTERNALS_CONFIG_KEY] as StorefrontExternals
      if (!externals) {
        return
      }

      // Regex patterns for package families
      externals.addInclude([/^@my-scope\//])

      // Explicit file paths for modules loaded via require.resolve at runtime
      // that @vercel/nft cannot statically discover
      externals.addTraceInclude([require.resolve('@my-scope/hooks')])

      // Packages whose entire contents should be copied (dynamic requires, runtime assets)
      externals.addFullTraceInclude(['my-dynamic-package'])
    },
  }
}

| Method | Purpose | | ------------------------------- | ------------------------------------------------------------------------------ | | addInclude(patterns) | Externalize and trace packages matching the given names or regex patterns. | | addTraceInclude(paths) | Add file paths or specifiers to the trace that NFT cannot discover statically. | | addFullTraceInclude(packages) | Copy all files for packages with dynamic requires or runtime asset loading. |

Output Structure with Traced Externals

After the SSR build, traced packages appear as real file copies in the output:

.output/server/
  index.mjs
  ssr.js
  chunks/
  node_modules/          # Traced dependencies (real files, not symlinks)
    @opentelemetry/
    import-in-the-middle/
    isomorphic-dompurify/
    ...
  package.json           # Generated with { type: "module" }

Production Impact of the Dev SSR Setup

The pattern that passes the Vite dev server via env and loads the SSR entry with ssrLoadModule (without caching) applies only in development.

  • Production build. Unchanged. The client and SSR builds and the virtual server entry are produced as before. No Vite dev server or env.vite is involved.
  • Production runtime. The app runs from .output/server/index.mjs. There is no Vite process, c.env.vite is never set. The ServerRenderer loads the built SSR bundle from disk once (e.g. .output/server/ssr.js), caches it in memory, and reuses it for every request. Production behavior and performance match the previous setup.

What is SCAYLE?

SCAYLE is a full-featured e-commerce software solution that comes with flexible APIs. Within SCAYLE, you can manage all aspects of your shop, such as products, stocks, customers, and transactions.

Learn more about SCAYLE's architecture and commerce modules in the docs.

Troubleshooting

Issue: GET /src/i18n/locales/en_US.json?import (or similar) returns 404 in dev.

Requests for JSON under src/ (e.g. i18n locales) must be served by Vite, not by Hono. The @scayle/storefront-build dev-server config excludes paths matching /^\/src\/.*\.json(\?.*)?$/. If you add JSON under src/ that is requested by URL, ensure the path starts with /src/ so it is excluded. If you have an API route whose path ends in .json (e.g. /api/spec.json), do not add a global .json exclude or that route will stop reaching the app.

Issue: Hydration mismatch when changing Tailwind classes in dev; reload does not fix it.

The app must receive the Vite dev server so the SSR entry is loaded via ssrLoadModule and not cached. Use @hono/vite-dev-server v0.25.0 or later (it injects vite into env by default). In the app, the Inertia middleware must pass c.env.vite into ServerRenderer.render(..., { viteServer }).

Community

The community and core teams are available in GitHub Discussions, where you can ask for support, discuss roadmap, and share ideas.

Other channels

References

License

Licensed under the MIT