@scayle/storefront-build
v0.1.0-alpha.1
Published
Vite plugin setup for the SCAYLE Storefront Application
Maintainers
Keywords
Readme

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-buildHow 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:
- Client: Entry is the HTML index (e.g.
./src/client/index.html). Output is a static bundle in.output/publicwith assets in.output/public/assets/<BUILD_ID>/. - 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), andchunks/[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:
@hono/vite-dev-server: Loads the Hono app fromserverEntry, forwards requests to it, and injects the Vite dev server asc.env.vite(as of v0.25.0) so handlers can use it for SSR.storefront-build-ssr: Applies only whencommand === 'build'andisSsrBuild === true. Configures the SSR build and a virtual module that emitsindex.mjs.storefront-build-client: Applies only whencommand === 'build'andisSsrBuild === false. Sets client entry andoutDir, and moves the emittedindex.htmlto.output/server/index.html.
Dev Server Request Handling
For each request the dev-server plugin:
- Checks if the request URL matches a file in Vite's
publicDir. If yes, callsnext()so Vite serves it. - Checks the request URL against the
excludelist. If it matches, callsnext()so Vite (or later middleware) handles it. - Otherwise loads the Hono app (if not already loaded) and calls
app.fetch(request, env)withenvincludingvite(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:
- Read the template from disk:
ServerRenderer.resolveHTMLTemplate()reads the file at the dev index path (e.g.src/client/index.html) withreadFile(). 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" />. - Replace placeholders and send HTML: Inertia replaces the placeholders with the SSR-rendered head and body and returns that HTML as the response.
- HMR client injection: The @hono/vite-dev-server plugin sees that the response is
Content-Type: text/htmland 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 callingtransformIndexHtml. - Script and style requests: When the browser requests
/src/client/main.tsor/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 callsnext()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@assetsorsrc/client/assets) are served by Vite./src/.../*.json. JSON undersrc/(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.entrytoserverEntry,build.outDirto.output/server, andbuild.ssrtotrue. - Adds a virtual module
virtual:build-entry-modulethat generates the runtime server source. That source imports the app fromserverEntry, callsserve({ fetch: app.fetch, port, hostname })from@hono/node-serverwith a listen callback that logs a startup banner viacreateLogger('server')from@scayle/storefront/shared, and registersSIGINT/SIGTERMhandlers with a 5s shutdown timeout. - Sets Rollup
inputto the virtual module andssrEntry, andexternalto Node built-in andnode:modules. - Uses
entryFileNamesso the virtual module output isindex.mjsand the SSR entry output isssr.js. - Sets
ssr.noExternal: trueso 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 usespnpm build:ssrthenpnpm build:clientso both SSR and client are built.pnpm build:ssr: Builds the SSR bundle and generates.output/server/index.mjsand.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.viteis involved. - Production runtime. The app runs from
.output/server/index.mjs. There is no Vite process,c.env.viteis 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
- Vite
- Server-Side Rendering (SSR)
- Setting up the dev server:
ssrLoadModulein development - Building for production
- SSR options
- Plugin API: configureServer
- @hono/vite-dev-server
License
Licensed under the MIT
