Template
Run on the nodesandbox runtime: harness toolchain, Hono server entry
The sandbox loads Vite's native tools as WASI (NAPI_RS_FORCE_WASI=error) and only ships glue for rolldown 1.1.x. Vite 8.3 pulls rolldown 1.2.x, whose binding needs emnapi 2.0 alpha, so boot failed with 'WASI binding not found'. Match the runtime-host harness TanStack sample instead: - Pin vite 8.1.3, rolldown + @rolldown/binding-wasm32-wasi 1.1.4, @vitejs/plugin-react 6.0.0, tailwindcss + @tailwindcss/vite 4.3.1 (exact versions: the sandbox resolves package.json, not yarn.lock) - Drop Nitro (it brings rolldown 1.2.x) and the Vercel config - Hono now sits in src/server.ts, Start's server entry, routing /api/* ahead of start.fetch, as in the harness - dev runs 'vite dev --host 0.0.0.0 --port 3000'; config is .mjs - Await product recommendations in the loader to fix a hydration mismatch between the SSR skeleton and the settled client render Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
099ba454e0
commit
e09bc0e1fe
@@ -1,9 +1,6 @@
|
||||
.DS_Store
|
||||
node_modules/*
|
||||
dist
|
||||
.output
|
||||
.nitro
|
||||
.vercel
|
||||
.yarn/install-state.gz
|
||||
.env*.local
|
||||
src/routeTree.gen.ts
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
# Vite Shopify Storefront
|
||||
|
||||
A server-rendered Shopify storefront built with TanStack Start (Vite + TanStack
|
||||
Router), TanStack Query, Tailwind CSS v4, Hono and `@shopify/hydrogen` (used
|
||||
only as a Storefront API client and typed `gql()`). It's a port of
|
||||
`nextjs-templates/shopify-storefront` without the AI store assistant.
|
||||
A server-rendered Shopify storefront built with Vite, TanStack Start (TanStack
|
||||
Router + TanStack Query), Hono and Tailwind CSS v4. `@shopify/hydrogen` is used
|
||||
only as the Storefront API client and for typed `gql()` documents. It's a port
|
||||
of `nextjs-templates/shopify-storefront` without the AI store assistant.
|
||||
|
||||
Runs in the nodesandbox runtime: `npm install` → `npm run dev` → port 3000.
|
||||
|
||||
## Getting started
|
||||
|
||||
```sh
|
||||
cp .env.example .env.local # defaults to the tokenless mock.shop
|
||||
cp .env.example .env.local # optional; without it the store is mock.shop
|
||||
corepack enable # uses the Yarn 4 pinned in package.json
|
||||
yarn install
|
||||
yarn dev # http://localhost:3000
|
||||
@@ -16,26 +18,29 @@ yarn dev # http://localhost:3000
|
||||
|
||||
| Script | What it does |
|
||||
| ---------------- | --------------------------------------------------------- |
|
||||
| `yarn dev` | Vite dev server with SSR and the `/api` routes |
|
||||
| `yarn build` | Production build via Nitro (`.output/`, or Vercel's Build Output on Vercel) |
|
||||
| `yarn start` | Run the Node build from `.output/` |
|
||||
| `yarn dev` | Vite dev server with SSR and the Hono `/api` routes |
|
||||
| `yarn build` | Production build to `dist/` |
|
||||
| `yarn typecheck` | `tsc` plus `hydrogen gql check` for the GraphQL documents |
|
||||
|
||||
## Deploying to Vercel
|
||||
## nodesandbox compatibility
|
||||
|
||||
`vercel.json` pins the **TanStack Start** preset. Without it Vercel's
|
||||
auto-detection sees `@shopify/hydrogen` and picks the Hydrogen preset. It also
|
||||
installs with Corepack so Vercel uses Yarn 4. Set the `VITE_SHOPIFY_*` variables
|
||||
in the project; the production domain is picked up for canonical URLs
|
||||
automatically, or set `VITE_SITE_URL`.
|
||||
The sandbox runs Vite's native tools as WebAssembly (`NAPI_RS_FORCE_WASI`), and
|
||||
its installer resolves from `package.json` ranges rather than `yarn.lock`. So:
|
||||
|
||||
- `vite` is pinned to **8.1.3**, which uses rolldown 1.1.x; `rolldown` and
|
||||
`@rolldown/binding-wasm32-wasi` are pinned to **1.1.4**. Rolldown 1.2.x's
|
||||
WASI binding needs glue the runtime doesn't support ("WASI binding not
|
||||
found"). Bump these only together with the runtime.
|
||||
- `@vitejs/plugin-react` (6.0.0) and Tailwind (4.3.1) match the harness samples.
|
||||
- No Nitro: it brings its own rolldown 1.2.x.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
src/
|
||||
server.ts Start's server entry: Hono routes /api/*, the rest goes to Start
|
||||
router.tsx Router + per-request QueryClient, SSR query integration
|
||||
routes/ File routes; loaders prefetch into TanStack Query
|
||||
api/$.ts Mounts the Hono app for every /api/* request
|
||||
server/
|
||||
app.ts Hono API
|
||||
account.ts login, logout, register, recover, reset, activate, me
|
||||
@@ -56,7 +61,7 @@ src/
|
||||
| `app/api/account/*` route handlers | `src/server/account.ts` (Hono) |
|
||||
| `next/headers` cookies | `hono/cookie`, and `getCookie` in server functions |
|
||||
| `next/image` | `src/components/ui/image.tsx` (Shopify CDN `srcset`) |
|
||||
| `NEXT_PUBLIC_*` env | `VITE_*` env |
|
||||
| `NEXT_PUBLIC_*` env | `VITE_*` env (`VITE_SHOPIFY_DOMAIN` defaults to mock.shop) |
|
||||
|
||||
Cart and catalogue reads after the first page load go straight from the browser
|
||||
to the Storefront API with the public token. Account requests stay on the
|
||||
|
||||
+7
-7
@@ -4,9 +4,8 @@
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"dev": "vite dev --host 0.0.0.0 --port 3000",
|
||||
"build": "vite build",
|
||||
"start": "node .output/server/index.mjs",
|
||||
"typecheck": "tsc --noEmit && hydrogen gql check"
|
||||
},
|
||||
"packageManager": "yarn@4.18.0+sha512.fcb8716fe7cd0eece141ffc18b92193a9df9204c1ba83189c288835223fc0bbe64af473bab0d5e9927a7daeb5caf2bb07eb2787cc9338ca040ea125f2a1f2f7e",
|
||||
@@ -14,6 +13,7 @@
|
||||
"@fontsource-variable/geist": "^5.3.0",
|
||||
"@fontsource-variable/geist-mono": "^5.3.0",
|
||||
"@remixicon/react": "^4.9.0",
|
||||
"@rolldown/binding-wasm32-wasi": "1.1.4",
|
||||
"@shopify/hydrogen": "0.0.0-preview-116d5d7-20260730141607",
|
||||
"@tanstack/query-core": "^5.104.1",
|
||||
"@tanstack/react-query": "^5.104.1",
|
||||
@@ -27,21 +27,21 @@
|
||||
"graphql": "^17.0.2",
|
||||
"hono": "^4.13.12",
|
||||
"lucide-react": "^0.562.0",
|
||||
"nitro": "3.0.260903-beta",
|
||||
"radix-ui": "^1.6.7",
|
||||
"react": "^19.3.0",
|
||||
"react-dom": "^19.3.0",
|
||||
"rolldown": "1.1.4",
|
||||
"tailwind-merge": "^3.7.0",
|
||||
"zustand": "^5.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.3.3",
|
||||
"@tailwindcss/vite": "4.3.1",
|
||||
"@types/node": "^26.6.4",
|
||||
"@types/react": "^19.3.0",
|
||||
"@types/react-dom": "^19.3.0",
|
||||
"@vitejs/plugin-react": "^6.1.1",
|
||||
"tailwindcss": "^4.3.3",
|
||||
"@vitejs/plugin-react": "6.0.0",
|
||||
"tailwindcss": "4.3.1",
|
||||
"typescript": "^5.7.3",
|
||||
"vite": "^8.3.2"
|
||||
"vite": "8.1.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import api from '@/server/app';
|
||||
|
||||
// Every /api/* request goes to the Hono app (src/server/app.ts), which owns
|
||||
// the customer-account endpoints and their session cookie.
|
||||
const handle = ({ request }: { request: Request }) => api.fetch(request);
|
||||
|
||||
export const Route = createFileRoute('/api/$')({
|
||||
server: {
|
||||
handlers: {
|
||||
GET: handle,
|
||||
POST: handle,
|
||||
PUT: handle,
|
||||
PATCH: handle,
|
||||
DELETE: handle,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -18,9 +18,13 @@ export const Route = createFileRoute('/products/$handle')({
|
||||
.ensureQueryData(productQueryOptions(handle))
|
||||
.catch(() => null);
|
||||
|
||||
// Warm the recommendations without holding up the page.
|
||||
// Await the recommendations too: a fetch still in flight during SSR renders
|
||||
// the loading skeleton on the server but the settled result on the client,
|
||||
// which is a hydration mismatch. A failure just leaves the section out.
|
||||
if (product) {
|
||||
queryClient.prefetchQuery(productRecommendationsQueryOptions(product.id));
|
||||
await queryClient
|
||||
.ensureQueryData(productRecommendationsQueryOptions(product.id))
|
||||
.catch(() => undefined);
|
||||
}
|
||||
|
||||
return { product };
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// TanStack Start's custom server entry. Hono owns request routing: /api/* goes
|
||||
// to the account API (src/server/app.ts), and everything else falls through to
|
||||
// TanStack Start for SSR, routes and server functions.
|
||||
import start from '@tanstack/react-start/server-entry';
|
||||
import { Hono } from 'hono';
|
||||
import api from '@/server/app';
|
||||
|
||||
const server = new Hono();
|
||||
|
||||
server.route('/', api);
|
||||
// Unknown API paths stay JSON 404s rather than rendering the app's 404 page.
|
||||
server.all('/api/*', (c) => c.json({ error: 'Not found' }, 404));
|
||||
server.all('*', (c) => start.fetch(c.req.raw));
|
||||
|
||||
export default {
|
||||
fetch(request: Request): Promise<Response> {
|
||||
return Promise.resolve(server.fetch(request));
|
||||
},
|
||||
};
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// The API surface, mounted by the Start server route in routes/api/$.ts.
|
||||
// The API surface, mounted ahead of TanStack Start in src/server.ts.
|
||||
import { Hono } from 'hono';
|
||||
import { account } from './account';
|
||||
|
||||
|
||||
+1
-1
@@ -17,6 +17,6 @@
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src", "vite.config.ts"],
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist", ".output"]
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"$schema": "https://openapi.vercel.sh/vercel.json",
|
||||
"framework": "tanstack-start",
|
||||
"installCommand": "corepack enable && yarn install",
|
||||
"buildCommand": "yarn build"
|
||||
}
|
||||
@@ -1,27 +1,18 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import { tanstackStart } from '@tanstack/react-start/plugin/vite';
|
||||
import { nitro } from 'nitro/vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import { fileURLToPath, URL } from 'node:url';
|
||||
|
||||
// Absolute origin for canonical and Open Graph URLs. Inlined at build time so
|
||||
// the server render and the client agree on it. Vercel provides the production
|
||||
// domain; set VITE_SITE_URL to override it anywhere.
|
||||
const siteUrl =
|
||||
process.env.VITE_SITE_URL ??
|
||||
(process.env.VERCEL_PROJECT_PRODUCTION_URL
|
||||
? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`
|
||||
: 'http://localhost:3000');
|
||||
// the server render and the client agree on it; set VITE_SITE_URL in production.
|
||||
const siteUrl = process.env.VITE_SITE_URL ?? 'http://localhost:3000';
|
||||
|
||||
export default defineConfig({
|
||||
server: { port: 3000 },
|
||||
plugins: [
|
||||
// Start must come before the React plugin.
|
||||
tanstackStart(),
|
||||
// Builds the server for the host: Vercel's Build Output when deployed
|
||||
// there, a standalone Node server (.output/) everywhere else.
|
||||
nitro(),
|
||||
react(),
|
||||
tailwindcss(),
|
||||
],
|
||||
Reference in New Issue
Block a user