Skip to content

fix(examples): make a fresh clone run with zero setup (token fallback + dev race)#3825

Open
div-cowboy wants to merge 7 commits into
Shopify:previewfrom
div-cowboy:fix/examples-private-token-fallback
Open

fix(examples): make a fresh clone run with zero setup (token fallback + dev race)#3825
div-cowboy wants to merge 7 commits into
Shopify:previewfrom
div-cowboy:fix/examples-private-token-fallback

Conversation

@div-cowboy

@div-cowboy div-cowboy commented Jun 30, 2026

Copy link
Copy Markdown

TLDR

When an engineer pulls down the hydrogen examples, they should be able to just run pnpm install and pnpm dev and have all of the apps working on different ports. This still throws the message letting them know that if they want to overwrite the default Shopify token, they can do so in the console.

Problem

A fresh clone of the preview branch can't cleanly run the example apps. Three things break a zero-setup pnpm dev:

  1. SSR 500s — every example throws on the first request:

    Error: PRIVATE_STOREFRONT_API_TOKEN is required for SSR requests.
    Run "pnpm run examples:secrets:decrypt" to create examples/shared/secrets.ts.
    

    The referenced examples:secrets:decrypt script didn't exist, and even with it ejson decrypt needs a Shopify-internal key — so external contributors have no path to the demo store's private token, and the shared helper just threw.

  2. A wall of "Module not found" at startuppnpm dev spews transient errors before settling:

    Module not found: Can't resolve '@shopify/hydrogen'
    
  3. Port collisionspnpm dev runs all example apps at once, but next dev and nuxt dev both default to :3000, so they fought over the port non-deterministically. Opening localhost:3000 returned whichever won the race (or nothing).

Together these mean a fresh clone can't just pnpm dev and see a storefront.

Fix

1. Zero-config public-token fallback (the 500)

The demo store's public Storefront token is already committed in examples/shared/config.ts, and createStorefrontClient supports type: "public". New shared factory examples/shared/storefront-client.ts:

  • createExampleStorefrontClient() builds a type: "private" (buyer-isolated SSR) client when PRIVATE_STOREFRONT_API_TOKEN is set (env first, then secrets.ts), and otherwise falls back to a type: "public" client using the committed demo-store token — so a fresh clone renders the hydrogen-preview demo store with zero setup. Warns once when falling back.
  • @shopify/hydrogen is imported type-only here and createStorefrontClient is injected by each caller, so the shared module (pulled into every example via the @shared/* path alias) carries no runtime dependency on the package.
  • All six framework examples (Next.js, React Router, SvelteKit, Astro, Solid, Nuxt) build their client through this factory.

2. Stop the @shopify/hydrogen watch from racing example bundlers (the "Module not found" wall)

Root cause: pnpm dev runs @shopify/hydrogen#build (one-shot, via turbo ^build) and @shopify/hydrogen#dev (tsdown --watch). The watch's default startup clean wipes the just-built dist/ while the example bundlers are reading it.

Fix (packages/hydrogen/tsdown.config.ts): skip the clean in --watch mode only. dist/ stays populated from the build, rolldown writes each output atomically, so example bundlers always resolve a complete module. Production builds still clean, and SDK hot reload is preserved (the watch keeps running).

Supersedes #3826, which removed the watch from the dev scripts — that fixed the race but cost SDK hot reload. This keeps the watch.

3. Deterministic ports + per-example dev scripts (the collision)

Pin a distinct port per example so pnpm dev runs all of them at once with no collision, and each can be opened at a known URL:

Example Port Example Port
Next.js 3000 SvelteKit 3004
Nuxt 3001 Astro 3005
Hydrogen (Oxygen) 3002 Solid Start 3006
React Router 3003

Added the missing per-example scripts so any single storefront runs on its own port with SDK hot reload: dev:nuxt, dev:svelte, dev:astro, dev:solid (joining the existing dev:next, dev:rr, dev:hydrogen).

Verification

Fresh-clone simulation, all on this branch:

  • pnpm dev (all 7 servers): each binds its assigned port, 0 collisions, 0 "Module not found" errors (previously ~22).
  • The six framework examples return HTTP 200 rendering Mock.shop — Hydrogen (demo store) with no token configured.
  • Per-example scripts (e.g. pnpm dev:svelte) run a single app on its port; SDK hot reload confirmed (edit packages/hydrogen/srctsdown rebuilds → example reloads).
  • Production @shopify/hydrogen build still cleans dist/.
  • typecheck, oxlint (0/0), and oxfmt all clean on changed files.
  • Setting PRIVATE_STOREFRONT_API_TOKEN still routes through the private (buyer-isolated) client.

A fresh clone hits a 503 on SSR because the shared
getPrivateStorefrontToken() helper only reads the committed (empty)
secrets.ts, and the error it throws points at "pnpm run
examples:secrets:decrypt" — a script that didn't exist. Even with it,
ejson decrypt needs the EJSON key, which external contributors don't
have, so the demo token was unreachable.

- Fall back to process.env.PRIVATE_STOREFRONT_API_TOKEN in the shared
  helper, matching the env-first pattern the hydrogen example already
  uses, so the Next.js/React Router/SvelteKit/Astro/Solid/Nuxt examples
  all gain an env-var escape hatch.
- Add the missing examples:secrets:decrypt script (mirrors :encrypt).
- Reword the error so it's accurate for both external users (set the
  env var) and maintainers (decrypt + update secrets.ts).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@div-cowboy
div-cowboy requested a review from a team as a code owner June 30, 2026 20:39
…lback

A fresh clone 503s on SSR for every framework example: the shared
getPrivateStorefrontToken() throws when no PRIVATE_STOREFRONT_API_TOKEN
is set, and external contributors can't supply one (the demo store's
private token is encrypted with a Shopify-internal EJSON key).

Add a shared createExampleStorefrontClient() factory that builds a
private (buyer-isolated SSR) client when a private token is available
and otherwise falls back to the public token already committed in
config.ts — so `pnpm dev` renders the hydrogen-preview demo store with
no setup. Setting PRIVATE_STOREFRONT_API_TOKEN still opts into the
private path.

- examples/shared/storefront-client.ts: the factory. Public and private
  clients share the same graphql/requestContext surface and nothing
  branches on the client's `type`, so the fallback is transparent to the
  cart-routes/redirects helpers (typed for the private client).
- examples/shared/private-env.ts: add non-throwing
  getOptionalPrivateStorefrontToken().
- Route nextjs, react-router, sveltekit, astro, solid-start, and nuxt
  through the factory.
- examples/shared becomes a workspace package so it can resolve
  @shopify/hydrogen (it's the first shared module to import it).

The hydrogen (Oxygen) example uses a different store config and is left
as a follow-up.

Verified: `pnpm dev:next` serves / and /collections with HTTP 200 (was
503); typecheck (9/9) and lint pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@div-cowboy div-cowboy changed the title fix: let examples read PRIVATE_STOREFRONT_API_TOKEN from env fix(examples): render demo store with zero setup (public-token fallback) Jun 30, 2026
The shared storefront-client module is pulled into each example via the
@shared/* path alias, so a *value* import of @shopify/hydrogen forced
every example's bundler (Turbopack, Vite, …) to resolve the package from
examples/shared. That resolution relies on a pnpm symlink that the
bundlers handle unreliably, producing:

  Module not found: Can't resolve '@shopify/hydrogen'

Import @shopify/hydrogen as a type only (erased at runtime, so no bundler
resolves it from examples/shared) and inject `createStorefrontClient` from
each example, where the package has always resolved. The single
public->private cast stays in the shared factory.

Verified with examples/shared/node_modules removed entirely (the broken
state): `pnpm dev:next` and `dev:rr` serve / and /collections at HTTP 200
with no module-not-found. typecheck (9/9) and lint still pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@div-cowboy
div-cowboy force-pushed the fix/examples-private-token-fallback branch from 4770627 to c27c7fa Compare June 30, 2026 22:06
oxfmt's sortImports groups third-party imports before relative ones.
Reorder so `pnpm run format:check` passes in CI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@div-cowboy

Copy link
Copy Markdown
Author

I have signed the CLA!

`pnpm dev` runs `@shopify/hydrogen#build` (one-shot, via turbo `^build`)
and then `@shopify/hydrogen#dev` (`tsdown --watch`). The watch's default
startup clean wipes the just-built `dist/` while the example bundlers are
reading it, spewing transient "Module not found: Can't resolve
'@shopify/hydrogen'" at every example.

Disable clean in `--watch` mode only: `dist/` stays populated from the
build, rolldown writes each output atomically, so example bundlers always
resolve a complete module. Production builds still clean; SDK hot reload
is preserved (verified: edit src -> tsdown rebuilds -> example reloads).

This replaces the dev-script workaround from Shopify#3826 (which removed the
watch and so lost SDK hot reload).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@div-cowboy div-cowboy changed the title fix(examples): render demo store with zero setup (public-token fallback) fix(examples): make a fresh clone run with zero setup (token fallback + dev race) Jun 30, 2026
div-cowboy and others added 2 commits June 30, 2026 19:36
`pnpm dev` runs all example apps at once, but `next dev` and `nuxt dev`
both default to :3000, so they collided non-deterministically — opening
localhost:3000 returned whichever won the race (or nothing).

Pin a distinct port per example (nextjs 3000, nuxt 3001, react-router
3003, sveltekit 3004, astro 3005, solid-start 3006; the Oxygen app keeps
3002), and add the missing per-example dev scripts (`dev:nuxt`,
`dev:svelte`, `dev:astro`, `dev:solid`) so any single storefront can be
run on a known port. `pnpm dev` now starts all of them with no collision.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@div-cowboy

Copy link
Copy Markdown
Author

@jplhomer @frandiox Is this not something wanted for the DX for Hydrogen? Happy to close if not wanted, just trying to start contributing

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant