Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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: {
Expand Down Expand Up @@ -48,6 +53,7 @@ export class AppSession implements HydrogenSession {
}

get set() {
this.#isPending = true;
return this.#session.set;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -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();

Expand Down
Original file line number Diff line number Diff line change
@@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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');
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
/// <reference types="vite/client" />
/// <reference types="@shopify/remix-oxygen" />
/// <reference types="@shopify/oxygen-workers-types" />

// Enhance TypeScript's built-in typings.
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Response> {
return wrapRequestHandler(
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
Loading
Loading