diff --git a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/app/entry.server.tsx b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/app/entry.server.tsx index acf5b4782b20..a60364a5570f 100644 --- a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/app/entry.server.tsx +++ b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/app/entry.server.tsx @@ -1,7 +1,7 @@ import '../instrument.server'; +import type { EntryContext } from 'react-router'; import { HandleErrorFunction, ServerRouter } from 'react-router'; import { createContentSecurityPolicy } from '@shopify/hydrogen'; -import type { EntryContext } from '@shopify/remix-oxygen'; import { renderToReadableStream } from 'react-dom/server'; import * as Sentry from '@sentry/react-router/cloudflare'; diff --git a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/app/lib/context.ts b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/app/lib/context.ts new file mode 100644 index 000000000000..ed90c91d469e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/app/lib/context.ts @@ -0,0 +1,29 @@ +import { createHydrogenContext } from '@shopify/hydrogen'; +import { AppSession } from '~/lib/session'; +import { CART_QUERY_FRAGMENT } from '~/lib/fragments'; + +/** + * Creates Hydrogen context for React Router 7.x + */ +export async function createHydrogenRouterContext(request: Request, env: Env, executionContext: ExecutionContext) { + if (!env?.SESSION_SECRET) { + throw new Error('SESSION_SECRET environment variable is not set'); + } + + const waitUntil = executionContext.waitUntil.bind(executionContext); + const [cache, session] = await Promise.all([caches.open('hydrogen'), AppSession.init(request, [env.SESSION_SECRET])]); + + const hydrogenContext = createHydrogenContext({ + env, + request, + cache, + waitUntil, + session, + i18n: { language: 'EN', country: 'US' }, + cart: { + queryFragment: CART_QUERY_FRAGMENT, + }, + }); + + return hydrogenContext; +} diff --git a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/app/lib/session.ts b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/app/lib/session.ts index 80d6e7b86b52..439891c74ae9 100644 --- a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/app/lib/session.ts +++ b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/app/lib/session.ts @@ -1,5 +1,5 @@ import type { HydrogenSession } from '@shopify/hydrogen'; -import { type Session, type SessionStorage, createCookieSessionStorage } from '@shopify/remix-oxygen'; +import { type Session, type SessionStorage, createCookieSessionStorage } from 'react-router'; /** * This is a custom session implementation for your Hydrogen shop. @@ -9,12 +9,17 @@ import { type Session, type SessionStorage, createCookieSessionStorage } from '@ export class AppSession implements HydrogenSession { #sessionStorage; #session; + #isPending = false; constructor(sessionStorage: SessionStorage, session: Session) { this.#sessionStorage = sessionStorage; this.#session = session; } + get isPending() { + return this.#isPending; + } + static async init(request: Request, secrets: string[]) { const storage = createCookieSessionStorage({ cookie: { @@ -48,6 +53,7 @@ export class AppSession implements HydrogenSession { } get set() { + this.#isPending = true; return this.#session.set; } diff --git a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/app/root.tsx b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/app/root.tsx index afa85270e045..27283806379a 100644 --- a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/app/root.tsx +++ b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/app/root.tsx @@ -1,5 +1,5 @@ import * as Sentry from '@sentry/react-router/cloudflare'; -import { type LoaderFunctionArgs } from '@shopify/remix-oxygen'; +import type { LoaderFunctionArgs } from 'react-router'; import { Outlet, isRouteErrorResponse, @@ -9,8 +9,6 @@ import { Scripts, ScrollRestoration, } from 'react-router'; -import { FOOTER_QUERY, HEADER_QUERY } from '~/lib/fragments'; - import { useNonce } from '@shopify/hydrogen'; export type RootLoader = typeof loader; @@ -57,17 +55,14 @@ export function links() { } export async function loader(args: LoaderFunctionArgs) { - // Start fetching non-critical data without blocking time to first byte - const deferredData = loadDeferredData(args); - - // Await the critical data required to render initial state of the page - const criticalData = await loadCriticalData(args); - const { env } = args.context; + // Simplified loader for Sentry SDK testing - skip storefront queries return { - ...deferredData, - ...criticalData, + header: null, + cart: null, + isLoggedIn: false, + footer: null, ENV: { sentryTrace: env.SENTRY_TRACE, sentryBaggage: env.SENTRY_BAGGAGE, @@ -77,61 +72,12 @@ export async function loader(args: LoaderFunctionArgs) { checkoutDomain: env.PUBLIC_CHECKOUT_DOMAIN, storefrontAccessToken: env.PUBLIC_STOREFRONT_API_TOKEN, withPrivacyBanner: false, - // localize the privacy banner - country: args.context.storefront.i18n.country, - language: args.context.storefront.i18n.language, + country: 'US', + language: 'EN', }, }; } -/** - * Load data necessary for rendering content above the fold. This is the critical data - * needed to render the page. If it's unavailable, the whole page should 400 or 500 error. - */ -async function loadCriticalData({ context }: LoaderFunctionArgs) { - const { storefront } = context; - - const [header] = await Promise.all([ - storefront.query(HEADER_QUERY, { - cache: storefront.CacheLong(), - variables: { - headerMenuHandle: 'main-menu', // Adjust to your header menu handle - }, - }), - // Add other queries here, so that they are loaded in parallel - ]); - - return { header }; -} - -/** - * Load data for rendering content below the fold. This data is deferred and will be - * fetched after the initial page load. If it's unavailable, the page should still 200. - * Make sure to not throw any errors here, as it will cause the page to 500. - */ -function loadDeferredData({ context }: LoaderFunctionArgs) { - const { storefront, customerAccount, cart } = context; - - // defer the footer query (below the fold) - const footer = storefront - .query(FOOTER_QUERY, { - cache: storefront.CacheLong(), - variables: { - footerMenuHandle: 'footer', // Adjust to your footer menu handle - }, - }) - .catch((error: any) => { - // Log query errors, but don't throw them so the page can still render - console.error(error); - return null; - }); - return { - cart: cart.get(), - isLoggedIn: customerAccount.isLoggedIn(), - footer, - }; -} - export function Layout({ children }: { children?: React.ReactNode }) { const nonce = useNonce(); diff --git a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/app/routes/loader-error.tsx b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/app/routes/loader-error.tsx index 1548c38084ad..006094a17f74 100644 --- a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/app/routes/loader-error.tsx +++ b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/app/routes/loader-error.tsx @@ -1,5 +1,5 @@ import { useLoaderData } from 'react-router'; -import type { LoaderFunction } from '@shopify/remix-oxygen'; +import type { LoaderFunction } from 'react-router'; export default function LoaderError() { useLoaderData(); diff --git a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/app/routes/navigate.tsx b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/app/routes/navigate.tsx index 06ca3d7f2ae0..300e39fb6409 100644 --- a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/app/routes/navigate.tsx +++ b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/app/routes/navigate.tsx @@ -1,7 +1,8 @@ import { useLoaderData } from 'react-router'; -import type { LoaderFunction } from '@shopify/remix-oxygen'; +import type { LoaderFunction } from 'react-router'; -export const loader: LoaderFunction = async ({ params: { id } }) => { +export const loader: LoaderFunction = async ({ params }) => { + const { id } = params as { id: string }; if (id === '-1') { throw new Error('Unexpected Server Error'); } diff --git a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/env.d.ts b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/env.d.ts index ce37d9f3c464..8577d306258c 100644 --- a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/env.d.ts +++ b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/env.d.ts @@ -1,5 +1,4 @@ /// -/// /// // Enhance TypeScript's built-in typings. @@ -26,12 +25,15 @@ declare global { PUBLIC_CUSTOMER_ACCOUNT_API_CLIENT_ID: string; PUBLIC_CUSTOMER_ACCOUNT_API_URL: string; PUBLIC_CHECKOUT_DOMAIN: string; + SHOP_ID: string; + SENTRY_TRACE?: string; + SENTRY_BAGGAGE?: string; } } declare module 'react-router' { /** - * Declare local additions to the Remix loader context. + * Declare local additions to the React Router loader context. */ interface AppLoadContext { env: Env; diff --git a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/package.json b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/package.json index f564462c7779..01b6c5c1396c 100644 --- a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/package.json +++ b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/package.json @@ -17,21 +17,20 @@ "@sentry/cloudflare": "latest || *", "@sentry/react-router": "latest || *", "@sentry/vite-plugin": "^4.6.2", - "@shopify/hydrogen": "2025.5.0", - "@shopify/remix-oxygen": "^3.0.0", + "@shopify/hydrogen": "2025.7.3", "graphql": "^16.10.0", "graphql-tag": "^2.12.6", "isbot": "^5.1.22", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-router": "7.9.6", - "react-router-dom": "7.9.6" + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router": "7.12.0", + "react-router-dom": "7.12.0" }, "devDependencies": { "@graphql-codegen/cli": "5.0.2", "@playwright/test": "~1.56.0", - "@react-router/dev": "7.9.6", - "@react-router/fs-routes": "7.9.6", + "@react-router/dev": "7.12.0", + "@react-router/fs-routes": "7.12.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@shopify/cli": "3.80.4", "@shopify/hydrogen-codegen": "^0.3.3", diff --git a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/server.ts b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/server.ts index b430f97b1f44..65820e5a94c6 100644 --- a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/server.ts +++ b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/server.ts @@ -1,34 +1,11 @@ -import { - cartGetIdDefault, - cartSetIdDefault, - createCartHandler, - createCustomerAccountClient, - createStorefrontClient, - storefrontRedirect, -} from '@shopify/hydrogen'; -import { type AppLoadContext, createRequestHandler, getStorefrontHeaders } from '@shopify/remix-oxygen'; -import { CART_QUERY_FRAGMENT } from '~/lib/fragments'; -import { AppSession } from '~/lib/session'; -import { wrapRequestHandler } from '@sentry/cloudflare'; -// Virtual entry point for the app -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-expect-error import * as serverBuild from 'virtual:react-router/server-build'; +import { createRequestHandler, storefrontRedirect } from '@shopify/hydrogen'; +import { createHydrogenRouterContext } from '~/lib/context'; +import { wrapRequestHandler } from '@sentry/cloudflare'; /** * Export a fetch handler in module format. */ -type Env = { - SESSION_SECRET: string; - PUBLIC_STOREFRONT_API_TOKEN: string; - PRIVATE_STOREFRONT_API_TOKEN: string; - PUBLIC_STORE_DOMAIN: string; - PUBLIC_STOREFRONT_ID: string; - PUBLIC_CUSTOMER_ACCOUNT_API_CLIENT_ID: string; - PUBLIC_CUSTOMER_ACCOUNT_API_URL: string; - // Add any other environment variables your app expects here -}; - export default { async fetch(request: Request, env: Env, executionContext: ExecutionContext): Promise { return wrapRequestHandler( @@ -45,82 +22,35 @@ export default { }, async () => { try { - /** - * Open a cache instance in the worker and a custom session instance. - */ - if (!env?.SESSION_SECRET) { - throw new Error('SESSION_SECRET environment variable is not set'); - } - - const waitUntil = executionContext.waitUntil.bind(executionContext); - const [cache, session] = await Promise.all([ - caches.open('hydrogen'), - AppSession.init(request, [env.SESSION_SECRET]), - ]); + const hydrogenContext = await createHydrogenRouterContext(request, env, executionContext); /** - * Create Hydrogen's Storefront client. - */ - const { storefront } = createStorefrontClient({ - cache, - waitUntil, - i18n: { language: 'EN', country: 'US' }, - publicStorefrontToken: env.PUBLIC_STOREFRONT_API_TOKEN, - privateStorefrontToken: env.PRIVATE_STOREFRONT_API_TOKEN, - storeDomain: env.PUBLIC_STORE_DOMAIN, - storefrontId: env.PUBLIC_STOREFRONT_ID, - storefrontHeaders: getStorefrontHeaders(request), - }); - - /** - * Create a client for Customer Account API. - */ - const customerAccount = createCustomerAccountClient({ - waitUntil, - request, - session, - customerAccountId: env.PUBLIC_CUSTOMER_ACCOUNT_API_CLIENT_ID, - shopId: env.PUBLIC_STORE_DOMAIN, - }); - - /* - * Create a cart handler that will be used to - * create and update the cart in the session. - */ - const cart = createCartHandler({ - storefront, - customerAccount, - getCartId: cartGetIdDefault(request.headers), - setCartId: cartSetIdDefault(), - cartQueryFragment: CART_QUERY_FRAGMENT, - }); - - /** - * Create a Remix request handler and pass - * Hydrogen's Storefront client to the loader context. + * Create a Hydrogen request handler that internally + * delegates to React Router for routing and rendering. */ const handleRequest = createRequestHandler({ build: serverBuild, mode: process.env.NODE_ENV, - getLoadContext: (): AppLoadContext => ({ - session, - storefront, - customerAccount, - cart, - env, - waitUntil, - }), + getLoadContext: () => hydrogenContext, }); const response = await handleRequest(request); + if (hydrogenContext.session.isPending) { + response.headers.set('Set-Cookie', await hydrogenContext.session.commit()); + } + if (response.status === 404) { /** * Check for redirects only when there's a 404 from the app. * If the redirect doesn't exist, then `storefrontRedirect` * will pass through the 404 response. */ - return storefrontRedirect({ request, response, storefront }); + return storefrontRedirect({ + request, + response, + storefront: hydrogenContext.storefront, + }); } return response; diff --git a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/tsconfig.json b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/tsconfig.json index 6b1b95f76f6f..cbbb69030316 100644 --- a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/tsconfig.json +++ b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/tsconfig.json @@ -1,5 +1,14 @@ { - "include": ["server.ts", "./app/**/*.d.ts", "./app/**/*.ts", "./app/**/*.tsx", ".react-router/types/**/*"], + "include": [ + "env.d.ts", + "globals.d.ts", + "virtual-modules.d.ts", + "server.ts", + "./app/**/*.d.ts", + "./app/**/*.ts", + "./app/**/*.tsx", + ".react-router/types/**/*" + ], "compilerOptions": { "lib": ["DOM", "DOM.Iterable", "ES2022"], "isolatedModules": true, diff --git a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/virtual-modules.d.ts b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/virtual-modules.d.ts new file mode 100644 index 000000000000..5581f5422868 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/virtual-modules.d.ts @@ -0,0 +1,5 @@ +declare module 'virtual:react-router/server-build' { + import type { ServerBuild } from 'react-router'; + const build: ServerBuild; + export = build; +}