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
|
.DS_Store
|
||||||
node_modules/*
|
node_modules/*
|
||||||
dist
|
dist
|
||||||
.output
|
|
||||||
.nitro
|
|
||||||
.vercel
|
|
||||||
.yarn/install-state.gz
|
.yarn/install-state.gz
|
||||||
.env*.local
|
.env*.local
|
||||||
src/routeTree.gen.ts
|
src/routeTree.gen.ts
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
# Vite Shopify Storefront
|
# Vite Shopify Storefront
|
||||||
|
|
||||||
A server-rendered Shopify storefront built with TanStack Start (Vite + TanStack
|
A server-rendered Shopify storefront built with Vite, TanStack Start (TanStack
|
||||||
Router), TanStack Query, Tailwind CSS v4, Hono and `@shopify/hydrogen` (used
|
Router + TanStack Query), Hono and Tailwind CSS v4. `@shopify/hydrogen` is used
|
||||||
only as a Storefront API client and typed `gql()`). It's a port of
|
only as the Storefront API client and for typed `gql()` documents. It's a port
|
||||||
`nextjs-templates/shopify-storefront` without the AI store assistant.
|
of `nextjs-templates/shopify-storefront` without the AI store assistant.
|
||||||
|
|
||||||
|
Runs in the nodesandbox runtime: `npm install` → `npm run dev` → port 3000.
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
||||||
```sh
|
```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
|
corepack enable # uses the Yarn 4 pinned in package.json
|
||||||
yarn install
|
yarn install
|
||||||
yarn dev # http://localhost:3000
|
yarn dev # http://localhost:3000
|
||||||
@@ -16,26 +18,29 @@ yarn dev # http://localhost:3000
|
|||||||
|
|
||||||
| Script | What it does |
|
| Script | What it does |
|
||||||
| ---------------- | --------------------------------------------------------- |
|
| ---------------- | --------------------------------------------------------- |
|
||||||
| `yarn dev` | Vite dev server with SSR and the `/api` routes |
|
| `yarn dev` | Vite dev server with SSR and the Hono `/api` routes |
|
||||||
| `yarn build` | Production build via Nitro (`.output/`, or Vercel's Build Output on Vercel) |
|
| `yarn build` | Production build to `dist/` |
|
||||||
| `yarn start` | Run the Node build from `.output/` |
|
|
||||||
| `yarn typecheck` | `tsc` plus `hydrogen gql check` for the GraphQL documents |
|
| `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
|
The sandbox runs Vite's native tools as WebAssembly (`NAPI_RS_FORCE_WASI`), and
|
||||||
auto-detection sees `@shopify/hydrogen` and picks the Hydrogen preset. It also
|
its installer resolves from `package.json` ranges rather than `yarn.lock`. So:
|
||||||
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
|
- `vite` is pinned to **8.1.3**, which uses rolldown 1.1.x; `rolldown` and
|
||||||
automatically, or set `VITE_SITE_URL`.
|
`@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
|
## Layout
|
||||||
|
|
||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
|
server.ts Start's server entry: Hono routes /api/*, the rest goes to Start
|
||||||
router.tsx Router + per-request QueryClient, SSR query integration
|
router.tsx Router + per-request QueryClient, SSR query integration
|
||||||
routes/ File routes; loaders prefetch into TanStack Query
|
routes/ File routes; loaders prefetch into TanStack Query
|
||||||
api/$.ts Mounts the Hono app for every /api/* request
|
|
||||||
server/
|
server/
|
||||||
app.ts Hono API
|
app.ts Hono API
|
||||||
account.ts login, logout, register, recover, reset, activate, me
|
account.ts login, logout, register, recover, reset, activate, me
|
||||||
@@ -56,7 +61,7 @@ src/
|
|||||||
| `app/api/account/*` route handlers | `src/server/account.ts` (Hono) |
|
| `app/api/account/*` route handlers | `src/server/account.ts` (Hono) |
|
||||||
| `next/headers` cookies | `hono/cookie`, and `getCookie` in server functions |
|
| `next/headers` cookies | `hono/cookie`, and `getCookie` in server functions |
|
||||||
| `next/image` | `src/components/ui/image.tsx` (Shopify CDN `srcset`) |
|
| `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
|
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
|
to the Storefront API with the public token. Account requests stay on the
|
||||||
|
|||||||
+7
-7
@@ -4,9 +4,8 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev --host 0.0.0.0 --port 3000",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"start": "node .output/server/index.mjs",
|
|
||||||
"typecheck": "tsc --noEmit && hydrogen gql check"
|
"typecheck": "tsc --noEmit && hydrogen gql check"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@4.18.0+sha512.fcb8716fe7cd0eece141ffc18b92193a9df9204c1ba83189c288835223fc0bbe64af473bab0d5e9927a7daeb5caf2bb07eb2787cc9338ca040ea125f2a1f2f7e",
|
"packageManager": "yarn@4.18.0+sha512.fcb8716fe7cd0eece141ffc18b92193a9df9204c1ba83189c288835223fc0bbe64af473bab0d5e9927a7daeb5caf2bb07eb2787cc9338ca040ea125f2a1f2f7e",
|
||||||
@@ -14,6 +13,7 @@
|
|||||||
"@fontsource-variable/geist": "^5.3.0",
|
"@fontsource-variable/geist": "^5.3.0",
|
||||||
"@fontsource-variable/geist-mono": "^5.3.0",
|
"@fontsource-variable/geist-mono": "^5.3.0",
|
||||||
"@remixicon/react": "^4.9.0",
|
"@remixicon/react": "^4.9.0",
|
||||||
|
"@rolldown/binding-wasm32-wasi": "1.1.4",
|
||||||
"@shopify/hydrogen": "0.0.0-preview-116d5d7-20260730141607",
|
"@shopify/hydrogen": "0.0.0-preview-116d5d7-20260730141607",
|
||||||
"@tanstack/query-core": "^5.104.1",
|
"@tanstack/query-core": "^5.104.1",
|
||||||
"@tanstack/react-query": "^5.104.1",
|
"@tanstack/react-query": "^5.104.1",
|
||||||
@@ -27,21 +27,21 @@
|
|||||||
"graphql": "^17.0.2",
|
"graphql": "^17.0.2",
|
||||||
"hono": "^4.13.12",
|
"hono": "^4.13.12",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
"nitro": "3.0.260903-beta",
|
|
||||||
"radix-ui": "^1.6.7",
|
"radix-ui": "^1.6.7",
|
||||||
"react": "^19.3.0",
|
"react": "^19.3.0",
|
||||||
"react-dom": "^19.3.0",
|
"react-dom": "^19.3.0",
|
||||||
|
"rolldown": "1.1.4",
|
||||||
"tailwind-merge": "^3.7.0",
|
"tailwind-merge": "^3.7.0",
|
||||||
"zustand": "^5.0.15"
|
"zustand": "^5.0.15"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/vite": "^4.3.3",
|
"@tailwindcss/vite": "4.3.1",
|
||||||
"@types/node": "^26.6.4",
|
"@types/node": "^26.6.4",
|
||||||
"@types/react": "^19.3.0",
|
"@types/react": "^19.3.0",
|
||||||
"@types/react-dom": "^19.3.0",
|
"@types/react-dom": "^19.3.0",
|
||||||
"@vitejs/plugin-react": "^6.1.1",
|
"@vitejs/plugin-react": "6.0.0",
|
||||||
"tailwindcss": "^4.3.3",
|
"tailwindcss": "4.3.1",
|
||||||
"typescript": "^5.7.3",
|
"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))
|
.ensureQueryData(productQueryOptions(handle))
|
||||||
.catch(() => null);
|
.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) {
|
if (product) {
|
||||||
queryClient.prefetchQuery(productRecommendationsQueryOptions(product.id));
|
await queryClient
|
||||||
|
.ensureQueryData(productRecommendationsQueryOptions(product.id))
|
||||||
|
.catch(() => undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { product };
|
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 { Hono } from 'hono';
|
||||||
import { account } from './account';
|
import { account } from './account';
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -17,6 +17,6 @@
|
|||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["src", "vite.config.ts"],
|
"include": ["src"],
|
||||||
"exclude": ["node_modules", "dist", ".output"]
|
"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 { defineConfig } from 'vite';
|
||||||
import { tanstackStart } from '@tanstack/react-start/plugin/vite';
|
import { tanstackStart } from '@tanstack/react-start/plugin/vite';
|
||||||
import { nitro } from 'nitro/vite';
|
|
||||||
import react from '@vitejs/plugin-react';
|
import react from '@vitejs/plugin-react';
|
||||||
import tailwindcss from '@tailwindcss/vite';
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
import { fileURLToPath, URL } from 'node:url';
|
import { fileURLToPath, URL } from 'node:url';
|
||||||
|
|
||||||
// Absolute origin for canonical and Open Graph URLs. Inlined at build time so
|
// 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
|
// the server render and the client agree on it; set VITE_SITE_URL in production.
|
||||||
// domain; set VITE_SITE_URL to override it anywhere.
|
const siteUrl = process.env.VITE_SITE_URL ?? 'http://localhost:3000';
|
||||||
const siteUrl =
|
|
||||||
process.env.VITE_SITE_URL ??
|
|
||||||
(process.env.VERCEL_PROJECT_PRODUCTION_URL
|
|
||||||
? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`
|
|
||||||
: 'http://localhost:3000');
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
server: { port: 3000 },
|
server: { port: 3000 },
|
||||||
plugins: [
|
plugins: [
|
||||||
// Start must come before the React plugin.
|
// Start must come before the React plugin.
|
||||||
tanstackStart(),
|
tanstackStart(),
|
||||||
// Builds the server for the host: Vercel's Build Output when deployed
|
|
||||||
// there, a standalone Node server (.output/) everywhere else.
|
|
||||||
nitro(),
|
|
||||||
react(),
|
react(),
|
||||||
tailwindcss(),
|
tailwindcss(),
|
||||||
],
|
],
|
||||||
Reference in New Issue
Block a user