From 9e3d88d498efaa20fe23de9837e0f444180bc787 Mon Sep 17 00:00:00 2001
From: Fran Dios
Date: Thu, 11 Jan 2024 18:13:28 +0900
Subject: [PATCH] Turn examples into template diffs for better maintenance
(#1549)
* Refactor symlink in init.test
* Add diff feature to dev command
* Add example diff
* Fix watcher when deleting files
* Allow importing from current dir and skeleton template at the same time using aliases
* Support --diff in build command
* Add build script to example
* Add turbo dependency
* Merge main
* Remove examples/diff
* Use --diff in examples/third-party-queries-caching
* Update package-lock
* Remove unnecessary files
* Refactor mergePackageJson
* Refactor mergePackageJson, move to another file
* Merge package.json with --diff flag
* Refactor
* Rename file, refactor
* Changesets
* Update example readme
* Convert examples/optimistic-cart-ui to diff
* Update examples/README.md
Co-authored-by: Michelle Chen
* Update package-lock
* Fix finding storefront.generated.d.ts
* Fix build dependencies
---------
Co-authored-by: Michelle Chen
---
.changeset/silly-vans-bake.md | 5 +
.gitignore | 2 +
examples/README.md | 15 +-
examples/optimistic-cart-ui/.eslintignore | 5 -
examples/optimistic-cart-ui/.eslintrc.js | 18 -
examples/optimistic-cart-ui/.gitignore | 8 -
examples/optimistic-cart-ui/.graphqlrc.yml | 1 -
.../app/components/Aside.tsx | 47 --
.../app/components/Header.tsx | 93 ----
.../app/components/Layout.tsx | 44 --
.../optimistic-cart-ui/app/entry.client.tsx | 12 -
.../optimistic-cart-ui/app/entry.server.tsx | 41 --
examples/optimistic-cart-ui/app/root.tsx | 143 ------
.../optimistic-cart-ui/app/routes/_index.tsx | 86 ----
.../app/routes/cart.$lines.tsx | 69 ---
.../optimistic-cart-ui/app/routes/cart.tsx | 102 ----
.../app/routes/products.$handle.tsx | 427 ----------------
.../optimistic-cart-ui/app/styles/app.css | 473 ------------------
.../optimistic-cart-ui/app/styles/reset.css | 129 -----
examples/optimistic-cart-ui/app/utils.ts | 46 --
examples/optimistic-cart-ui/package.json | 37 +-
.../optimistic-cart-ui/public/favicon.svg | 28 --
examples/optimistic-cart-ui/remix.config.js | 19 -
examples/optimistic-cart-ui/remix.env.d.ts | 42 --
examples/optimistic-cart-ui/server.ts | 257 ----------
.../storefrontapi.generated.d.ts | 386 --------------
examples/optimistic-cart-ui/tsconfig.json | 20 +-
.../third-party-queries-caching/.eslintignore | 5 -
.../third-party-queries-caching/.eslintrc.js | 18 -
.../third-party-queries-caching/.gitignore | 8 -
.../.graphqlrc.yml | 1 -
.../third-party-queries-caching/README.md | 4 +-
.../app/entry.client.tsx | 12 -
.../app/entry.server.tsx | 41 --
.../third-party-queries-caching/app/root.tsx | 49 --
.../app/styles/app.css | 473 ------------------
.../app/styles/reset.css | 129 -----
.../third-party-queries-caching/package.json | 36 +-
.../public/favicon.svg | 28 --
.../remix.config.js | 19 -
.../storefrontapi.generated.d.ts | 115 -----
.../third-party-queries-caching/tsconfig.json | 20 +-
package-lock.json | 196 +++-----
packages/cli/oclif.manifest.json | 14 +
packages/cli/package.json | 1 +
packages/cli/src/commands/hydrogen/build.ts | 27 +-
packages/cli/src/commands/hydrogen/dev.ts | 12 +-
.../cli/src/commands/hydrogen/init.test.ts | 10 +-
packages/cli/src/lib/build.ts | 9 +
packages/cli/src/lib/file.ts | 88 +++-
packages/cli/src/lib/flags.ts | 6 +
packages/cli/src/lib/mini-oxygen/node.ts | 4 +
packages/cli/src/lib/request-events.ts | 4 +-
packages/cli/src/lib/setups/css/assets.ts | 76 ---
.../cli/src/lib/setups/css/css-modules.ts | 5 +-
packages/cli/src/lib/setups/css/postcss.ts | 6 +-
packages/cli/src/lib/setups/css/tailwind.ts | 8 +-
.../cli/src/lib/setups/css/vanilla-extract.ts | 7 +-
packages/cli/src/lib/template-diff.ts | 120 +++++
59 files changed, 395 insertions(+), 3711 deletions(-)
create mode 100644 .changeset/silly-vans-bake.md
delete mode 100644 examples/optimistic-cart-ui/.eslintignore
delete mode 100644 examples/optimistic-cart-ui/.eslintrc.js
delete mode 100644 examples/optimistic-cart-ui/.gitignore
delete mode 100644 examples/optimistic-cart-ui/.graphqlrc.yml
delete mode 100644 examples/optimistic-cart-ui/app/components/Aside.tsx
delete mode 100644 examples/optimistic-cart-ui/app/components/Header.tsx
delete mode 100644 examples/optimistic-cart-ui/app/components/Layout.tsx
delete mode 100644 examples/optimistic-cart-ui/app/entry.client.tsx
delete mode 100644 examples/optimistic-cart-ui/app/entry.server.tsx
delete mode 100644 examples/optimistic-cart-ui/app/root.tsx
delete mode 100644 examples/optimistic-cart-ui/app/routes/_index.tsx
delete mode 100644 examples/optimistic-cart-ui/app/routes/cart.$lines.tsx
delete mode 100644 examples/optimistic-cart-ui/app/routes/cart.tsx
delete mode 100644 examples/optimistic-cart-ui/app/routes/products.$handle.tsx
delete mode 100644 examples/optimistic-cart-ui/app/styles/app.css
delete mode 100644 examples/optimistic-cart-ui/app/styles/reset.css
delete mode 100644 examples/optimistic-cart-ui/app/utils.ts
delete mode 100644 examples/optimistic-cart-ui/public/favicon.svg
delete mode 100644 examples/optimistic-cart-ui/remix.config.js
delete mode 100644 examples/optimistic-cart-ui/remix.env.d.ts
delete mode 100644 examples/optimistic-cart-ui/server.ts
delete mode 100644 examples/optimistic-cart-ui/storefrontapi.generated.d.ts
delete mode 100644 examples/third-party-queries-caching/.eslintignore
delete mode 100644 examples/third-party-queries-caching/.eslintrc.js
delete mode 100644 examples/third-party-queries-caching/.gitignore
delete mode 100644 examples/third-party-queries-caching/.graphqlrc.yml
delete mode 100644 examples/third-party-queries-caching/app/entry.client.tsx
delete mode 100644 examples/third-party-queries-caching/app/entry.server.tsx
delete mode 100644 examples/third-party-queries-caching/app/root.tsx
delete mode 100644 examples/third-party-queries-caching/app/styles/app.css
delete mode 100644 examples/third-party-queries-caching/app/styles/reset.css
delete mode 100644 examples/third-party-queries-caching/public/favicon.svg
delete mode 100644 examples/third-party-queries-caching/remix.config.js
delete mode 100644 examples/third-party-queries-caching/storefrontapi.generated.d.ts
create mode 100644 packages/cli/src/lib/template-diff.ts
diff --git a/.changeset/silly-vans-bake.md b/.changeset/silly-vans-bake.md
new file mode 100644
index 0000000000..221fc93531
--- /dev/null
+++ b/.changeset/silly-vans-bake.md
@@ -0,0 +1,5 @@
+---
+'@shopify/cli-hydrogen': minor
+---
+
+Add `--diff` flag to dev and build commands to run examples in monorepo. Examples are now a diff applied on top of the starter template.
diff --git a/.gitignore b/.gitignore
index ebfc9de2f4..8f23240b50 100644
--- a/.gitignore
+++ b/.gitignore
@@ -137,3 +137,5 @@ yarn.lock
admin.schema.json
business-platform.schema.json
+
+examples/**/*.generated.d.ts
\ No newline at end of file
diff --git a/examples/README.md b/examples/README.md
index 5697be616e..8fc5232697 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -2,7 +2,7 @@
The example apps in this directory show how to implement popular design patterns in Hydrogen.
-Each example is a complete Hydrogen app, based on Hydrogen’s [Skeleton template](/templates/skeleton/), that you can inspect and run locally. See each example's README file for details on the problem it solves, how the solution works, and any other requirements you’ll need.
+Each example only contains the files that are different from Hydrogen’s [Skeleton template](/templates/skeleton/), but it's still possible to inspect and run them locally. See each example's README file for details on the problem it solves, how the solution works, and any other requirements you’ll need.
Examples are kept intentionally minimal, containing only the new and updated code required to illustrate a working use case.
@@ -21,3 +21,16 @@ If you don’t see the example you’re looking for, you can [request one throug
## Contributing examples
Hydrogen is an open-source project, and we welcome your input! See the Hydrogen [contribution docs](/docs/examples/README.md) for more details on how to add your own examples to this repo.
+
+### Creating new examples as diffs
+
+An example diff is a partial Hydrogen app that only contains the files that change from the skeleton template. The Hydrogen CLI will merge the example diff on top of the skeleton template in a temporary directory before running the `dev` or `build` commands.
+
+Keep the following in mind when creating a new example diff:
+
+- Start a new example diff by copying another minimal example and changing code.
+- Only include and commit files that are different from the skeleton template.
+- `package.json` must always be created with a unique name with example- prefix, and this name must be included in the NPM workspace (root `package.json`).
+- `dependencies`, `devDependencies`, and `peerDependencies` in `package.json` are also merged to those in skeleton. Therefore, only list new or modified dependencies in the example.
+- The scripts in `package.json` must pass the `--diff` flag to the `dev` and `build` commands. Otherwise, it will be treated as a full Hydrogen app instead of a diff.
+- The `tsconfig.json` must have special values. Copy it from another existing diff example.
diff --git a/examples/optimistic-cart-ui/.eslintignore b/examples/optimistic-cart-ui/.eslintignore
deleted file mode 100644
index a362bcaa13..0000000000
--- a/examples/optimistic-cart-ui/.eslintignore
+++ /dev/null
@@ -1,5 +0,0 @@
-build
-node_modules
-bin
-*.d.ts
-dist
diff --git a/examples/optimistic-cart-ui/.eslintrc.js b/examples/optimistic-cart-ui/.eslintrc.js
deleted file mode 100644
index 57a969e3ad..0000000000
--- a/examples/optimistic-cart-ui/.eslintrc.js
+++ /dev/null
@@ -1,18 +0,0 @@
-/**
- * @type {import("@types/eslint").Linter.BaseConfig}
- */
-module.exports = {
- extends: [
- '@remix-run/eslint-config',
- 'plugin:hydrogen/recommended',
- 'plugin:hydrogen/typescript',
- ],
- rules: {
- '@typescript-eslint/ban-ts-comment': 'off',
- '@typescript-eslint/naming-convention': 'off',
- 'hydrogen/prefer-image-component': 'off',
- 'no-useless-escape': 'off',
- '@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
- 'no-case-declarations': 'off',
- },
-};
diff --git a/examples/optimistic-cart-ui/.gitignore b/examples/optimistic-cart-ui/.gitignore
deleted file mode 100644
index 336224ba36..0000000000
--- a/examples/optimistic-cart-ui/.gitignore
+++ /dev/null
@@ -1,8 +0,0 @@
-node_modules
-/.cache
-/build
-/dist
-/public/build
-/.mf
-.env
-.shopify
diff --git a/examples/optimistic-cart-ui/.graphqlrc.yml b/examples/optimistic-cart-ui/.graphqlrc.yml
deleted file mode 100644
index bd38d076bc..0000000000
--- a/examples/optimistic-cart-ui/.graphqlrc.yml
+++ /dev/null
@@ -1 +0,0 @@
-schema: node_modules/@shopify/hydrogen-react/storefront.schema.json
diff --git a/examples/optimistic-cart-ui/app/components/Aside.tsx b/examples/optimistic-cart-ui/app/components/Aside.tsx
deleted file mode 100644
index f486f1992e..0000000000
--- a/examples/optimistic-cart-ui/app/components/Aside.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-/**
- * A side bar component with Overlay that works without JavaScript.
- * @example
- * ```jsx
- *
- * ```
- */
-export function Aside({
- children,
- heading,
- id = 'aside',
-}: {
- children?: React.ReactNode;
- heading: React.ReactNode;
- id?: string;
-}) {
- return (
-
-
{
- history.go(-1);
- window.location.hash = '';
- }}
- />
-
-
- );
-}
-
-function CloseAside() {
- return (
- /* eslint-disable-next-line jsx-a11y/anchor-is-valid */
- history.go(-1)}>
- ×
-
- );
-}
diff --git a/examples/optimistic-cart-ui/app/components/Header.tsx b/examples/optimistic-cart-ui/app/components/Header.tsx
deleted file mode 100644
index 3577fda7b1..0000000000
--- a/examples/optimistic-cart-ui/app/components/Header.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-import {Await, NavLink} from '@remix-run/react';
-import {Suspense} from 'react';
-import type {LayoutProps} from './Layout';
-
-type HeaderProps = Pick;
-
-type Viewport = 'desktop' | 'mobile';
-
-export function Header({cart}: HeaderProps) {
- return (
-
-
- Optimistic Cart UI example
-
-
-
-
- );
-}
-
-export function HeaderMenu({viewport}: {viewport: Viewport}) {
- const className = `header-menu-${viewport}`;
-
- function closeAside(event: React.MouseEvent) {
- if (viewport === 'mobile') {
- event.preventDefault();
- window.location.href = event.currentTarget.href;
- }
- }
-
- return (
-
- {viewport === 'mobile' && (
-
- Home
-
- )}
-
- );
-}
-
-function HeaderCtas({cart}: Pick) {
- return (
-
-
-
-
- );
-}
-
-function HeaderMenuMobileToggle() {
- return (
-
- ☰
-
- );
-}
-
-function CartBadge({count}: {count: number}) {
- return Cart {count} ;
-}
-
-function CartToggle({cart}: Pick) {
- return (
- }>
-
- {(cart) => {
- if (!cart) return ;
- return ;
- }}
-
-
- );
-}
-
-function activeLinkStyle({
- isActive,
- isPending,
-}: {
- isActive: boolean;
- isPending: boolean;
-}) {
- return {
- fontWeight: isActive ? 'bold' : undefined,
- color: isPending ? 'grey' : 'black',
- };
-}
diff --git a/examples/optimistic-cart-ui/app/components/Layout.tsx b/examples/optimistic-cart-ui/app/components/Layout.tsx
deleted file mode 100644
index 3208cabbd7..0000000000
--- a/examples/optimistic-cart-ui/app/components/Layout.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import {Await} from '@remix-run/react';
-import {Suspense} from 'react';
-import type {CartApiQueryFragment} from 'storefrontapi.generated';
-import {Aside} from '~/components/Aside';
-import {Header, HeaderMenu} from '~/components/Header';
-import {CartMain} from '~/components/Cart';
-
-export type LayoutProps = {
- cart: Promise;
- children?: React.ReactNode;
-};
-
-export function Layout({cart, children = null}: LayoutProps) {
- return (
- <>
-
-
-
- {children}
- >
- );
-}
-
-function CartAside({cart}: {cart: LayoutProps['cart']}) {
- return (
-
}>
-
- {(cart) => {
- return ;
- }}
-
-
-
- );
-}
-
-function MobileMenuAside() {
- return (
-
- );
-}
diff --git a/examples/optimistic-cart-ui/app/entry.client.tsx b/examples/optimistic-cart-ui/app/entry.client.tsx
deleted file mode 100644
index ba957c430e..0000000000
--- a/examples/optimistic-cart-ui/app/entry.client.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import {RemixBrowser} from '@remix-run/react';
-import {startTransition, StrictMode} from 'react';
-import {hydrateRoot} from 'react-dom/client';
-
-startTransition(() => {
- hydrateRoot(
- document,
-
-
- ,
- );
-});
diff --git a/examples/optimistic-cart-ui/app/entry.server.tsx b/examples/optimistic-cart-ui/app/entry.server.tsx
deleted file mode 100644
index a645a41078..0000000000
--- a/examples/optimistic-cart-ui/app/entry.server.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import type {EntryContext} from '@shopify/remix-oxygen';
-import {RemixServer} from '@remix-run/react';
-import isbot from 'isbot';
-import {renderToReadableStream} from 'react-dom/server';
-import {createContentSecurityPolicy} from '@shopify/hydrogen';
-
-export default async function handleRequest(
- request: Request,
- responseStatusCode: number,
- responseHeaders: Headers,
- remixContext: EntryContext,
-) {
- const {nonce, header, NonceProvider} = createContentSecurityPolicy();
-
- const body = await renderToReadableStream(
-
-
- ,
- {
- nonce,
- signal: request.signal,
- onError(error) {
- // eslint-disable-next-line no-console
- console.error(error);
- responseStatusCode = 500;
- },
- },
- );
-
- if (isbot(request.headers.get('user-agent'))) {
- await body.allReady;
- }
-
- responseHeaders.set('Content-Type', 'text/html');
- responseHeaders.set('Content-Security-Policy', header);
-
- return new Response(body, {
- headers: responseHeaders,
- status: responseStatusCode,
- });
-}
diff --git a/examples/optimistic-cart-ui/app/root.tsx b/examples/optimistic-cart-ui/app/root.tsx
deleted file mode 100644
index b476c06d63..0000000000
--- a/examples/optimistic-cart-ui/app/root.tsx
+++ /dev/null
@@ -1,143 +0,0 @@
-import {useNonce} from '@shopify/hydrogen';
-import {
- defer,
- type SerializeFrom,
- type LoaderFunctionArgs,
-} from '@shopify/remix-oxygen';
-import {
- Links,
- Meta,
- Outlet,
- Scripts,
- LiveReload,
- useMatches,
- useRouteError,
- useLoaderData,
- ScrollRestoration,
- isRouteErrorResponse,
- type ShouldRevalidateFunction,
-} from '@remix-run/react';
-import favicon from '../public/favicon.svg';
-import resetStyles from './styles/reset.css';
-import appStyles from './styles/app.css';
-import {Layout} from '~/components/Layout';
-
-/**
- * This is important to avoid re-fetching root queries on sub-navigations
- */
-export const shouldRevalidate: ShouldRevalidateFunction = ({
- formMethod,
- currentUrl,
- nextUrl,
-}) => {
- // revalidate when a mutation is performed e.g add to cart, login...
- if (formMethod && formMethod !== 'GET') {
- return true;
- }
-
- // revalidate when manually revalidating via useRevalidator
- if (currentUrl.toString() === nextUrl.toString()) {
- return true;
- }
-
- return false;
-};
-
-export function links() {
- return [
- {rel: 'stylesheet', href: resetStyles},
- {rel: 'stylesheet', href: appStyles},
- {
- rel: 'preconnect',
- href: 'https://cdn.shopify.com',
- },
- {
- rel: 'preconnect',
- href: 'https://shop.app',
- },
- {rel: 'icon', type: 'image/svg+xml', href: favicon},
- ];
-}
-
-export const useRootLoaderData = () => {
- const [root] = useMatches();
- return root?.data as SerializeFrom;
-};
-
-export async function loader({context}: LoaderFunctionArgs) {
- const {cart} = context;
- const publicStoreDomain = context.env.PUBLIC_STORE_DOMAIN;
-
- const cartPromise = cart.get();
-
- return defer({
- cart: cartPromise,
- publicStoreDomain,
- });
-}
-
-export default function App() {
- const nonce = useNonce();
- const data = useLoaderData();
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-export function ErrorBoundary() {
- const error = useRouteError();
- const rootData = useRootLoaderData();
- const nonce = useNonce();
- let errorMessage = 'Unknown error';
- let errorStatus = 500;
-
- if (isRouteErrorResponse(error)) {
- errorMessage = error?.data?.message ?? error.data;
- errorStatus = error.status;
- } else if (error instanceof Error) {
- errorMessage = error.message;
- }
-
- return (
-
-
-
-
-
-
-
-
-
-
-
Oops
-
{errorStatus}
- {errorMessage && (
-
- {errorMessage}
-
- )}
-
-
-
-
-
-
-
- );
-}
diff --git a/examples/optimistic-cart-ui/app/routes/_index.tsx b/examples/optimistic-cart-ui/app/routes/_index.tsx
deleted file mode 100644
index b0b7830ca1..0000000000
--- a/examples/optimistic-cart-ui/app/routes/_index.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-import {json, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
-import {useLoaderData, Link, type MetaFunction} from '@remix-run/react';
-import {Image, Money} from '@shopify/hydrogen';
-import type {RecommendedProductsQuery} from 'storefrontapi.generated';
-
-export const meta: MetaFunction = () => {
- return [{title: 'Hydrogen | Home'}];
-};
-
-export async function loader({context}: LoaderFunctionArgs) {
- const {storefront} = context;
- const recommendedProducts = await storefront.query(
- RECOMMENDED_PRODUCTS_QUERY,
- );
- return json({recommendedProducts});
-}
-
-export default function Homepage() {
- const {recommendedProducts} = useLoaderData();
- return (
-
-
-
- );
-}
-
-function RecommendedProducts({
- products,
-}: {
- products: RecommendedProductsQuery['products'];
-}) {
- return (
-
-
Recommended Products
-
- {products.nodes.map((product) => (
-
-
-
{product.title}
-
-
-
-
- ))}
-
-
- );
-}
-
-const RECOMMENDED_PRODUCTS_QUERY = `#graphql
- fragment RecommendedProduct on Product {
- id
- title
- handle
- priceRange {
- minVariantPrice {
- amount
- currencyCode
- }
- }
- images(first: 1) {
- nodes {
- id
- url
- altText
- width
- height
- }
- }
- }
- query RecommendedProducts ($country: CountryCode, $language: LanguageCode)
- @inContext(country: $country, language: $language) {
- products(first: 4, sortKey: UPDATED_AT, reverse: true) {
- nodes {
- ...RecommendedProduct
- }
- }
- }
-` as const;
diff --git a/examples/optimistic-cart-ui/app/routes/cart.$lines.tsx b/examples/optimistic-cart-ui/app/routes/cart.$lines.tsx
deleted file mode 100644
index fab617e264..0000000000
--- a/examples/optimistic-cart-ui/app/routes/cart.$lines.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-import {redirect, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
-
-/**
- * Automatically creates a new cart based on the URL and redirects straight to checkout.
- * Expected URL structure:
- * ```js
- * /cart/:
- *
- * ```
- *
- * More than one `:` separated by a comma, can be supplied in the URL, for
- * carts with more than one product variant.
- *
- * @example
- * Example path creating a cart with two product variants, different quantities, and a discount code in the querystring:
- * ```js
- * /cart/41007289663544:1,41007289696312:2?discount=HYDROBOARD
- *
- * ```
- */
-export async function loader({request, context, params}: LoaderFunctionArgs) {
- const {cart} = context;
- const {lines} = params;
- if (!lines) return redirect('/cart');
- const linesMap = lines.split(',').map((line) => {
- const lineDetails = line.split(':');
- const variantId = lineDetails[0];
- const quantity = parseInt(lineDetails[1], 10);
-
- return {
- merchandiseId: `gid://shopify/ProductVariant/${variantId}`,
- quantity,
- };
- });
-
- const url = new URL(request.url);
- const searchParams = new URLSearchParams(url.search);
-
- const discount = searchParams.get('discount');
- const discountArray = discount ? [discount] : [];
-
- // create a cart
- const result = await cart.create({
- lines: linesMap,
- discountCodes: discountArray,
- });
-
- const cartResult = result.cart;
-
- if (result.errors?.length || !cartResult) {
- throw new Response('Link may be expired. Try checking the URL.', {
- status: 410,
- });
- }
-
- // Update cart id in cookie
- const headers = cart.setCartId(cartResult.id);
-
- // redirect to checkout
- if (cartResult.checkoutUrl) {
- return redirect(cartResult.checkoutUrl, {headers});
- } else {
- throw new Error('No checkout URL found');
- }
-}
-
-export default function Component() {
- return null;
-}
diff --git a/examples/optimistic-cart-ui/app/routes/cart.tsx b/examples/optimistic-cart-ui/app/routes/cart.tsx
deleted file mode 100644
index 6174735d0f..0000000000
--- a/examples/optimistic-cart-ui/app/routes/cart.tsx
+++ /dev/null
@@ -1,102 +0,0 @@
-import {Await, type MetaFunction} from '@remix-run/react';
-import {Suspense} from 'react';
-import type {CartQueryData} from '@shopify/hydrogen';
-import {CartForm} from '@shopify/hydrogen';
-import {json, type ActionFunctionArgs} from '@shopify/remix-oxygen';
-import {CartMain} from '~/components/Cart';
-import {useRootLoaderData} from '~/root';
-
-export const meta: MetaFunction = () => {
- return [{title: `Hydrogen | Cart`}];
-};
-
-export async function action({request, context}: ActionFunctionArgs) {
- const {cart} = context;
-
- const formData = await request.formData();
-
- const {action, inputs} = CartForm.getFormInput(formData);
-
- if (!action) {
- throw new Error('No action provided');
- }
-
- let status = 200;
- let result: CartQueryData;
-
- switch (action) {
- case CartForm.ACTIONS.LinesAdd:
- result = await cart.addLines(inputs.lines);
- break;
- case CartForm.ACTIONS.LinesUpdate:
- result = await cart.updateLines(inputs.lines);
- break;
- case CartForm.ACTIONS.LinesRemove:
- result = await cart.removeLines(inputs.lineIds);
- break;
- case CartForm.ACTIONS.DiscountCodesUpdate: {
- const formDiscountCode = inputs.discountCode;
-
- // User inputted discount code
- const discountCodes = (
- formDiscountCode ? [formDiscountCode] : []
- ) as string[];
-
- // Combine discount codes already applied on cart
- discountCodes.push(...inputs.discountCodes);
-
- result = await cart.updateDiscountCodes(discountCodes);
- break;
- }
- case CartForm.ACTIONS.BuyerIdentityUpdate: {
- result = await cart.updateBuyerIdentity({
- ...inputs.buyerIdentity,
- });
- break;
- }
- default:
- throw new Error(`${action} cart action is not defined`);
- }
-
- const cartId = result.cart.id;
- const headers = cart.setCartId(result.cart.id);
- const {cart: cartResult, errors} = result;
-
- const redirectTo = formData.get('redirectTo') ?? null;
- if (typeof redirectTo === 'string') {
- status = 303;
- headers.set('Location', redirectTo);
- }
-
- return json(
- {
- cart: cartResult,
- errors,
- analytics: {
- cartId,
- },
- },
- {status, headers},
- );
-}
-
-export default function Cart() {
- const rootData = useRootLoaderData();
- const cartPromise = rootData.cart;
-
- return (
-
-
Cart
-
Loading cart ...}>
- An error occurred }
- >
- {(cart) => {
- return ;
- }}
-
-
-
- );
-}
diff --git a/examples/optimistic-cart-ui/app/routes/products.$handle.tsx b/examples/optimistic-cart-ui/app/routes/products.$handle.tsx
deleted file mode 100644
index b14d1813d4..0000000000
--- a/examples/optimistic-cart-ui/app/routes/products.$handle.tsx
+++ /dev/null
@@ -1,427 +0,0 @@
-import {Suspense} from 'react';
-import {defer, redirect, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
-import {
- Await,
- Link,
- useLoaderData,
- type MetaFunction,
- type FetcherWithComponents,
-} from '@remix-run/react';
-import type {
- ProductFragment,
- ProductVariantsQuery,
- ProductVariantFragment,
-} from 'storefrontapi.generated';
-
-import {
- Image,
- Money,
- VariantSelector,
- type VariantOption,
- getSelectedProductOptions,
- CartForm,
-} from '@shopify/hydrogen';
-import type {
- CartLineInput,
- SelectedOption,
-} from '@shopify/hydrogen/storefront-api-types';
-import {getVariantUrl} from '~/utils';
-
-export const meta: MetaFunction = ({data}) => {
- return [{title: `Hydrogen | ${data?.product.title ?? ''}`}];
-};
-
-export async function loader({params, request, context}: LoaderFunctionArgs) {
- const {handle} = params;
- const {storefront} = context;
-
- const selectedOptions = getSelectedProductOptions(request).filter(
- (option) =>
- // Filter out Shopify predictive search query params
- !option.name.startsWith('_sid') &&
- !option.name.startsWith('_pos') &&
- !option.name.startsWith('_psq') &&
- !option.name.startsWith('_ss') &&
- !option.name.startsWith('_v') &&
- // Filter out third party tracking params
- !option.name.startsWith('fbclid'),
- );
-
- if (!handle) {
- throw new Error('Expected product handle to be defined');
- }
-
- // await the query for the critical product data
- const {product} = await storefront.query(PRODUCT_QUERY, {
- variables: {handle, selectedOptions},
- });
-
- if (!product?.id) {
- throw new Response(null, {status: 404});
- }
-
- const firstVariant = product.variants.nodes[0];
- const firstVariantIsDefault = Boolean(
- firstVariant.selectedOptions.find(
- (option: SelectedOption) =>
- option.name === 'Title' && option.value === 'Default Title',
- ),
- );
-
- if (firstVariantIsDefault) {
- product.selectedVariant = firstVariant;
- } else {
- // if no selected variant was returned from the selected options,
- // we redirect to the first variant's url with it's selected options applied
- if (!product.selectedVariant) {
- throw redirectToFirstVariant({product, request});
- }
- }
-
- // In order to show which variants are available in the UI, we need to query
- // all of them. But there might be a *lot*, so instead separate the variants
- // into it's own separate query that is deferred. So there's a brief moment
- // where variant options might show as available when they're not, but after
- // this deffered query resolves, the UI will update.
- const variants = storefront.query(VARIANTS_QUERY, {
- variables: {handle},
- });
-
- return defer({product, variants});
-}
-
-function redirectToFirstVariant({
- product,
- request,
-}: {
- product: ProductFragment;
- request: Request;
-}) {
- const url = new URL(request.url);
- const firstVariant = product.variants.nodes[0];
-
- return redirect(
- getVariantUrl({
- pathname: url.pathname,
- handle: product.handle,
- selectedOptions: firstVariant.selectedOptions,
- searchParams: new URLSearchParams(url.search),
- }),
- {
- status: 302,
- },
- );
-}
-
-export default function Product() {
- const {product, variants} = useLoaderData();
- const {selectedVariant} = product;
- return (
-
- );
-}
-
-function ProductImage({image}: {image: ProductVariantFragment['image']}) {
- if (!image) {
- return
;
- }
- return (
-
-
-
- );
-}
-
-function ProductMain({
- selectedVariant,
- product,
- variants,
-}: {
- product: ProductFragment;
- selectedVariant: ProductFragment['selectedVariant'];
- variants: Promise;
-}) {
- const {title, descriptionHtml} = product;
- return (
-
-
{title}
-
-
-
- }
- >
-
- {(data) => (
-
- )}
-
-
-
-
-
- Description
-
-
-
-
-
- );
-}
-
-function ProductPrice({
- selectedVariant,
-}: {
- selectedVariant: ProductFragment['selectedVariant'];
-}) {
- return (
-
- {selectedVariant?.compareAtPrice ? (
- <>
-
Sale
-
-
- {selectedVariant ? : null}
-
-
-
-
- >
- ) : (
- selectedVariant?.price &&
- )}
-
- );
-}
-
-function ProductForm({
- product,
- selectedVariant,
- variants,
-}: {
- product: ProductFragment;
- selectedVariant: ProductFragment['selectedVariant'];
- variants: Array;
-}) {
- return (
-
-
- {({option}) => }
-
-
-
{
- window.location.href = window.location.href + '#cart-aside';
- }}
- lines={
- selectedVariant
- ? [
- {
- merchandiseId: selectedVariant.id,
- quantity: 1,
- },
- ]
- : []
- }
- >
- {selectedVariant?.availableForSale ? 'Add to cart' : 'Sold out'}
-
-
- );
-}
-
-function ProductOptions({option}: {option: VariantOption}) {
- return (
-
-
{option.name}
-
- {option.values.map(({value, isAvailable, isActive, to}) => {
- return (
-
- {value}
-
- );
- })}
-
-
-
- );
-}
-
-function AddToCartButton({
- analytics,
- children,
- disabled,
- lines,
- onClick,
-}: {
- analytics?: unknown;
- children: React.ReactNode;
- disabled?: boolean;
- lines: CartLineInput[];
- onClick?: () => void;
-}) {
- return (
-
- {(fetcher: FetcherWithComponents) => (
- <>
-
-
- {children}
-
- >
- )}
-
- );
-}
-
-const PRODUCT_VARIANT_FRAGMENT = `#graphql
- fragment ProductVariant on ProductVariant {
- availableForSale
- compareAtPrice {
- amount
- currencyCode
- }
- id
- image {
- __typename
- id
- url
- altText
- width
- height
- }
- price {
- amount
- currencyCode
- }
- product {
- title
- handle
- }
- selectedOptions {
- name
- value
- }
- sku
- title
- unitPrice {
- amount
- currencyCode
- }
- }
-` as const;
-
-const PRODUCT_FRAGMENT = `#graphql
- fragment Product on Product {
- id
- title
- vendor
- handle
- descriptionHtml
- description
- options {
- name
- values
- }
- selectedVariant: variantBySelectedOptions(selectedOptions: $selectedOptions) {
- ...ProductVariant
- }
- variants(first: 1) {
- nodes {
- ...ProductVariant
- }
- }
- seo {
- description
- title
- }
- }
- ${PRODUCT_VARIANT_FRAGMENT}
-` as const;
-
-const PRODUCT_QUERY = `#graphql
- query Product(
- $country: CountryCode
- $handle: String!
- $language: LanguageCode
- $selectedOptions: [SelectedOptionInput!]!
- ) @inContext(country: $country, language: $language) {
- product(handle: $handle) {
- ...Product
- }
- }
- ${PRODUCT_FRAGMENT}
-` as const;
-
-const PRODUCT_VARIANTS_FRAGMENT = `#graphql
- fragment ProductVariants on Product {
- variants(first: 250) {
- nodes {
- ...ProductVariant
- }
- }
- }
- ${PRODUCT_VARIANT_FRAGMENT}
-` as const;
-
-const VARIANTS_QUERY = `#graphql
- ${PRODUCT_VARIANTS_FRAGMENT}
- query ProductVariants(
- $country: CountryCode
- $language: LanguageCode
- $handle: String!
- ) @inContext(country: $country, language: $language) {
- product(handle: $handle) {
- ...ProductVariants
- }
- }
-` as const;
diff --git a/examples/optimistic-cart-ui/app/styles/app.css b/examples/optimistic-cart-ui/app/styles/app.css
deleted file mode 100644
index 6c943bd95e..0000000000
--- a/examples/optimistic-cart-ui/app/styles/app.css
+++ /dev/null
@@ -1,473 +0,0 @@
-:root {
- --aside-width: 400px;
- --cart-aside-summary-height-with-discount: 300px;
- --cart-aside-summary-height: 250px;
- --grid-item-width: 355px;
- --header-height: 64px;
- --color-dark: #000;
- --color-light: #fff;
-}
-
-img {
- border-radius: 4px;
-}
-
-/*
-* --------------------------------------------------
-* components/Aside
-* --------------------------------------------------
-*/
-aside {
- background: var(--color-light);
- box-shadow: 0 0 50px rgba(0, 0, 0, 0.3);
- height: 100vh;
- max-width: var(--aside-width);
- min-width: var(--aside-width);
- position: fixed;
- right: calc(-1 * var(--aside-width));
- top: 0;
- transition: transform 200ms ease-in-out;
-}
-
-aside header {
- align-items: center;
- border-bottom: 1px solid var(--color-dark);
- display: flex;
- height: var(--header-height);
- justify-content: space-between;
- padding: 0 20px;
-}
-
-aside header h3 {
- margin: 0;
-}
-
-aside header .close {
- font-weight: bold;
- opacity: 0.8;
- text-decoration: none;
- transition: all 200ms;
- width: 20px;
-}
-
-aside header .close:hover {
- opacity: 1;
-}
-
-aside header h2 {
- margin-bottom: 0.6rem;
- margin-top: 0;
-}
-
-aside main {
- margin: 1rem;
-}
-
-aside p {
- margin: 0 0 0.25rem;
-}
-
-aside p:last-child {
- margin: 0;
-}
-
-aside li {
- margin-bottom: 0.125rem;
-}
-
-.overlay {
- background: rgba(0, 0, 0, 0.2);
- bottom: 0;
- left: 0;
- opacity: 0;
- pointer-events: none;
- position: fixed;
- right: 0;
- top: 0;
- transition: opacity 400ms ease-in-out;
- transition: opacity 400ms;
- visibility: hidden;
- z-index: 10;
-}
-
-.overlay .close-outside {
- background: transparent;
- border: none;
- color: transparent;
- height: 100%;
- left: 0;
- position: absolute;
- top: 0;
- width: calc(100% - var(--aside-width));
-}
-
-.overlay .light {
- background: rgba(255, 255, 255, 0.5);
-}
-
-.overlay .cancel {
- cursor: default;
- height: 100%;
- position: absolute;
- width: 100%;
-}
-
-.overlay:target {
- opacity: 1;
- pointer-events: auto;
- visibility: visible;
-}
-/* reveal aside */
-.overlay:target aside {
- transform: translateX(calc(var(--aside-width) * -1));
-}
-
-/*
-* --------------------------------------------------
-* components/Header
-* --------------------------------------------------
-*/
-.header {
- align-items: center;
- background: #fff;
- display: flex;
- height: var(--header-height);
- padding: 0 1rem;
- position: sticky;
- top: 0;
- z-index: 1;
-}
-
-.header-menu-mobile-toggle {
- @media (min-width: 48em) {
- display: none;
- }
-}
-
-.header-menu-mobile {
- display: flex;
- flex-direction: column;
- grid-gap: 1rem;
-}
-
-.header-menu-desktop {
- display: none;
- grid-gap: 1rem;
- @media (min-width: 45em) {
- display: flex;
- grid-gap: 1rem;
- margin-left: 3rem;
- }
-}
-
-.header-menu-item {
- cursor: pointer;
-}
-
-.header-ctas {
- align-items: center;
- display: flex;
- grid-gap: 1rem;
- margin-left: auto;
-}
-
-/*
-* --------------------------------------------------
-* components/Footer
-* --------------------------------------------------
-*/
-.footer {
- background: var(--color-dark);
- margin-top: auto;
-}
-
-.footer-menu-missing {
- display: inline-block;
- margin: 1rem;
-}
-
-.footer-menu {
- align-items: center;
- display: flex;
- grid-gap: 1rem;
- padding: 1rem;
-}
-
-.footer-menu a {
- color: var(--color-light);
-}
-
-/*
-* --------------------------------------------------
-* components/Cart
-* --------------------------------------------------
-*/
-.cart-main {
- height: 100%;
- max-height: calc(100vh - var(--cart-aside-summary-height));
- overflow-y: auto;
- width: auto;
-}
-
-.cart-main.with-discount {
- max-height: calc(100vh - var(--cart-aside-summary-height-with-discount));
-}
-
-.cart-line {
- display: flex;
- padding: 0.75rem 0;
-}
-
-.cart-line img {
- height: 100%;
- display: block;
- margin-right: 0.75rem;
-}
-
-.cart-summary-page {
- position: relative;
-}
-
-.cart-summary-aside {
- background: white;
- border-top: 1px solid var(--color-dark);
- bottom: 0;
- padding-top: 0.75rem;
- position: absolute;
- width: calc(var(--aside-width) - 40px);
-}
-
-.cart-line-quantiy {
- display: flex;
-}
-
-.cart-discount {
- align-items: center;
- display: flex;
- margin-top: 0.25rem;
-}
-
-.cart-subtotal {
- align-items: center;
- display: flex;
-}
-/*
-* --------------------------------------------------
-* components/Search
-* --------------------------------------------------
-*/
-.predictive-search {
- height: calc(100vh - var(--header-height) - 40px);
- overflow-y: auto;
-}
-
-.predictive-search-form {
- background: var(--color-light);
- position: sticky;
- top: 0;
-}
-
-.predictive-search-result {
- margin-bottom: 2rem;
-}
-
-.predictive-search-result h5 {
- text-transform: uppercase;
-}
-
-.predictive-search-result-item {
- margin-bottom: 0.5rem;
-}
-
-.predictive-search-result-item a {
- align-items: center;
- display: flex;
-}
-
-.predictive-search-result-item a img {
- margin-right: 0.75rem;
- height: 100%;
-}
-
-.search-result {
- margin-bottom: 1.5rem;
-}
-
-.search-results-item {
- margin-bottom: 0.5rem;
-}
-
-/*
-* --------------------------------------------------
-* routes/__index
-* --------------------------------------------------
-*/
-.featured-collection {
- display: block;
- margin-bottom: 2rem;
- position: relative;
-}
-
-.featured-collection-image {
- aspect-ratio: 1 / 1;
- @media (min-width: 45em) {
- aspect-ratio: 16 / 9;
- }
-}
-
-.featured-collection img {
- height: auto;
- max-height: 100%;
- object-fit: cover;
-}
-
-.recommended-products-grid {
- display: grid;
- grid-gap: 1.5rem;
- grid-template-columns: repeat(2, 1fr);
- @media (min-width: 45em) {
- grid-template-columns: repeat(4, 1fr);
- }
-}
-
-.recommended-product img {
- height: auto;
-}
-
-/*
-* --------------------------------------------------
-* routes/collections._index.tsx
-* --------------------------------------------------
-*/
-.collections-grid {
- display: grid;
- grid-gap: 1.5rem;
- grid-template-columns: repeat(auto-fit, minmax(var(--grid-item-width), 1fr));
- margin-bottom: 2rem;
-}
-
-.collection-item img {
- height: auto;
-}
-
-/*
-* --------------------------------------------------
-* routes/collections.$handle.tsx
-* --------------------------------------------------
-*/
-.collection-description {
- margin-bottom: 1rem;
- max-width: 95%;
- @media (min-width: 45em) {
- max-width: 600px;
- }
-}
-
-.products-grid {
- display: grid;
- grid-gap: 1.5rem;
- grid-template-columns: repeat(auto-fit, minmax(var(--grid-item-width), 1fr));
- margin-bottom: 2rem;
-}
-
-.product-item img {
- height: auto;
- width: 100%;
-}
-
-/*
-* --------------------------------------------------
-* routes/products.$handle.tsx
-* --------------------------------------------------
-*/
-.product {
- display: grid;
- @media (min-width: 45em) {
- grid-template-columns: 1fr 1fr;
- grid-gap: 4rem;
- }
-}
-
-.product h1 {
- margin-top: 0;
-}
-
-.product-images {
- display: grid;
- grid-gap: 1rem;
-}
-
-.product-image img {
- height: auto;
- width: 100%;
-}
-
-.product-main {
- align-self: start;
- position: sticky;
- top: 6rem;
-}
-
-.product-price-on-sale {
- display: flex;
- grid-gap: 0.5rem;
-}
-
-.product-price-on-sale s {
- opacity: 0.5;
-}
-
-.product-options-grid {
- display: flex;
- flex-wrap: wrap;
- grid-gap: 0.75rem;
-}
-
-.product-options-item {
- padding: 0.25rem 0.5rem;
-}
-
-/*
-* --------------------------------------------------
-* routes/blog._index.tsx
-* --------------------------------------------------
-*/
-.blog-grid {
- display: grid;
- grid-gap: 1.5rem;
- grid-template-columns: repeat(auto-fit, minmax(var(--grid-item-width), 1fr));
- margin-bottom: 2rem;
-}
-
-.blog-article-image {
- aspect-ratio: 3/2;
- display: block;
-}
-
-.blog-article-image img {
- height: 100%;
-}
-
-/*
-* --------------------------------------------------
-* routes/blog.$articlehandle.tsx
-* --------------------------------------------------
-*/
-.article img {
- height: auto;
- width: 100%;
-}
-
-/*
-* --------------------------------------------------
-* routes/account
-* --------------------------------------------------
-*/
-.account-profile-marketing {
- display: flex;
- align-items: center;
-}
-
-.account-logout {
- display: inline-block;
-}
diff --git a/examples/optimistic-cart-ui/app/styles/reset.css b/examples/optimistic-cart-ui/app/styles/reset.css
deleted file mode 100644
index 451a9a58de..0000000000
--- a/examples/optimistic-cart-ui/app/styles/reset.css
+++ /dev/null
@@ -1,129 +0,0 @@
-body {
- font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
- Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
- margin: 0;
- padding: 0;
-}
-
-h1,
-h2,
-p {
- margin: 0;
- padding: 0;
-}
-
-h1 {
- font-size: 1.6rem;
- font-weight: 700;
- line-height: 1.4;
- margin-bottom: 2rem;
- margin-top: 2rem;
-}
-
-h2 {
- font-size: 1.2rem;
- font-weight: 700;
- line-height: 1.4;
- margin-bottom: 1rem;
-}
-
-h4 {
- margin-top: 0.5rem;
- margin-bottom: 0.5rem;
-}
-
-h5 {
- margin-bottom: 1rem;
- margin-top: 0.5rem;
-}
-
-p {
- font-size: 1rem;
- line-height: 1.4;
-}
-
-a {
- color: #000;
- text-decoration: none;
-}
-
-a:hover {
- text-decoration: underline;
- cursor: pointer;
-}
-
-hr {
- border-bottom: none;
- border-top: 1px solid #000;
- margin: 0;
-}
-
-pre {
- white-space: pre-wrap;
-}
-
-body {
- display: flex;
- flex-direction: column;
- min-height: 100vh;
-}
-
-body > main {
- margin: 0 1rem 1rem 1rem;
-}
-
-section {
- padding: 1rem 0;
- @media (min-width: 768px) {
- padding: 2rem 0;
- }
-}
-
-fieldset {
- display: flex;
- flex-direction: column;
- margin-bottom: 0.5rem;
- padding: 1rem;
-}
-
-form {
- max-width: 100%;
- @media (min-width: 768px) {
- max-width: 400px;
- }
-}
-
-input {
- border-radius: 4px;
- border: 1px solid #000;
- font-size: 1rem;
- margin-bottom: 0.5rem;
- margin-top: 0.25rem;
- padding: 0.5rem;
-}
-
-legend {
- font-weight: 600;
- margin-bottom: 0.5rem;
-}
-
-ul {
- list-style: none;
- margin: 0;
- padding: 0;
-}
-
-li {
- margin-bottom: 0.5rem;
-}
-
-dl {
- margin: 0.5rem 0;
-}
-
-code {
- background: #ddd;
- border-radius: 4px;
- font-family: monospace;
- padding: 0.25rem;
-}
diff --git a/examples/optimistic-cart-ui/app/utils.ts b/examples/optimistic-cart-ui/app/utils.ts
deleted file mode 100644
index ffea0a7306..0000000000
--- a/examples/optimistic-cart-ui/app/utils.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import {useLocation} from '@remix-run/react';
-import type {SelectedOption} from '@shopify/hydrogen/storefront-api-types';
-import {useMemo} from 'react';
-
-export function useVariantUrl(
- handle: string,
- selectedOptions: SelectedOption[],
-) {
- const {pathname} = useLocation();
-
- return useMemo(() => {
- return getVariantUrl({
- handle,
- pathname,
- searchParams: new URLSearchParams(),
- selectedOptions,
- });
- }, [handle, selectedOptions, pathname]);
-}
-
-export function getVariantUrl({
- handle,
- pathname,
- searchParams,
- selectedOptions,
-}: {
- handle: string;
- pathname: string;
- searchParams: URLSearchParams;
- selectedOptions: SelectedOption[];
-}) {
- const match = /(\/[a-zA-Z]{2}-[a-zA-Z]{2}\/)/g.exec(pathname);
- const isLocalePathname = match && match.length > 0;
-
- const path = isLocalePathname
- ? `${match![0]}products/${handle}`
- : `/products/${handle}`;
-
- selectedOptions.forEach((option) => {
- searchParams.set(option.name, option.value);
- });
-
- const searchString = searchParams.toString();
-
- return path + (searchString ? '?' + searchParams.toString() : '');
-}
diff --git a/examples/optimistic-cart-ui/package.json b/examples/optimistic-cart-ui/package.json
index 00bc25368f..b3f2848c29 100644
--- a/examples/optimistic-cart-ui/package.json
+++ b/examples/optimistic-cart-ui/package.json
@@ -1,44 +1,13 @@
{
"name": "example-optimistic-cart-ui",
"private": true,
- "sideEffects": false,
+ "prettier": "@shopify/prettier-config",
"scripts": {
- "build": "shopify hydrogen build",
- "dev": "shopify hydrogen dev --worker --codegen",
+ "build": "shopify hydrogen build --diff",
+ "dev": "shopify hydrogen dev --worker --codegen --diff",
"preview": "npm run build && shopify hydrogen preview --worker",
"lint": "eslint --no-error-on-unmatched-pattern --ext .js,.ts,.jsx,.tsx .",
"typecheck": "tsc --noEmit",
"codegen": "shopify hydrogen codegen"
- },
- "prettier": "@shopify/prettier-config",
- "dependencies": {
- "@remix-run/react": "2.1.0",
- "@remix-run/server-runtime": "2.1.0",
- "@shopify/cli": "3.52.0",
- "@shopify/cli-hydrogen": "^6.1.0",
- "@shopify/hydrogen": "~2023.10.3",
- "@shopify/remix-oxygen": "^2.0.2",
- "graphql": "^16.6.0",
- "graphql-tag": "^2.12.6",
- "isbot": "^3.6.6",
- "react": "^18.2.0",
- "react-dom": "^18.2.0"
- },
- "devDependencies": {
- "@remix-run/dev": "2.1.0",
- "@remix-run/eslint-config": "2.1.0",
- "@shopify/oxygen-workers-types": "^4.0.0",
- "@shopify/prettier-config": "^1.1.2",
- "@total-typescript/ts-reset": "^0.4.2",
- "@types/eslint": "^8.4.10",
- "@types/react": "^18.2.22",
- "@types/react-dom": "^18.2.7",
- "eslint": "^8.20.0",
- "eslint-plugin-hydrogen": "0.12.2",
- "prettier": "^2.8.4",
- "typescript": "^5.2.2"
- },
- "engines": {
- "node": ">=18.0.0"
}
}
diff --git a/examples/optimistic-cart-ui/public/favicon.svg b/examples/optimistic-cart-ui/public/favicon.svg
deleted file mode 100644
index f6c649733d..0000000000
--- a/examples/optimistic-cart-ui/public/favicon.svg
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-
-
diff --git a/examples/optimistic-cart-ui/remix.config.js b/examples/optimistic-cart-ui/remix.config.js
deleted file mode 100644
index bfb46bf566..0000000000
--- a/examples/optimistic-cart-ui/remix.config.js
+++ /dev/null
@@ -1,19 +0,0 @@
-/** @type {import('@remix-run/dev').AppConfig} */
-module.exports = {
- appDirectory: 'app',
- ignoredRouteFiles: ['**/.*'],
- watchPaths: ['./public', './.env'],
- server: './server.ts',
- /**
- * The following settings are required to deploy Hydrogen apps to Oxygen:
- */
- publicPath: (process.env.HYDROGEN_ASSET_BASE_URL ?? '/') + 'build/',
- assetsBuildDirectory: 'dist/client/build',
- serverBuildPath: 'dist/worker/index.js',
- serverMainFields: ['browser', 'module', 'main'],
- serverConditions: ['worker', process.env.NODE_ENV],
- serverDependenciesToBundle: 'all',
- serverModuleFormat: 'esm',
- serverPlatform: 'neutral',
- serverMinify: process.env.NODE_ENV === 'production',
-};
diff --git a/examples/optimistic-cart-ui/remix.env.d.ts b/examples/optimistic-cart-ui/remix.env.d.ts
deleted file mode 100644
index 33ae0120d2..0000000000
--- a/examples/optimistic-cart-ui/remix.env.d.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-///
-///
-///
-
-// Enhance TypeScript's built-in typings.
-import '@total-typescript/ts-reset';
-
-import type {Storefront, HydrogenCart} from '@shopify/hydrogen';
-import type {AppSession} from './server';
-
-declare global {
- /**
- * A global `process` object is only available during build to access NODE_ENV.
- */
- const process: {env: {NODE_ENV: 'production' | 'development'}};
-
- /**
- * Declare expected Env parameter in fetch handler.
- */
- interface 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;
- }
-}
-
-declare module '@shopify/remix-oxygen' {
- /**
- * Declare local additions to the Remix loader context.
- */
- export interface AppLoadContext {
- env: Env;
- cart: HydrogenCart;
- storefront: Storefront;
- session: AppSession;
- waitUntil: ExecutionContext['waitUntil'];
- }
-}
diff --git a/examples/optimistic-cart-ui/server.ts b/examples/optimistic-cart-ui/server.ts
deleted file mode 100644
index 50626559e6..0000000000
--- a/examples/optimistic-cart-ui/server.ts
+++ /dev/null
@@ -1,257 +0,0 @@
-// Virtual entry point for the app
-import * as remixBuild from '@remix-run/dev/server-build';
-import {
- cartGetIdDefault,
- cartSetIdDefault,
- createCartHandler,
- createStorefrontClient,
- storefrontRedirect,
- type HydrogenSession,
-} from '@shopify/hydrogen';
-import {
- createRequestHandler,
- getStorefrontHeaders,
- createCookieSessionStorage,
- type SessionStorage,
- type Session,
-} from '@shopify/remix-oxygen';
-
-/**
- * Export a fetch handler in module format.
- */
-export default {
- async fetch(
- request: Request,
- env: Env,
- executionContext: ExecutionContext,
- ): Promise {
- 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]),
- ]);
-
- /**
- * 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 cart handler that will be used to
- * create and update the cart in the session.
- */
- const cart = createCartHandler({
- storefront,
- 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.
- */
- const handleRequest = createRequestHandler({
- build: remixBuild,
- mode: process.env.NODE_ENV,
- getLoadContext: () => ({session, storefront, cart, env, waitUntil}),
- });
-
- const response = await handleRequest(request);
-
- 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 response;
- } catch (error) {
- // eslint-disable-next-line no-console
- console.error(error);
- return new Response('An unexpected error occurred', {status: 500});
- }
- },
-};
-
-/**
- * This is a custom session implementation for your Hydrogen shop.
- * Feel free to customize it to your needs, add helper methods, or
- * swap out the cookie-based implementation with something else!
- */
-export class AppSession implements HydrogenSession {
- #sessionStorage;
- #session;
-
- constructor(sessionStorage: SessionStorage, session: Session) {
- this.#sessionStorage = sessionStorage;
- this.#session = session;
- }
-
- static async init(request: Request, secrets: string[]) {
- const storage = createCookieSessionStorage({
- cookie: {
- name: 'session',
- httpOnly: true,
- path: '/',
- sameSite: 'lax',
- secrets,
- },
- });
-
- const session = await storage.getSession(request.headers.get('Cookie'));
-
- return new this(storage, session);
- }
-
- get has() {
- return this.#session.has;
- }
-
- get get() {
- return this.#session.get;
- }
-
- get flash() {
- return this.#session.flash;
- }
-
- get unset() {
- return this.#session.unset;
- }
-
- get set() {
- return this.#session.set;
- }
-
- destroy() {
- return this.#sessionStorage.destroySession(this.#session);
- }
-
- commit() {
- return this.#sessionStorage.commitSession(this.#session);
- }
-}
-
-// NOTE: https://shopify.dev/docs/api/storefront/latest/queries/cart
-const CART_QUERY_FRAGMENT = `#graphql
- fragment Money on MoneyV2 {
- currencyCode
- amount
- }
- fragment CartLine on CartLine {
- id
- quantity
- attributes {
- key
- value
- }
- cost {
- totalAmount {
- ...Money
- }
- amountPerQuantity {
- ...Money
- }
- compareAtAmountPerQuantity {
- ...Money
- }
- }
- merchandise {
- ... on ProductVariant {
- id
- availableForSale
- compareAtPrice {
- ...Money
- }
- price {
- ...Money
- }
- requiresShipping
- title
- image {
- id
- url
- altText
- width
- height
-
- }
- product {
- handle
- title
- id
- }
- selectedOptions {
- name
- value
- }
- }
- }
- }
- fragment CartApiQuery on Cart {
- id
- checkoutUrl
- totalQuantity
- buyerIdentity {
- countryCode
- customer {
- id
- email
- firstName
- lastName
- displayName
- }
- email
- phone
- }
- lines(first: $numCartLines) {
- nodes {
- ...CartLine
- }
- }
- cost {
- subtotalAmount {
- ...Money
- }
- totalAmount {
- ...Money
- }
- totalDutyAmount {
- ...Money
- }
- totalTaxAmount {
- ...Money
- }
- }
- note
- attributes {
- key
- value
- }
- discountCodes {
- code
- applicable
- }
- }
-` as const;
diff --git a/examples/optimistic-cart-ui/storefrontapi.generated.d.ts b/examples/optimistic-cart-ui/storefrontapi.generated.d.ts
deleted file mode 100644
index 9019a1065d..0000000000
--- a/examples/optimistic-cart-ui/storefrontapi.generated.d.ts
+++ /dev/null
@@ -1,386 +0,0 @@
-/* eslint-disable eslint-comments/disable-enable-pair */
-/* eslint-disable eslint-comments/no-unlimited-disable */
-/* eslint-disable */
-import * as StorefrontAPI from '@shopify/hydrogen/storefront-api-types';
-
-export type RecommendedProductFragment = Pick<
- StorefrontAPI.Product,
- 'id' | 'title' | 'handle'
-> & {
- priceRange: {
- minVariantPrice: Pick;
- };
- images: {
- nodes: Array<
- Pick
- >;
- };
-};
-
-export type RecommendedProductsQueryVariables = StorefrontAPI.Exact<{
- country?: StorefrontAPI.InputMaybe;
- language?: StorefrontAPI.InputMaybe;
-}>;
-
-export type RecommendedProductsQuery = {
- products: {
- nodes: Array<
- Pick & {
- priceRange: {
- minVariantPrice: Pick<
- StorefrontAPI.MoneyV2,
- 'amount' | 'currencyCode'
- >;
- };
- images: {
- nodes: Array<
- Pick<
- StorefrontAPI.Image,
- 'id' | 'url' | 'altText' | 'width' | 'height'
- >
- >;
- };
- }
- >;
- };
-};
-
-export type ProductVariantFragment = Pick<
- StorefrontAPI.ProductVariant,
- 'availableForSale' | 'id' | 'sku' | 'title'
-> & {
- compareAtPrice?: StorefrontAPI.Maybe<
- Pick
- >;
- image?: StorefrontAPI.Maybe<
- {__typename: 'Image'} & Pick<
- StorefrontAPI.Image,
- 'id' | 'url' | 'altText' | 'width' | 'height'
- >
- >;
- price: Pick;
- product: Pick;
- selectedOptions: Array>;
- unitPrice?: StorefrontAPI.Maybe<
- Pick
- >;
-};
-
-export type ProductFragment = Pick<
- StorefrontAPI.Product,
- 'id' | 'title' | 'vendor' | 'handle' | 'descriptionHtml' | 'description'
-> & {
- options: Array>;
- selectedVariant?: StorefrontAPI.Maybe<
- Pick<
- StorefrontAPI.ProductVariant,
- 'availableForSale' | 'id' | 'sku' | 'title'
- > & {
- compareAtPrice?: StorefrontAPI.Maybe<
- Pick
- >;
- image?: StorefrontAPI.Maybe<
- {__typename: 'Image'} & Pick<
- StorefrontAPI.Image,
- 'id' | 'url' | 'altText' | 'width' | 'height'
- >
- >;
- price: Pick;
- product: Pick;
- selectedOptions: Array<
- Pick
- >;
- unitPrice?: StorefrontAPI.Maybe<
- Pick
- >;
- }
- >;
- variants: {
- nodes: Array<
- Pick<
- StorefrontAPI.ProductVariant,
- 'availableForSale' | 'id' | 'sku' | 'title'
- > & {
- compareAtPrice?: StorefrontAPI.Maybe<
- Pick
- >;
- image?: StorefrontAPI.Maybe<
- {__typename: 'Image'} & Pick<
- StorefrontAPI.Image,
- 'id' | 'url' | 'altText' | 'width' | 'height'
- >
- >;
- price: Pick;
- product: Pick;
- selectedOptions: Array<
- Pick
- >;
- unitPrice?: StorefrontAPI.Maybe<
- Pick
- >;
- }
- >;
- };
- seo: Pick;
-};
-
-export type ProductQueryVariables = StorefrontAPI.Exact<{
- country?: StorefrontAPI.InputMaybe;
- handle: StorefrontAPI.Scalars['String']['input'];
- language?: StorefrontAPI.InputMaybe;
- selectedOptions:
- | Array
- | StorefrontAPI.SelectedOptionInput;
-}>;
-
-export type ProductQuery = {
- product?: StorefrontAPI.Maybe<
- Pick<
- StorefrontAPI.Product,
- 'id' | 'title' | 'vendor' | 'handle' | 'descriptionHtml' | 'description'
- > & {
- options: Array>;
- selectedVariant?: StorefrontAPI.Maybe<
- Pick<
- StorefrontAPI.ProductVariant,
- 'availableForSale' | 'id' | 'sku' | 'title'
- > & {
- compareAtPrice?: StorefrontAPI.Maybe<
- Pick
- >;
- image?: StorefrontAPI.Maybe<
- {__typename: 'Image'} & Pick<
- StorefrontAPI.Image,
- 'id' | 'url' | 'altText' | 'width' | 'height'
- >
- >;
- price: Pick;
- product: Pick;
- selectedOptions: Array<
- Pick
- >;
- unitPrice?: StorefrontAPI.Maybe<
- Pick
- >;
- }
- >;
- variants: {
- nodes: Array<
- Pick<
- StorefrontAPI.ProductVariant,
- 'availableForSale' | 'id' | 'sku' | 'title'
- > & {
- compareAtPrice?: StorefrontAPI.Maybe<
- Pick
- >;
- image?: StorefrontAPI.Maybe<
- {__typename: 'Image'} & Pick<
- StorefrontAPI.Image,
- 'id' | 'url' | 'altText' | 'width' | 'height'
- >
- >;
- price: Pick;
- product: Pick;
- selectedOptions: Array<
- Pick
- >;
- unitPrice?: StorefrontAPI.Maybe<
- Pick
- >;
- }
- >;
- };
- seo: Pick;
- }
- >;
-};
-
-export type ProductVariantsFragment = {
- variants: {
- nodes: Array<
- Pick<
- StorefrontAPI.ProductVariant,
- 'availableForSale' | 'id' | 'sku' | 'title'
- > & {
- compareAtPrice?: StorefrontAPI.Maybe<
- Pick
- >;
- image?: StorefrontAPI.Maybe<
- {__typename: 'Image'} & Pick<
- StorefrontAPI.Image,
- 'id' | 'url' | 'altText' | 'width' | 'height'
- >
- >;
- price: Pick;
- product: Pick;
- selectedOptions: Array<
- Pick
- >;
- unitPrice?: StorefrontAPI.Maybe<
- Pick
- >;
- }
- >;
- };
-};
-
-export type ProductVariantsQueryVariables = StorefrontAPI.Exact<{
- country?: StorefrontAPI.InputMaybe;
- language?: StorefrontAPI.InputMaybe;
- handle: StorefrontAPI.Scalars['String']['input'];
-}>;
-
-export type ProductVariantsQuery = {
- product?: StorefrontAPI.Maybe<{
- variants: {
- nodes: Array<
- Pick<
- StorefrontAPI.ProductVariant,
- 'availableForSale' | 'id' | 'sku' | 'title'
- > & {
- compareAtPrice?: StorefrontAPI.Maybe<
- Pick
- >;
- image?: StorefrontAPI.Maybe<
- {__typename: 'Image'} & Pick<
- StorefrontAPI.Image,
- 'id' | 'url' | 'altText' | 'width' | 'height'
- >
- >;
- price: Pick;
- product: Pick;
- selectedOptions: Array<
- Pick
- >;
- unitPrice?: StorefrontAPI.Maybe<
- Pick
- >;
- }
- >;
- };
- }>;
-};
-
-export type MoneyFragment = Pick<
- StorefrontAPI.MoneyV2,
- 'currencyCode' | 'amount'
->;
-
-export type CartLineFragment = Pick<
- StorefrontAPI.CartLine,
- 'id' | 'quantity'
-> & {
- attributes: Array>;
- cost: {
- totalAmount: Pick;
- amountPerQuantity: Pick;
- compareAtAmountPerQuantity?: StorefrontAPI.Maybe<
- Pick
- >;
- };
- merchandise: Pick<
- StorefrontAPI.ProductVariant,
- 'id' | 'availableForSale' | 'requiresShipping' | 'title'
- > & {
- compareAtPrice?: StorefrontAPI.Maybe<
- Pick
- >;
- price: Pick;
- image?: StorefrontAPI.Maybe<
- Pick
- >;
- product: Pick;
- selectedOptions: Array<
- Pick
- >;
- };
-};
-
-export type CartApiQueryFragment = Pick<
- StorefrontAPI.Cart,
- 'id' | 'checkoutUrl' | 'totalQuantity' | 'note'
-> & {
- buyerIdentity: Pick<
- StorefrontAPI.CartBuyerIdentity,
- 'countryCode' | 'email' | 'phone'
- > & {
- customer?: StorefrontAPI.Maybe<
- Pick<
- StorefrontAPI.Customer,
- 'id' | 'email' | 'firstName' | 'lastName' | 'displayName'
- >
- >;
- };
- lines: {
- nodes: Array<
- Pick & {
- attributes: Array>;
- cost: {
- totalAmount: Pick;
- amountPerQuantity: Pick<
- StorefrontAPI.MoneyV2,
- 'currencyCode' | 'amount'
- >;
- compareAtAmountPerQuantity?: StorefrontAPI.Maybe<
- Pick
- >;
- };
- merchandise: Pick<
- StorefrontAPI.ProductVariant,
- 'id' | 'availableForSale' | 'requiresShipping' | 'title'
- > & {
- compareAtPrice?: StorefrontAPI.Maybe<
- Pick
- >;
- price: Pick;
- image?: StorefrontAPI.Maybe<
- Pick<
- StorefrontAPI.Image,
- 'id' | 'url' | 'altText' | 'width' | 'height'
- >
- >;
- product: Pick;
- selectedOptions: Array<
- Pick
- >;
- };
- }
- >;
- };
- cost: {
- subtotalAmount: Pick;
- totalAmount: Pick;
- totalDutyAmount?: StorefrontAPI.Maybe<
- Pick
- >;
- totalTaxAmount?: StorefrontAPI.Maybe<
- Pick
- >;
- };
- attributes: Array>;
- discountCodes: Array<
- Pick
- >;
-};
-
-interface GeneratedQueryTypes {
- '#graphql\n fragment RecommendedProduct on Product {\n id\n title\n handle\n priceRange {\n minVariantPrice {\n amount\n currencyCode\n }\n }\n images(first: 1) {\n nodes {\n id\n url\n altText\n width\n height\n }\n }\n }\n query RecommendedProducts ($country: CountryCode, $language: LanguageCode)\n @inContext(country: $country, language: $language) {\n products(first: 4, sortKey: UPDATED_AT, reverse: true) {\n nodes {\n ...RecommendedProduct\n }\n }\n }\n': {
- return: RecommendedProductsQuery;
- variables: RecommendedProductsQueryVariables;
- };
- '#graphql\n query Product(\n $country: CountryCode\n $handle: String!\n $language: LanguageCode\n $selectedOptions: [SelectedOptionInput!]!\n ) @inContext(country: $country, language: $language) {\n product(handle: $handle) {\n ...Product\n }\n }\n #graphql\n fragment Product on Product {\n id\n title\n vendor\n handle\n descriptionHtml\n description\n options {\n name\n values\n }\n selectedVariant: variantBySelectedOptions(selectedOptions: $selectedOptions) {\n ...ProductVariant\n }\n variants(first: 1) {\n nodes {\n ...ProductVariant\n }\n }\n seo {\n description\n title\n }\n }\n #graphql\n fragment ProductVariant on ProductVariant {\n availableForSale\n compareAtPrice {\n amount\n currencyCode\n }\n id\n image {\n __typename\n id\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n product {\n title\n handle\n }\n selectedOptions {\n name\n value\n }\n sku\n title\n unitPrice {\n amount\n currencyCode\n }\n }\n\n\n': {
- return: ProductQuery;
- variables: ProductQueryVariables;
- };
- '#graphql\n #graphql\n fragment ProductVariants on Product {\n variants(first: 250) {\n nodes {\n ...ProductVariant\n }\n }\n }\n #graphql\n fragment ProductVariant on ProductVariant {\n availableForSale\n compareAtPrice {\n amount\n currencyCode\n }\n id\n image {\n __typename\n id\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n product {\n title\n handle\n }\n selectedOptions {\n name\n value\n }\n sku\n title\n unitPrice {\n amount\n currencyCode\n }\n }\n\n\n query ProductVariants(\n $country: CountryCode\n $language: LanguageCode\n $handle: String!\n ) @inContext(country: $country, language: $language) {\n product(handle: $handle) {\n ...ProductVariants\n }\n }\n': {
- return: ProductVariantsQuery;
- variables: ProductVariantsQueryVariables;
- };
-}
-
-interface GeneratedMutationTypes {}
-
-declare module '@shopify/hydrogen' {
- interface StorefrontQueries extends GeneratedQueryTypes {}
- interface StorefrontMutations extends GeneratedMutationTypes {}
-}
diff --git a/examples/optimistic-cart-ui/tsconfig.json b/examples/optimistic-cart-ui/tsconfig.json
index dcd7c7237a..110d781eea 100644
--- a/examples/optimistic-cart-ui/tsconfig.json
+++ b/examples/optimistic-cart-ui/tsconfig.json
@@ -1,23 +1,11 @@
{
+ "extends": "../../templates/skeleton/tsconfig.json",
"include": ["./**/*.d.ts", "./**/*.ts", "./**/*.tsx"],
"compilerOptions": {
- "lib": ["DOM", "DOM.Iterable", "ES2022"],
- "isolatedModules": true,
- "esModuleInterop": true,
- "jsx": "react-jsx",
- "moduleResolution": "Bundler",
- "resolveJsonModule": true,
- "module": "ES2022",
- "target": "ES2022",
- "strict": true,
- "allowJs": true,
- "forceConsistentCasingInFileNames": true,
- "skipLibCheck": true,
"baseUrl": ".",
- "types": ["@shopify/oxygen-workers-types"],
"paths": {
- "~/*": ["app/*"]
- },
- "noEmit": true
+ "*": ["./*", "../../templates/skeleton/*"],
+ "~/*": ["app/*", "../../templates/skeleton/app/*"]
+ }
}
}
diff --git a/examples/third-party-queries-caching/.eslintignore b/examples/third-party-queries-caching/.eslintignore
deleted file mode 100644
index a362bcaa13..0000000000
--- a/examples/third-party-queries-caching/.eslintignore
+++ /dev/null
@@ -1,5 +0,0 @@
-build
-node_modules
-bin
-*.d.ts
-dist
diff --git a/examples/third-party-queries-caching/.eslintrc.js b/examples/third-party-queries-caching/.eslintrc.js
deleted file mode 100644
index 57a969e3ad..0000000000
--- a/examples/third-party-queries-caching/.eslintrc.js
+++ /dev/null
@@ -1,18 +0,0 @@
-/**
- * @type {import("@types/eslint").Linter.BaseConfig}
- */
-module.exports = {
- extends: [
- '@remix-run/eslint-config',
- 'plugin:hydrogen/recommended',
- 'plugin:hydrogen/typescript',
- ],
- rules: {
- '@typescript-eslint/ban-ts-comment': 'off',
- '@typescript-eslint/naming-convention': 'off',
- 'hydrogen/prefer-image-component': 'off',
- 'no-useless-escape': 'off',
- '@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
- 'no-case-declarations': 'off',
- },
-};
diff --git a/examples/third-party-queries-caching/.gitignore b/examples/third-party-queries-caching/.gitignore
deleted file mode 100644
index 336224ba36..0000000000
--- a/examples/third-party-queries-caching/.gitignore
+++ /dev/null
@@ -1,8 +0,0 @@
-node_modules
-/.cache
-/build
-/dist
-/public/build
-/.mf
-.env
-.shopify
diff --git a/examples/third-party-queries-caching/.graphqlrc.yml b/examples/third-party-queries-caching/.graphqlrc.yml
deleted file mode 100644
index bd38d076bc..0000000000
--- a/examples/third-party-queries-caching/.graphqlrc.yml
+++ /dev/null
@@ -1 +0,0 @@
-schema: node_modules/@shopify/hydrogen-react/storefront.schema.json
diff --git a/examples/third-party-queries-caching/README.md b/examples/third-party-queries-caching/README.md
index 0eb1248c5a..fa2a207445 100644
--- a/examples/third-party-queries-caching/README.md
+++ b/examples/third-party-queries-caching/README.md
@@ -1,6 +1,6 @@
# Hydrogen example: Third-party Queries and Caching
-This folder contains shows how to leverage Oxygen's sub-request caching when querying
+This folder shows how to leverage Oxygen's sub-request caching when querying
third-party GraphQL API in Hydrogen. This example uses the public [Rick & Morty API](https://rickandmortyapi.com/documentation/#graphql)
@@ -146,5 +146,3 @@ export default function Homepage() {
);
}
```
-
-[View the complete remix.d.ts file](/app/routes/_index.tsx) to see these updates in context.
diff --git a/examples/third-party-queries-caching/app/entry.client.tsx b/examples/third-party-queries-caching/app/entry.client.tsx
deleted file mode 100644
index ba957c430e..0000000000
--- a/examples/third-party-queries-caching/app/entry.client.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import {RemixBrowser} from '@remix-run/react';
-import {startTransition, StrictMode} from 'react';
-import {hydrateRoot} from 'react-dom/client';
-
-startTransition(() => {
- hydrateRoot(
- document,
-
-
- ,
- );
-});
diff --git a/examples/third-party-queries-caching/app/entry.server.tsx b/examples/third-party-queries-caching/app/entry.server.tsx
deleted file mode 100644
index a645a41078..0000000000
--- a/examples/third-party-queries-caching/app/entry.server.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import type {EntryContext} from '@shopify/remix-oxygen';
-import {RemixServer} from '@remix-run/react';
-import isbot from 'isbot';
-import {renderToReadableStream} from 'react-dom/server';
-import {createContentSecurityPolicy} from '@shopify/hydrogen';
-
-export default async function handleRequest(
- request: Request,
- responseStatusCode: number,
- responseHeaders: Headers,
- remixContext: EntryContext,
-) {
- const {nonce, header, NonceProvider} = createContentSecurityPolicy();
-
- const body = await renderToReadableStream(
-
-
- ,
- {
- nonce,
- signal: request.signal,
- onError(error) {
- // eslint-disable-next-line no-console
- console.error(error);
- responseStatusCode = 500;
- },
- },
- );
-
- if (isbot(request.headers.get('user-agent'))) {
- await body.allReady;
- }
-
- responseHeaders.set('Content-Type', 'text/html');
- responseHeaders.set('Content-Security-Policy', header);
-
- return new Response(body, {
- headers: responseHeaders,
- status: responseStatusCode,
- });
-}
diff --git a/examples/third-party-queries-caching/app/root.tsx b/examples/third-party-queries-caching/app/root.tsx
deleted file mode 100644
index 45aa341acb..0000000000
--- a/examples/third-party-queries-caching/app/root.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import {useNonce} from '@shopify/hydrogen';
-import {
- Links,
- Meta,
- Outlet,
- Scripts,
- LiveReload,
- ScrollRestoration,
-} from '@remix-run/react';
-import favicon from '../public/favicon.svg';
-import resetStyles from './styles/reset.css';
-import appStyles from './styles/app.css';
-
-export function links() {
- return [
- {rel: 'stylesheet', href: resetStyles},
- {rel: 'stylesheet', href: appStyles},
- {
- rel: 'preconnect',
- href: 'https://cdn.shopify.com',
- },
- {
- rel: 'preconnect',
- href: 'https://shop.app',
- },
- {rel: 'icon', type: 'image/svg+xml', href: favicon},
- ];
-}
-
-export default function App() {
- const nonce = useNonce();
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/examples/third-party-queries-caching/app/styles/app.css b/examples/third-party-queries-caching/app/styles/app.css
deleted file mode 100644
index 6c943bd95e..0000000000
--- a/examples/third-party-queries-caching/app/styles/app.css
+++ /dev/null
@@ -1,473 +0,0 @@
-:root {
- --aside-width: 400px;
- --cart-aside-summary-height-with-discount: 300px;
- --cart-aside-summary-height: 250px;
- --grid-item-width: 355px;
- --header-height: 64px;
- --color-dark: #000;
- --color-light: #fff;
-}
-
-img {
- border-radius: 4px;
-}
-
-/*
-* --------------------------------------------------
-* components/Aside
-* --------------------------------------------------
-*/
-aside {
- background: var(--color-light);
- box-shadow: 0 0 50px rgba(0, 0, 0, 0.3);
- height: 100vh;
- max-width: var(--aside-width);
- min-width: var(--aside-width);
- position: fixed;
- right: calc(-1 * var(--aside-width));
- top: 0;
- transition: transform 200ms ease-in-out;
-}
-
-aside header {
- align-items: center;
- border-bottom: 1px solid var(--color-dark);
- display: flex;
- height: var(--header-height);
- justify-content: space-between;
- padding: 0 20px;
-}
-
-aside header h3 {
- margin: 0;
-}
-
-aside header .close {
- font-weight: bold;
- opacity: 0.8;
- text-decoration: none;
- transition: all 200ms;
- width: 20px;
-}
-
-aside header .close:hover {
- opacity: 1;
-}
-
-aside header h2 {
- margin-bottom: 0.6rem;
- margin-top: 0;
-}
-
-aside main {
- margin: 1rem;
-}
-
-aside p {
- margin: 0 0 0.25rem;
-}
-
-aside p:last-child {
- margin: 0;
-}
-
-aside li {
- margin-bottom: 0.125rem;
-}
-
-.overlay {
- background: rgba(0, 0, 0, 0.2);
- bottom: 0;
- left: 0;
- opacity: 0;
- pointer-events: none;
- position: fixed;
- right: 0;
- top: 0;
- transition: opacity 400ms ease-in-out;
- transition: opacity 400ms;
- visibility: hidden;
- z-index: 10;
-}
-
-.overlay .close-outside {
- background: transparent;
- border: none;
- color: transparent;
- height: 100%;
- left: 0;
- position: absolute;
- top: 0;
- width: calc(100% - var(--aside-width));
-}
-
-.overlay .light {
- background: rgba(255, 255, 255, 0.5);
-}
-
-.overlay .cancel {
- cursor: default;
- height: 100%;
- position: absolute;
- width: 100%;
-}
-
-.overlay:target {
- opacity: 1;
- pointer-events: auto;
- visibility: visible;
-}
-/* reveal aside */
-.overlay:target aside {
- transform: translateX(calc(var(--aside-width) * -1));
-}
-
-/*
-* --------------------------------------------------
-* components/Header
-* --------------------------------------------------
-*/
-.header {
- align-items: center;
- background: #fff;
- display: flex;
- height: var(--header-height);
- padding: 0 1rem;
- position: sticky;
- top: 0;
- z-index: 1;
-}
-
-.header-menu-mobile-toggle {
- @media (min-width: 48em) {
- display: none;
- }
-}
-
-.header-menu-mobile {
- display: flex;
- flex-direction: column;
- grid-gap: 1rem;
-}
-
-.header-menu-desktop {
- display: none;
- grid-gap: 1rem;
- @media (min-width: 45em) {
- display: flex;
- grid-gap: 1rem;
- margin-left: 3rem;
- }
-}
-
-.header-menu-item {
- cursor: pointer;
-}
-
-.header-ctas {
- align-items: center;
- display: flex;
- grid-gap: 1rem;
- margin-left: auto;
-}
-
-/*
-* --------------------------------------------------
-* components/Footer
-* --------------------------------------------------
-*/
-.footer {
- background: var(--color-dark);
- margin-top: auto;
-}
-
-.footer-menu-missing {
- display: inline-block;
- margin: 1rem;
-}
-
-.footer-menu {
- align-items: center;
- display: flex;
- grid-gap: 1rem;
- padding: 1rem;
-}
-
-.footer-menu a {
- color: var(--color-light);
-}
-
-/*
-* --------------------------------------------------
-* components/Cart
-* --------------------------------------------------
-*/
-.cart-main {
- height: 100%;
- max-height: calc(100vh - var(--cart-aside-summary-height));
- overflow-y: auto;
- width: auto;
-}
-
-.cart-main.with-discount {
- max-height: calc(100vh - var(--cart-aside-summary-height-with-discount));
-}
-
-.cart-line {
- display: flex;
- padding: 0.75rem 0;
-}
-
-.cart-line img {
- height: 100%;
- display: block;
- margin-right: 0.75rem;
-}
-
-.cart-summary-page {
- position: relative;
-}
-
-.cart-summary-aside {
- background: white;
- border-top: 1px solid var(--color-dark);
- bottom: 0;
- padding-top: 0.75rem;
- position: absolute;
- width: calc(var(--aside-width) - 40px);
-}
-
-.cart-line-quantiy {
- display: flex;
-}
-
-.cart-discount {
- align-items: center;
- display: flex;
- margin-top: 0.25rem;
-}
-
-.cart-subtotal {
- align-items: center;
- display: flex;
-}
-/*
-* --------------------------------------------------
-* components/Search
-* --------------------------------------------------
-*/
-.predictive-search {
- height: calc(100vh - var(--header-height) - 40px);
- overflow-y: auto;
-}
-
-.predictive-search-form {
- background: var(--color-light);
- position: sticky;
- top: 0;
-}
-
-.predictive-search-result {
- margin-bottom: 2rem;
-}
-
-.predictive-search-result h5 {
- text-transform: uppercase;
-}
-
-.predictive-search-result-item {
- margin-bottom: 0.5rem;
-}
-
-.predictive-search-result-item a {
- align-items: center;
- display: flex;
-}
-
-.predictive-search-result-item a img {
- margin-right: 0.75rem;
- height: 100%;
-}
-
-.search-result {
- margin-bottom: 1.5rem;
-}
-
-.search-results-item {
- margin-bottom: 0.5rem;
-}
-
-/*
-* --------------------------------------------------
-* routes/__index
-* --------------------------------------------------
-*/
-.featured-collection {
- display: block;
- margin-bottom: 2rem;
- position: relative;
-}
-
-.featured-collection-image {
- aspect-ratio: 1 / 1;
- @media (min-width: 45em) {
- aspect-ratio: 16 / 9;
- }
-}
-
-.featured-collection img {
- height: auto;
- max-height: 100%;
- object-fit: cover;
-}
-
-.recommended-products-grid {
- display: grid;
- grid-gap: 1.5rem;
- grid-template-columns: repeat(2, 1fr);
- @media (min-width: 45em) {
- grid-template-columns: repeat(4, 1fr);
- }
-}
-
-.recommended-product img {
- height: auto;
-}
-
-/*
-* --------------------------------------------------
-* routes/collections._index.tsx
-* --------------------------------------------------
-*/
-.collections-grid {
- display: grid;
- grid-gap: 1.5rem;
- grid-template-columns: repeat(auto-fit, minmax(var(--grid-item-width), 1fr));
- margin-bottom: 2rem;
-}
-
-.collection-item img {
- height: auto;
-}
-
-/*
-* --------------------------------------------------
-* routes/collections.$handle.tsx
-* --------------------------------------------------
-*/
-.collection-description {
- margin-bottom: 1rem;
- max-width: 95%;
- @media (min-width: 45em) {
- max-width: 600px;
- }
-}
-
-.products-grid {
- display: grid;
- grid-gap: 1.5rem;
- grid-template-columns: repeat(auto-fit, minmax(var(--grid-item-width), 1fr));
- margin-bottom: 2rem;
-}
-
-.product-item img {
- height: auto;
- width: 100%;
-}
-
-/*
-* --------------------------------------------------
-* routes/products.$handle.tsx
-* --------------------------------------------------
-*/
-.product {
- display: grid;
- @media (min-width: 45em) {
- grid-template-columns: 1fr 1fr;
- grid-gap: 4rem;
- }
-}
-
-.product h1 {
- margin-top: 0;
-}
-
-.product-images {
- display: grid;
- grid-gap: 1rem;
-}
-
-.product-image img {
- height: auto;
- width: 100%;
-}
-
-.product-main {
- align-self: start;
- position: sticky;
- top: 6rem;
-}
-
-.product-price-on-sale {
- display: flex;
- grid-gap: 0.5rem;
-}
-
-.product-price-on-sale s {
- opacity: 0.5;
-}
-
-.product-options-grid {
- display: flex;
- flex-wrap: wrap;
- grid-gap: 0.75rem;
-}
-
-.product-options-item {
- padding: 0.25rem 0.5rem;
-}
-
-/*
-* --------------------------------------------------
-* routes/blog._index.tsx
-* --------------------------------------------------
-*/
-.blog-grid {
- display: grid;
- grid-gap: 1.5rem;
- grid-template-columns: repeat(auto-fit, minmax(var(--grid-item-width), 1fr));
- margin-bottom: 2rem;
-}
-
-.blog-article-image {
- aspect-ratio: 3/2;
- display: block;
-}
-
-.blog-article-image img {
- height: 100%;
-}
-
-/*
-* --------------------------------------------------
-* routes/blog.$articlehandle.tsx
-* --------------------------------------------------
-*/
-.article img {
- height: auto;
- width: 100%;
-}
-
-/*
-* --------------------------------------------------
-* routes/account
-* --------------------------------------------------
-*/
-.account-profile-marketing {
- display: flex;
- align-items: center;
-}
-
-.account-logout {
- display: inline-block;
-}
diff --git a/examples/third-party-queries-caching/app/styles/reset.css b/examples/third-party-queries-caching/app/styles/reset.css
deleted file mode 100644
index 451a9a58de..0000000000
--- a/examples/third-party-queries-caching/app/styles/reset.css
+++ /dev/null
@@ -1,129 +0,0 @@
-body {
- font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
- Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
- margin: 0;
- padding: 0;
-}
-
-h1,
-h2,
-p {
- margin: 0;
- padding: 0;
-}
-
-h1 {
- font-size: 1.6rem;
- font-weight: 700;
- line-height: 1.4;
- margin-bottom: 2rem;
- margin-top: 2rem;
-}
-
-h2 {
- font-size: 1.2rem;
- font-weight: 700;
- line-height: 1.4;
- margin-bottom: 1rem;
-}
-
-h4 {
- margin-top: 0.5rem;
- margin-bottom: 0.5rem;
-}
-
-h5 {
- margin-bottom: 1rem;
- margin-top: 0.5rem;
-}
-
-p {
- font-size: 1rem;
- line-height: 1.4;
-}
-
-a {
- color: #000;
- text-decoration: none;
-}
-
-a:hover {
- text-decoration: underline;
- cursor: pointer;
-}
-
-hr {
- border-bottom: none;
- border-top: 1px solid #000;
- margin: 0;
-}
-
-pre {
- white-space: pre-wrap;
-}
-
-body {
- display: flex;
- flex-direction: column;
- min-height: 100vh;
-}
-
-body > main {
- margin: 0 1rem 1rem 1rem;
-}
-
-section {
- padding: 1rem 0;
- @media (min-width: 768px) {
- padding: 2rem 0;
- }
-}
-
-fieldset {
- display: flex;
- flex-direction: column;
- margin-bottom: 0.5rem;
- padding: 1rem;
-}
-
-form {
- max-width: 100%;
- @media (min-width: 768px) {
- max-width: 400px;
- }
-}
-
-input {
- border-radius: 4px;
- border: 1px solid #000;
- font-size: 1rem;
- margin-bottom: 0.5rem;
- margin-top: 0.25rem;
- padding: 0.5rem;
-}
-
-legend {
- font-weight: 600;
- margin-bottom: 0.5rem;
-}
-
-ul {
- list-style: none;
- margin: 0;
- padding: 0;
-}
-
-li {
- margin-bottom: 0.5rem;
-}
-
-dl {
- margin: 0.5rem 0;
-}
-
-code {
- background: #ddd;
- border-radius: 4px;
- font-family: monospace;
- padding: 0.25rem;
-}
diff --git a/examples/third-party-queries-caching/package.json b/examples/third-party-queries-caching/package.json
index ab81f218b2..88ba5658d6 100644
--- a/examples/third-party-queries-caching/package.json
+++ b/examples/third-party-queries-caching/package.json
@@ -1,43 +1,13 @@
{
"name": "example-third-party-queries-caching",
"private": true,
- "sideEffects": false,
+ "prettier": "@shopify/prettier-config",
"scripts": {
- "build": "shopify hydrogen build",
- "dev": "shopify hydrogen dev --worker --codegen",
+ "build": "shopify hydrogen build --diff",
+ "dev": "shopify hydrogen dev --worker --codegen --diff",
"preview": "npm run build && shopify hydrogen preview --worker",
"lint": "eslint --no-error-on-unmatched-pattern --ext .js,.ts,.jsx,.tsx .",
"typecheck": "tsc --noEmit",
"codegen": "shopify hydrogen codegen"
- },
- "prettier": "@shopify/prettier-config",
- "dependencies": {
- "@remix-run/react": "2.1.0",
- "@shopify/cli": "^3.51.0",
- "@shopify/cli-hydrogen": "^6.1.0",
- "@shopify/hydrogen": "^2023.10.3",
- "@shopify/remix-oxygen": "^2.0.2",
- "graphql": "^16.6.0",
- "graphql-tag": "^2.12.6",
- "isbot": "^3.6.6",
- "react": "^18.2.0",
- "react-dom": "^18.2.0"
- },
- "devDependencies": {
- "@remix-run/dev": "2.1.0",
- "@remix-run/eslint-config": "2.1.0",
- "@shopify/oxygen-workers-types": "^4.0.0",
- "@shopify/prettier-config": "^1.1.2",
- "@total-typescript/ts-reset": "^0.4.2",
- "@types/eslint": "^8.4.10",
- "@types/react": "^18.2.22",
- "@types/react-dom": "^18.2.7",
- "eslint": "^8.20.0",
- "eslint-plugin-hydrogen": "0.12.2",
- "prettier": "^2.8.4",
- "typescript": "^5.2.2"
- },
- "engines": {
- "node": ">=18"
}
}
diff --git a/examples/third-party-queries-caching/public/favicon.svg b/examples/third-party-queries-caching/public/favicon.svg
deleted file mode 100644
index f6c649733d..0000000000
--- a/examples/third-party-queries-caching/public/favicon.svg
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-
-
diff --git a/examples/third-party-queries-caching/remix.config.js b/examples/third-party-queries-caching/remix.config.js
deleted file mode 100644
index bfb46bf566..0000000000
--- a/examples/third-party-queries-caching/remix.config.js
+++ /dev/null
@@ -1,19 +0,0 @@
-/** @type {import('@remix-run/dev').AppConfig} */
-module.exports = {
- appDirectory: 'app',
- ignoredRouteFiles: ['**/.*'],
- watchPaths: ['./public', './.env'],
- server: './server.ts',
- /**
- * The following settings are required to deploy Hydrogen apps to Oxygen:
- */
- publicPath: (process.env.HYDROGEN_ASSET_BASE_URL ?? '/') + 'build/',
- assetsBuildDirectory: 'dist/client/build',
- serverBuildPath: 'dist/worker/index.js',
- serverMainFields: ['browser', 'module', 'main'],
- serverConditions: ['worker', process.env.NODE_ENV],
- serverDependenciesToBundle: 'all',
- serverModuleFormat: 'esm',
- serverPlatform: 'neutral',
- serverMinify: process.env.NODE_ENV === 'production',
-};
diff --git a/examples/third-party-queries-caching/storefrontapi.generated.d.ts b/examples/third-party-queries-caching/storefrontapi.generated.d.ts
deleted file mode 100644
index be71567b4e..0000000000
--- a/examples/third-party-queries-caching/storefrontapi.generated.d.ts
+++ /dev/null
@@ -1,115 +0,0 @@
-/* eslint-disable eslint-comments/disable-enable-pair */
-/* eslint-disable eslint-comments/no-unlimited-disable */
-/* eslint-disable */
-import * as StorefrontAPI from '@shopify/hydrogen/storefront-api-types';
-
-export type MoneyFragment = Pick<
- StorefrontAPI.MoneyV2,
- 'currencyCode' | 'amount'
->;
-
-export type CartLineFragment = Pick<
- StorefrontAPI.CartLine,
- 'id' | 'quantity'
-> & {
- attributes: Array>;
- cost: {
- totalAmount: Pick;
- amountPerQuantity: Pick;
- compareAtAmountPerQuantity?: StorefrontAPI.Maybe<
- Pick
- >;
- };
- merchandise: Pick<
- StorefrontAPI.ProductVariant,
- 'id' | 'availableForSale' | 'requiresShipping' | 'title'
- > & {
- compareAtPrice?: StorefrontAPI.Maybe<
- Pick
- >;
- price: Pick;
- image?: StorefrontAPI.Maybe<
- Pick
- >;
- product: Pick;
- selectedOptions: Array<
- Pick
- >;
- };
-};
-
-export type CartApiQueryFragment = Pick<
- StorefrontAPI.Cart,
- 'id' | 'checkoutUrl' | 'totalQuantity' | 'note'
-> & {
- buyerIdentity: Pick<
- StorefrontAPI.CartBuyerIdentity,
- 'countryCode' | 'email' | 'phone'
- > & {
- customer?: StorefrontAPI.Maybe<
- Pick<
- StorefrontAPI.Customer,
- 'id' | 'email' | 'firstName' | 'lastName' | 'displayName'
- >
- >;
- };
- lines: {
- nodes: Array<
- Pick & {
- attributes: Array>;
- cost: {
- totalAmount: Pick;
- amountPerQuantity: Pick<
- StorefrontAPI.MoneyV2,
- 'currencyCode' | 'amount'
- >;
- compareAtAmountPerQuantity?: StorefrontAPI.Maybe<
- Pick
- >;
- };
- merchandise: Pick<
- StorefrontAPI.ProductVariant,
- 'id' | 'availableForSale' | 'requiresShipping' | 'title'
- > & {
- compareAtPrice?: StorefrontAPI.Maybe<
- Pick
- >;
- price: Pick;
- image?: StorefrontAPI.Maybe<
- Pick<
- StorefrontAPI.Image,
- 'id' | 'url' | 'altText' | 'width' | 'height'
- >
- >;
- product: Pick;
- selectedOptions: Array<
- Pick
- >;
- };
- }
- >;
- };
- cost: {
- subtotalAmount: Pick;
- totalAmount: Pick;
- totalDutyAmount?: StorefrontAPI.Maybe<
- Pick
- >;
- totalTaxAmount?: StorefrontAPI.Maybe<
- Pick
- >;
- };
- attributes: Array>;
- discountCodes: Array<
- Pick
- >;
-};
-
-interface GeneratedQueryTypes {}
-
-interface GeneratedMutationTypes {}
-
-declare module '@shopify/hydrogen' {
- interface StorefrontQueries extends GeneratedQueryTypes {}
- interface StorefrontMutations extends GeneratedMutationTypes {}
-}
diff --git a/examples/third-party-queries-caching/tsconfig.json b/examples/third-party-queries-caching/tsconfig.json
index dcd7c7237a..110d781eea 100644
--- a/examples/third-party-queries-caching/tsconfig.json
+++ b/examples/third-party-queries-caching/tsconfig.json
@@ -1,23 +1,11 @@
{
+ "extends": "../../templates/skeleton/tsconfig.json",
"include": ["./**/*.d.ts", "./**/*.ts", "./**/*.tsx"],
"compilerOptions": {
- "lib": ["DOM", "DOM.Iterable", "ES2022"],
- "isolatedModules": true,
- "esModuleInterop": true,
- "jsx": "react-jsx",
- "moduleResolution": "Bundler",
- "resolveJsonModule": true,
- "module": "ES2022",
- "target": "ES2022",
- "strict": true,
- "allowJs": true,
- "forceConsistentCasingInFileNames": true,
- "skipLibCheck": true,
"baseUrl": ".",
- "types": ["@shopify/oxygen-workers-types"],
"paths": {
- "~/*": ["app/*"]
- },
- "noEmit": true
+ "*": ["./*", "../../templates/skeleton/*"],
+ "~/*": ["app/*", "../../templates/skeleton/app/*"]
+ }
}
}
diff --git a/package-lock.json b/package-lock.json
index 0b1d8ca43d..bdab190883 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -77,7 +77,6 @@
},
"examples/customer-api": {
"name": "example-customer-api",
- "version": "0.0.2",
"dependencies": {
"@remix-run/react": "2.1.0",
"@remix-run/server-runtime": "2.1.0",
@@ -107,6 +106,39 @@
"node": ">=18.0.0"
}
},
+ "examples/diff": {
+ "version": "1.0.0",
+ "extraneous": true,
+ "dependencies": {
+ "@remix-run/react": "2.1.0",
+ "@shopify/cli": "3.50.0",
+ "@shopify/cli-hydrogen": "^6.0.0",
+ "@shopify/hydrogen": "^2023.10.0",
+ "@shopify/remix-oxygen": "^2.0.0",
+ "graphql": "^16.6.0",
+ "graphql-tag": "^2.12.6",
+ "isbot": "^3.6.6",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0"
+ },
+ "devDependencies": {
+ "@remix-run/dev": "2.1.0",
+ "@remix-run/eslint-config": "2.1.0",
+ "@shopify/oxygen-workers-types": "^3.17.3",
+ "@shopify/prettier-config": "^1.1.2",
+ "@total-typescript/ts-reset": "^0.4.2",
+ "@types/eslint": "^8.4.10",
+ "@types/react": "^18.2.22",
+ "@types/react-dom": "^18.2.7",
+ "eslint": "^8.20.0",
+ "eslint-plugin-hydrogen": "0.12.2",
+ "prettier": "^2.8.4",
+ "typescript": "^5.2.2"
+ },
+ "engines": {
+ "node": ">=16.13"
+ }
+ },
"examples/express": {
"name": "example-hydrogen-express",
"dependencies": {
@@ -146,7 +178,6 @@
},
"examples/multipass": {
"name": "example-multipass",
- "version": "1.0.1",
"dependencies": {
"@remix-run/react": "2.1.0",
"@shopify/cli": "3.50.0",
@@ -552,42 +583,10 @@
}
},
"examples/optimistic-cart-ui": {
- "name": "example-optimistic-cart-ui",
- "version": "1.0.1",
- "dependencies": {
- "@remix-run/react": "2.1.0",
- "@remix-run/server-runtime": "2.1.0",
- "@shopify/cli": "3.52.0",
- "@shopify/cli-hydrogen": "^6.1.0",
- "@shopify/hydrogen": "~2023.10.3",
- "@shopify/remix-oxygen": "^2.0.2",
- "graphql": "^16.6.0",
- "graphql-tag": "^2.12.6",
- "isbot": "^3.6.6",
- "react": "^18.2.0",
- "react-dom": "^18.2.0"
- },
- "devDependencies": {
- "@remix-run/dev": "2.1.0",
- "@remix-run/eslint-config": "2.1.0",
- "@shopify/oxygen-workers-types": "^4.0.0",
- "@shopify/prettier-config": "^1.1.2",
- "@total-typescript/ts-reset": "^0.4.2",
- "@types/eslint": "^8.4.10",
- "@types/react": "^18.2.22",
- "@types/react-dom": "^18.2.7",
- "eslint": "^8.20.0",
- "eslint-plugin-hydrogen": "0.12.2",
- "prettier": "^2.8.4",
- "typescript": "^5.2.2"
- },
- "engines": {
- "node": ">=18.0.0"
- }
+ "name": "example-optimistic-cart-ui"
},
"examples/subscriptions": {
"name": "example-subscriptions",
- "version": "1.0.0",
"dependencies": {
"@remix-run/react": "2.1.0",
"@shopify/cli": "^3.51.0",
@@ -625,37 +624,7 @@
}
},
"examples/third-party-queries-caching": {
- "name": "example-third-party-queries-caching",
- "version": "1.0.0",
- "dependencies": {
- "@remix-run/react": "2.1.0",
- "@shopify/cli": "^3.51.0",
- "@shopify/cli-hydrogen": "^6.1.0",
- "@shopify/hydrogen": "^2023.10.3",
- "@shopify/remix-oxygen": "^2.0.2",
- "graphql": "^16.6.0",
- "graphql-tag": "^2.12.6",
- "isbot": "^3.6.6",
- "react": "^18.2.0",
- "react-dom": "^18.2.0"
- },
- "devDependencies": {
- "@remix-run/dev": "2.1.0",
- "@remix-run/eslint-config": "2.1.0",
- "@shopify/oxygen-workers-types": "^4.0.0",
- "@shopify/prettier-config": "^1.1.2",
- "@total-typescript/ts-reset": "^0.4.2",
- "@types/eslint": "^8.4.10",
- "@types/react": "^18.2.22",
- "@types/react-dom": "^18.2.7",
- "eslint": "^8.20.0",
- "eslint-plugin-hydrogen": "0.12.2",
- "prettier": "^2.8.4",
- "typescript": "^5.2.2"
- },
- "engines": {
- "node": ">=18"
- }
+ "name": "example-third-party-queries-caching"
},
"node_modules/@aashutoshrathi/word-wrap": {
"version": "1.2.6",
@@ -13213,13 +13182,6 @@
"dev": true,
"license": "Apache-2.0"
},
- "node_modules/diff": {
- "version": "5.1.0",
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">=0.3.1"
- }
- },
"node_modules/diff-sequences": {
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz",
@@ -29944,6 +29906,15 @@
"node": ">=8"
}
},
+ "node_modules/uvu/node_modules/diff": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz",
+ "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==",
+ "devOptional": true,
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
"node_modules/v8-compile-cache-lib": {
"version": "3.0.1",
"license": "MIT"
@@ -31383,6 +31354,7 @@
"source-map": "^0.7.4",
"stack-trace": "^1.0.0-pre2",
"tar-fs": "^2.1.1",
+ "tempy": "^3.0.0",
"ts-morph": "20.0.0",
"use-resize-observer": "^9.1.0",
"ws": "^8.13.0"
@@ -31482,6 +31454,14 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "packages/cli/node_modules/diff": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz",
+ "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==",
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
"packages/cli/node_modules/emoji-regex": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz",
@@ -37742,6 +37722,7 @@
"source-map": "^0.7.4",
"stack-trace": "^1.0.0-pre2",
"tar-fs": "^2.1.1",
+ "tempy": "^3.0.0",
"ts-morph": "20.0.0",
"type-fest": "^4.5.0",
"use-resize-observer": "^9.1.0",
@@ -37781,6 +37762,11 @@
"string-width": "^7.0.0"
}
},
+ "diff": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz",
+ "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw=="
+ },
"emoji-regex": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz",
@@ -41699,9 +41685,6 @@
"version": "1.2.2",
"dev": true
},
- "diff": {
- "version": "5.1.0"
- },
"diff-sequences": {
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz",
@@ -43117,32 +43100,7 @@
}
},
"example-optimistic-cart-ui": {
- "version": "file:examples/optimistic-cart-ui",
- "requires": {
- "@remix-run/dev": "2.1.0",
- "@remix-run/eslint-config": "2.1.0",
- "@remix-run/react": "2.1.0",
- "@remix-run/server-runtime": "2.1.0",
- "@shopify/cli": "3.52.0",
- "@shopify/cli-hydrogen": "^6.1.0",
- "@shopify/hydrogen": "~2023.10.3",
- "@shopify/oxygen-workers-types": "^4.0.0",
- "@shopify/prettier-config": "^1.1.2",
- "@shopify/remix-oxygen": "^2.0.2",
- "@total-typescript/ts-reset": "^0.4.2",
- "@types/eslint": "^8.4.10",
- "@types/react": "^18.2.22",
- "@types/react-dom": "^18.2.7",
- "eslint": "^8.20.0",
- "eslint-plugin-hydrogen": "0.12.2",
- "graphql": "^16.6.0",
- "graphql-tag": "^2.12.6",
- "isbot": "^3.6.6",
- "prettier": "^2.8.4",
- "react": "^18.2.0",
- "react-dom": "^18.2.0",
- "typescript": "^5.2.2"
- }
+ "version": "file:examples/optimistic-cart-ui"
},
"example-subscriptions": {
"version": "file:examples/subscriptions",
@@ -43178,31 +43136,7 @@
}
},
"example-third-party-queries-caching": {
- "version": "file:examples/third-party-queries-caching",
- "requires": {
- "@remix-run/dev": "2.1.0",
- "@remix-run/eslint-config": "2.1.0",
- "@remix-run/react": "2.1.0",
- "@shopify/cli": "^3.51.0",
- "@shopify/cli-hydrogen": "^6.1.0",
- "@shopify/hydrogen": "^2023.10.3",
- "@shopify/oxygen-workers-types": "^4.0.0",
- "@shopify/prettier-config": "^1.1.2",
- "@shopify/remix-oxygen": "^2.0.2",
- "@total-typescript/ts-reset": "^0.4.2",
- "@types/eslint": "^8.4.10",
- "@types/react": "^18.2.22",
- "@types/react-dom": "^18.2.7",
- "eslint": "^8.20.0",
- "eslint-plugin-hydrogen": "0.12.2",
- "graphql": "^16.6.0",
- "graphql-tag": "^2.12.6",
- "isbot": "^3.6.6",
- "prettier": "^2.8.4",
- "react": "^18.2.0",
- "react-dom": "^18.2.0",
- "typescript": "^5.2.2"
- }
+ "version": "file:examples/third-party-queries-caching"
},
"execa": {
"version": "7.2.0",
@@ -52849,6 +52783,14 @@
"diff": "^5.0.0",
"kleur": "^4.0.3",
"sade": "^1.7.3"
+ },
+ "dependencies": {
+ "diff": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz",
+ "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==",
+ "devOptional": true
+ }
}
},
"v8-compile-cache-lib": {
diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json
index acdae4fd0e..d171d133a5 100644
--- a/packages/cli/oclif.manifest.json
+++ b/packages/cli/oclif.manifest.json
@@ -60,6 +60,13 @@
"codegen"
]
},
+ "diff": {
+ "name": "diff",
+ "type": "boolean",
+ "description": "Applies the current files on top of Hydrogen's starter template in a temporary directory.",
+ "required": false,
+ "allowNo": false
+ },
"base": {
"name": "base",
"type": "option",
@@ -344,6 +351,13 @@
"description": "Skip the version check when running `hydrogen dev`",
"required": false,
"allowNo": false
+ },
+ "diff": {
+ "name": "diff",
+ "type": "boolean",
+ "description": "Applies the current files on top of Hydrogen's starter template in a temporary directory.",
+ "required": false,
+ "allowNo": false
}
},
"args": {}
diff --git a/packages/cli/package.json b/packages/cli/package.json
index 3a4e2f08cc..ac64052d3b 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -52,6 +52,7 @@
"source-map": "^0.7.4",
"stack-trace": "^1.0.0-pre2",
"tar-fs": "^2.1.1",
+ "tempy": "^3.0.0",
"ts-morph": "20.0.0",
"use-resize-observer": "^9.1.0",
"ws": "^8.13.0"
diff --git a/packages/cli/src/commands/hydrogen/build.ts b/packages/cli/src/commands/hydrogen/build.ts
index 2482b7aba9..aaf9b7ac12 100644
--- a/packages/cli/src/commands/hydrogen/build.ts
+++ b/packages/cli/src/commands/hydrogen/build.ts
@@ -38,6 +38,7 @@ import {
} from '../../lib/bundle/analyzer.js';
import {AbortError} from '@shopify/cli-kit/node/error';
import {isCI} from '../../lib/is-ci.js';
+import {copyDiffBuild, prepareDiffDirectory} from '../../lib/template-diff.js';
const LOG_WORKER_BUILT = '📦 Worker built';
@@ -69,6 +70,7 @@ export default class Build extends Command {
}),
codegen: commonFlags.codegen,
'codegen-config-path': commonFlags.codegenConfigPath,
+ diff: commonFlags.diff,
base: deprecated('--base')(),
entry: deprecated('--entry')(),
@@ -77,13 +79,29 @@ export default class Build extends Command {
async run(): Promise {
const {flags} = await this.parse(Build);
- const directory = flags.path ? resolvePath(flags.path) : process.cwd();
+ const originalDirectory = flags.path
+ ? resolvePath(flags.path)
+ : process.cwd();
+ let directory = originalDirectory;
+
+ if (flags.diff) {
+ directory = await prepareDiffDirectory(originalDirectory, false);
+ }
await runBuild({
...flagsToCamelObject(flags),
useCodegen: flags.codegen,
directory,
});
+
+ if (flags.diff) {
+ await copyDiffBuild(directory, originalDirectory);
+ }
+
+ // The Remix compiler hangs due to a bug in ESBuild:
+ // https://github.com/evanw/esbuild/issues/2727
+ // The actual build has already finished so we can kill the process.
+ process.exit(0);
}
}
@@ -200,13 +218,6 @@ export async function runBuild({
if (process.env.NODE_ENV !== 'development') {
await cleanClientSourcemaps(buildPathClient);
}
-
- // The Remix compiler hangs due to a bug in ESBuild:
- // https://github.com/evanw/esbuild/issues/2727
- // The actual build has already finished so we can kill the process.
- if (!process.env.SHOPIFY_UNIT_TEST && !assetPath) {
- process.exit(0);
- }
}
async function cleanClientSourcemaps(buildPathClient: string) {
diff --git a/packages/cli/src/commands/hydrogen/dev.ts b/packages/cli/src/commands/hydrogen/dev.ts
index d7788da8ea..8d6a4f2289 100644
--- a/packages/cli/src/commands/hydrogen/dev.ts
+++ b/packages/cli/src/commands/hydrogen/dev.ts
@@ -1,5 +1,5 @@
-import path from 'path';
-import fs from 'fs/promises';
+import path from 'node:path';
+import fs from 'node:fs/promises';
import {outputDebug, outputInfo} from '@shopify/cli-kit/node/output';
import {fileExists} from '@shopify/cli-kit/node/fs';
import {renderFatalError} from '@shopify/cli-kit/node/ui';
@@ -35,6 +35,7 @@ import {checkRemixVersions} from '../../lib/remix-version-check.js';
import {getGraphiQLUrl} from '../../lib/graphiql-url.js';
import {displayDevUpgradeNotice} from './upgrade.js';
import {findPort} from '../../lib/find-port.js';
+import {prepareDiffDirectory} from '../../lib/template-diff.js';
const LOG_REBUILDING = '🧱 Rebuilding...';
const LOG_REBUILT = '🚀 Rebuilt';
@@ -68,11 +69,16 @@ export default class Dev extends Command {
default: false,
required: false,
}),
+ diff: commonFlags.diff,
};
async run(): Promise {
const {flags} = await this.parse(Dev);
- const directory = flags.path ? path.resolve(flags.path) : process.cwd();
+ let directory = flags.path ? path.resolve(flags.path) : process.cwd();
+
+ if (flags.diff) {
+ directory = await prepareDiffDirectory(directory, true);
+ }
await runDev({
...flagsToCamelObject(flags),
diff --git a/packages/cli/src/commands/hydrogen/init.test.ts b/packages/cli/src/commands/hydrogen/init.test.ts
index 348d3d1cfe..30f4764350 100644
--- a/packages/cli/src/commands/hydrogen/init.test.ts
+++ b/packages/cli/src/commands/hydrogen/init.test.ts
@@ -15,9 +15,9 @@ import {basename, joinPath} from '@shopify/cli-kit/node/path';
import {checkHydrogenVersion} from '../../lib/check-version.js';
import {handleProjectLocation} from '../../lib/onboarding/common.js';
import glob from 'fast-glob';
-import {getSkeletonSourceDir} from '../../lib/build.js';
+import {getRepoNodeModules, getSkeletonSourceDir} from '../../lib/build.js';
import {execAsync} from '../../lib/process.js';
-import {symlink, rmdir} from 'fs-extra';
+import {createSymlink, remove as rmdir} from 'fs-extra/esm';
import {runCheckRoutes} from './check.js';
import {runCodegen} from './codegen.js';
import {runBuild} from './build.js';
@@ -73,10 +73,8 @@ vi.mock(
// "Install" dependencies by linking to monorepo's node_modules
await rmdir(joinPath(directory, 'node_modules')).catch(() => {});
- await symlink(
- fileURLToPath(
- new URL('../../../../../node_modules', import.meta.url),
- ),
+ await createSymlink(
+ await getRepoNodeModules(),
joinPath(directory, 'node_modules'),
);
}),
diff --git a/packages/cli/src/lib/build.ts b/packages/cli/src/lib/build.ts
index d0b3ceb8ce..9269b086b9 100644
--- a/packages/cli/src/lib/build.ts
+++ b/packages/cli/src/lib/build.ts
@@ -1,4 +1,5 @@
import {fileURLToPath} from 'node:url';
+import {execAsync} from './process.js';
export const GENERATOR_TEMPLATES_DIR = 'generator-templates';
export const GENERATOR_STARTER_DIR = 'starter';
@@ -55,3 +56,11 @@ export function getSkeletonSourceDir() {
new URL(`../../../../templates/skeleton`, import.meta.url),
);
}
+
+export async function getRepoNodeModules() {
+ const {stdout} = await execAsync('npm root');
+ return (
+ stdout.trim() ||
+ fileURLToPath(new URL(`../../../../node_modules`, import.meta.url))
+ );
+}
diff --git a/packages/cli/src/lib/file.ts b/packages/cli/src/lib/file.ts
index e75ae13be8..75d98fcde6 100644
--- a/packages/cli/src/lib/file.ts
+++ b/packages/cli/src/lib/file.ts
@@ -1,11 +1,16 @@
-import {resolvePath} from '@shopify/cli-kit/node/path';
+import {readdir} from 'node:fs/promises';
+import {resolvePath, joinPath} from '@shopify/cli-kit/node/path';
import {
readFile,
writeFile,
fileExists,
isDirectory,
} from '@shopify/cli-kit/node/fs';
-import {readdir} from 'fs/promises';
+import {
+ readAndParsePackageJson,
+ writePackageJSON,
+ type PackageJson as _PackageJson,
+} from '@shopify/cli-kit/node/node-package-manager';
import {formatCode, type FormatOptions} from './format-code.js';
export async function replaceFileContent(
@@ -60,3 +65,82 @@ export async function findFileWithExtension(
return {};
}
+
+type PackageJson = _PackageJson & {
+ peerDependencies?: _PackageJson['dependencies'];
+ comment?: string;
+};
+
+const MANAGED_PACKAGE_JSON_KEYS = Object.freeze([
+ 'dependencies',
+ 'devDependencies',
+ 'peerDependencies',
+] as const);
+
+type ManagedKey = (typeof MANAGED_PACKAGE_JSON_KEYS)[number];
+
+export async function mergePackageJson(
+ sourceDir: string,
+ targetDir: string,
+ options?: {ignoredKeys?: string[]},
+) {
+ const targetPkgJson: PackageJson = await readAndParsePackageJson(
+ joinPath(targetDir, 'package.json'),
+ );
+ const sourcePkgJson: PackageJson = await readAndParsePackageJson(
+ joinPath(sourceDir, 'package.json'),
+ );
+
+ const ignoredKeys = new Set(['comment', ...(options?.ignoredKeys ?? [])]);
+
+ const unmanagedKeys = Object.keys(sourcePkgJson).filter(
+ (key) => !MANAGED_PACKAGE_JSON_KEYS.includes(key as ManagedKey),
+ ) as Exclude[];
+
+ for (const key of unmanagedKeys) {
+ if (ignoredKeys.has(key)) continue;
+
+ const sourceValue = sourcePkgJson[key];
+ const targetValue = targetPkgJson[key];
+
+ const newValue =
+ Array.isArray(sourceValue) && Array.isArray(targetValue)
+ ? [...targetValue, ...sourceValue]
+ : typeof sourceValue === 'object' && typeof targetValue === 'object'
+ ? {...targetValue, ...sourceValue}
+ : sourceValue;
+
+ targetPkgJson[key] = newValue as any;
+ }
+
+ const remixVersion = Object.entries(targetPkgJson.dependencies || {}).find(
+ ([dep]) => dep.startsWith('@remix-run/'),
+ )?.[1];
+
+ for (const key of MANAGED_PACKAGE_JSON_KEYS) {
+ if (ignoredKeys.has(key)) continue;
+
+ if (sourcePkgJson[key]) {
+ targetPkgJson[key] = [
+ ...new Set([
+ ...Object.keys(targetPkgJson[key] ?? {}),
+ ...Object.keys(sourcePkgJson[key] ?? {}),
+ ]),
+ ]
+ .sort()
+ .reduce((acc, dep) => {
+ let version = (sourcePkgJson[key]?.[dep] ??
+ targetPkgJson[key]?.[dep])!;
+
+ if (dep.startsWith('@remix-run/') && remixVersion) {
+ version = remixVersion;
+ }
+
+ acc[dep] = version;
+ return acc;
+ }, {} as Record);
+ }
+ }
+
+ await writePackageJSON(targetDir, targetPkgJson);
+}
diff --git a/packages/cli/src/lib/flags.ts b/packages/cli/src/lib/flags.ts
index 56961efcf4..dd4c1c4ae1 100644
--- a/packages/cli/src/lib/flags.ts
+++ b/packages/cli/src/lib/flags.ts
@@ -100,6 +100,12 @@ export const commonFlags = {
env: 'SHOPIFY_HYDROGEN_FLAG_INSPECTOR_PORT',
default: DEFAULT_INSPECTOR_PORT,
}),
+ diff: Flags.boolean({
+ description:
+ "Applies the current files on top of Hydrogen's starter template in a temporary directory.",
+ default: false,
+ required: false,
+ }),
};
export function flagsToCamelObject>(obj: T) {
diff --git a/packages/cli/src/lib/mini-oxygen/node.ts b/packages/cli/src/lib/mini-oxygen/node.ts
index e255574b5a..3f14a91126 100644
--- a/packages/cli/src/lib/mini-oxygen/node.ts
+++ b/packages/cli/src/lib/mini-oxygen/node.ts
@@ -2,6 +2,7 @@ import {randomUUID} from 'node:crypto';
import {AsyncLocalStorage} from 'node:async_hooks';
import {readFile} from '@shopify/cli-kit/node/fs';
import {renderSuccess} from '@shopify/cli-kit/node/ui';
+import {Response} from '@shopify/mini-oxygen';
import {
startServer,
Request,
@@ -14,6 +15,7 @@ import {
H2O_BINDING_NAME,
logRequestEvent,
handleDebugNetworkRequest,
+ setConstructors,
} from '../request-events.js';
export async function startNodeServer({
@@ -31,6 +33,8 @@ export async function startNodeServer({
}),
);
+ setConstructors({Response});
+
const asyncLocalStorage = new AsyncLocalStorage();
const serviceBindings = {
[H2O_BINDING_NAME]: {
diff --git a/packages/cli/src/lib/request-events.ts b/packages/cli/src/lib/request-events.ts
index ed003c0618..145fc26a10 100644
--- a/packages/cli/src/lib/request-events.ts
+++ b/packages/cli/src/lib/request-events.ts
@@ -1,7 +1,7 @@
import {EventEmitter} from 'node:events';
import {ReadableStream} from 'node:stream/web';
import {getGraphiQLUrl} from './graphiql-url.js';
-import {Request, Response} from '@shopify/mini-oxygen';
+import type {Request, Response} from '@shopify/mini-oxygen';
import type {
Request as WorkerdRequest,
Response as WorkerdResponse,
@@ -16,7 +16,7 @@ type InferredResponse = R extends WorkerdRequest
? WorkerdResponse
: Response;
-let ResponseConstructor = Response as typeof Response | typeof WorkerdResponse;
+let ResponseConstructor: typeof Response | typeof WorkerdResponse;
export function setConstructors(constructors: {
Response: typeof ResponseConstructor;
}) {
diff --git a/packages/cli/src/lib/setups/css/assets.ts b/packages/cli/src/lib/setups/css/assets.ts
index f8cbbc7134..74c70f7aa4 100644
--- a/packages/cli/src/lib/setups/css/assets.ts
+++ b/packages/cli/src/lib/setups/css/assets.ts
@@ -1,11 +1,6 @@
import {fileExists, readFile, writeFile} from '@shopify/cli-kit/node/fs';
import {joinPath} from '@shopify/cli-kit/node/path';
import {renderConfirmationPrompt} from '@shopify/cli-kit/node/ui';
-import {
- readAndParsePackageJson,
- writePackageJSON,
- type PackageJson as _PackageJson,
-} from '@shopify/cli-kit/node/node-package-manager';
import {
type AssetDir,
getAssetDir,
@@ -67,74 +62,3 @@ export async function canWriteFiles(
return true;
}
-
-type PackageJson = _PackageJson & {
- peerDependencies?: _PackageJson['dependencies'];
- comment?: string;
-};
-
-const MANAGED_PACKAGE_JSON_KEYS = Object.freeze([
- 'dependencies',
- 'devDependencies',
- 'peerDependencies',
-] as const);
-
-type ManagedKey = (typeof MANAGED_PACKAGE_JSON_KEYS)[number];
-
-export async function mergePackageJson(feature: AssetDir, projectDir: string) {
- const targetPkgJson: PackageJson = await readAndParsePackageJson(
- joinPath(projectDir, 'package.json'),
- );
- const sourcePkgJson: PackageJson = await readAndParsePackageJson(
- joinPath(getAssetDir(feature), 'package.json'),
- );
-
- delete sourcePkgJson.comment;
-
- const unmanagedKeys = Object.keys(sourcePkgJson).filter(
- (key) => !MANAGED_PACKAGE_JSON_KEYS.includes(key as ManagedKey),
- ) as Exclude[];
-
- for (const key of unmanagedKeys) {
- const sourceValue = sourcePkgJson[key];
- const targetValue = targetPkgJson[key];
-
- const newValue =
- Array.isArray(sourceValue) && Array.isArray(targetValue)
- ? [...targetValue, ...sourceValue]
- : typeof sourceValue === 'object' && typeof targetValue === 'object'
- ? {...targetValue, ...sourceValue}
- : sourceValue;
-
- targetPkgJson[key] = newValue as any;
- }
-
- const remixVersion = Object.entries(targetPkgJson.dependencies || {}).find(
- ([dep]) => dep.startsWith('@remix-run/'),
- )?.[1];
-
- for (const key of MANAGED_PACKAGE_JSON_KEYS) {
- if (sourcePkgJson[key]) {
- targetPkgJson[key] = [
- ...new Set([
- ...Object.keys(targetPkgJson[key] ?? {}),
- ...Object.keys(sourcePkgJson[key] ?? {}),
- ]),
- ]
- .sort()
- .reduce((acc, dep) => {
- let version = (sourcePkgJson[key]?.[dep] ??
- targetPkgJson[key]?.[dep])!;
-
- if (dep.startsWith('@remix-run/') && remixVersion) {
- version = remixVersion;
- }
-
- acc[dep] = version;
- return acc;
- }, {} as Record);
- }
- }
-
- await writePackageJSON(projectDir, targetPkgJson);
-}
diff --git a/packages/cli/src/lib/setups/css/css-modules.ts b/packages/cli/src/lib/setups/css/css-modules.ts
index 37de59f59f..1c61e572b4 100644
--- a/packages/cli/src/lib/setups/css/css-modules.ts
+++ b/packages/cli/src/lib/setups/css/css-modules.ts
@@ -1,14 +1,15 @@
-import {mergePackageJson} from './assets.js';
+import {mergePackageJson} from '../../file.js';
import {getCodeFormatOptions} from '../../format-code.js';
import type {CssSetupConfig, CssSetupResult} from './common.js';
import {injectCssBundlingLink} from './replacers.js';
+import {getAssetDir} from '../../build.js';
export async function setupCssModules({
rootDirectory,
appDirectory,
}: CssSetupConfig): Promise {
const workPromise = Promise.all([
- mergePackageJson('css-modules', rootDirectory),
+ mergePackageJson(getAssetDir('css-modules'), rootDirectory),
getCodeFormatOptions(rootDirectory).then((formatConfig) =>
injectCssBundlingLink(appDirectory, formatConfig),
),
diff --git a/packages/cli/src/lib/setups/css/postcss.ts b/packages/cli/src/lib/setups/css/postcss.ts
index 4b31d4baf2..efed3b0b86 100644
--- a/packages/cli/src/lib/setups/css/postcss.ts
+++ b/packages/cli/src/lib/setups/css/postcss.ts
@@ -1,5 +1,7 @@
import {outputInfo} from '@shopify/cli-kit/node/output';
-import {canWriteFiles, copyAssets, mergePackageJson} from './assets.js';
+import {mergePackageJson} from '../../file.js';
+import {canWriteFiles, copyAssets} from './assets.js';
+import {getAssetDir} from '../../build.js';
import type {CssSetupConfig, CssSetupResult} from './common.js';
export async function setupPostCss(
@@ -24,7 +26,7 @@ export async function setupPostCss(
}
const workPromise = Promise.all([
- mergePackageJson('postcss', rootDirectory),
+ mergePackageJson(getAssetDir('postcss'), rootDirectory),
copyAssets('postcss', assetMap, rootDirectory),
]);
diff --git a/packages/cli/src/lib/setups/css/tailwind.ts b/packages/cli/src/lib/setups/css/tailwind.ts
index df7d1aa097..81473013f7 100644
--- a/packages/cli/src/lib/setups/css/tailwind.ts
+++ b/packages/cli/src/lib/setups/css/tailwind.ts
@@ -1,9 +1,11 @@
import {outputInfo} from '@shopify/cli-kit/node/output';
import {joinPath, relativePath} from '@shopify/cli-kit/node/path';
-import {canWriteFiles, copyAssets, mergePackageJson} from './assets.js';
+import {mergePackageJson} from '../../file.js';
+import {canWriteFiles, copyAssets} from './assets.js';
import {getCodeFormatOptions} from '../../format-code.js';
-import type {CssSetupConfig, CssSetupResult} from './common.js';
import {replaceRootLinks} from './replacers.js';
+import {getAssetDir} from '../../build.js';
+import type {CssSetupConfig, CssSetupResult} from './common.js';
const tailwindCssPath = 'styles/tailwind.css';
@@ -33,7 +35,7 @@ export async function setupTailwind(
}
const workPromise = Promise.all([
- mergePackageJson('tailwind', rootDirectory),
+ mergePackageJson(getAssetDir('tailwind'), rootDirectory),
copyAssets('tailwind', assetMap, rootDirectory, (content, filepath) =>
filepath === 'tailwind.config.js'
? content.replace('{src-dir}', relativeAppDirectory)
diff --git a/packages/cli/src/lib/setups/css/vanilla-extract.ts b/packages/cli/src/lib/setups/css/vanilla-extract.ts
index 1334ab47f0..7c92eb6131 100644
--- a/packages/cli/src/lib/setups/css/vanilla-extract.ts
+++ b/packages/cli/src/lib/setups/css/vanilla-extract.ts
@@ -1,14 +1,15 @@
-import {mergePackageJson} from './assets.js';
+import {mergePackageJson} from '../../file.js';
import {getCodeFormatOptions} from '../../format-code.js';
-import type {CssSetupConfig, CssSetupResult} from './common.js';
import {injectCssBundlingLink} from './replacers.js';
+import {getAssetDir} from '../../build.js';
+import type {CssSetupConfig, CssSetupResult} from './common.js';
export async function setupVanillaExtract({
rootDirectory,
appDirectory,
}: CssSetupConfig): Promise {
const workPromise = Promise.all([
- mergePackageJson('vanilla-extract', rootDirectory),
+ mergePackageJson(getAssetDir('vanilla-extract'), rootDirectory),
getCodeFormatOptions(rootDirectory).then((formatConfig) =>
injectCssBundlingLink(appDirectory, formatConfig),
),
diff --git a/packages/cli/src/lib/template-diff.ts b/packages/cli/src/lib/template-diff.ts
new file mode 100644
index 0000000000..86088661e8
--- /dev/null
+++ b/packages/cli/src/lib/template-diff.ts
@@ -0,0 +1,120 @@
+import {rmdirSync} from 'node:fs';
+import {temporaryDirectory} from 'tempy';
+import {
+ createSymlink,
+ copy as copyDirectory,
+ remove as removeDirectory,
+} from 'fs-extra/esm';
+import {copyFile, removeFile} from '@shopify/cli-kit/node/fs';
+import {joinPath, relativePath} from '@shopify/cli-kit/node/path';
+import colors from '@shopify/cli-kit/node/colors';
+import {getRepoNodeModules, getStarterDir} from './build.js';
+import {mergePackageJson} from './file.js';
+
+/**
+ * Creates a new temporary project directory with the starter template and diff applied.
+ * Adds a watcher to sync files from the diff directory to the temporary directory.
+ * @param diffDirectory Directory with files to apply to the starter template
+ * @returns Temporary directory with the starter template and diff applied
+ */
+export async function prepareDiffDirectory(
+ diffDirectory: string,
+ watch: boolean,
+) {
+ const targetDirectory = temporaryDirectory({prefix: 'tmp-hydrogen-diff-'});
+ process.on('exit', () => rmdirSync(targetDirectory, {recursive: true}));
+
+ console.info(
+ `\n-- Applying diff to starter template in\n${colors.dim(
+ targetDirectory,
+ )}\n`,
+ );
+
+ await applyTemplateDiff(targetDirectory, diffDirectory);
+
+ await createSymlink(
+ await getRepoNodeModules(),
+ joinPath(targetDirectory, 'node_modules'),
+ );
+
+ if (watch) {
+ const pw = await import('@parcel/watcher').catch((error) => {
+ console.log('Could not watch for file changes.', error);
+ });
+
+ pw?.subscribe(
+ targetDirectory,
+ (error, events) => {
+ if (error) {
+ console.error(error);
+ return;
+ }
+
+ events.map((event) => {
+ return copyFile(
+ event.path,
+ joinPath(diffDirectory, relativePath(targetDirectory, event.path)),
+ );
+ });
+ },
+ {ignore: ['!*.generated.d.ts']},
+ );
+
+ pw?.subscribe(
+ diffDirectory,
+ async (error, events) => {
+ if (error) {
+ console.error(error);
+ return;
+ }
+
+ await events.map((event) => {
+ const targetFile = joinPath(
+ targetDirectory,
+ relativePath(diffDirectory, event.path),
+ );
+
+ return event.type === 'delete'
+ ? removeFile(targetFile).catch(() => {})
+ : copyFile(event.path, targetFile);
+ });
+ },
+ {ignore: ['*.generated.d.ts', 'package.json', 'tsconfig.json']},
+ );
+ }
+
+ return targetDirectory;
+}
+
+export async function applyTemplateDiff(
+ targetDirectory: string,
+ diffDirectory: string,
+ templateDir = getStarterDir(),
+) {
+ const createFilter = (re: RegExp) => (filepath: string) =>
+ !re.test(relativePath(templateDir, filepath));
+
+ await copyDirectory(templateDir, targetDirectory, {
+ filter: createFilter(/[\/\\](dist|node_modules|\.cache)(\/|\\|$)/i),
+ });
+ await copyDirectory(diffDirectory, targetDirectory, {
+ filter: createFilter(
+ /[\/\\](dist|node_modules|\.cache|package\.json|tsconfig\.json)(\/|\\|$)/i,
+ ),
+ });
+
+ await mergePackageJson(diffDirectory, targetDirectory, {
+ ignoredKeys: ['scripts'],
+ });
+}
+
+export async function copyDiffBuild(
+ targetDirectory: string,
+ diffDirectory: string,
+) {
+ const targetDist = joinPath(diffDirectory, 'dist');
+ await removeDirectory(targetDist);
+ await copyDirectory(joinPath(targetDirectory, 'dist'), targetDist, {
+ overwrite: true,
+ });
+}