Skip to content

Commit

Permalink
Back to Vite 🎉 (#1728)
Browse files Browse the repository at this point in the history
* Bump Remix to 2.6.0

* Super experimental Vite

* Add vite-skeleton template

* Add more missing features

* Refactor and support Subrequest Profiler

* Pass client info as bindings instead of replacing strings

* Basic support for inspector

* Simplify servers

* Fix server HMR

* Log request lines

* Add comments

* Refactor and simplify

* Extract remix patch

* Fix requests with body

* Turn Vite template into diff example

* Fix Vite diffs

* Cleanup

* Add --entry flag

* Add build-vite command

* Fix build

* Support for deploy command

* Support --diff in deploy command

* Avoid consuming redirects in Vite<>Workerd

* Simplify HMR logic

* Warmup Vite cache

* Support local dev and reload config

* Cleanup

* Minor refactor and fix

* Support subrequest profiler

* Cleanup

* Update to Vite 5.1 stable

* Adjust isbot version

* Move files

* Generate types in CLI for entry points

* Refactor, remove early publicUrl dependency

* Extract as a Vite plugin

* Fix minioxygen entry in main compiler

* Fix sourcemaps

* Extract shared utilities

* Move Vite config responsibilities to plugin

* Simplify workerd entry module, add comments

* Simplify server HMR

* Avoid running in Remix child compiler

* Fix HMR when using the same server for HTTP and WS

* Default to minify:true for worker build

* Refactor subrequest profiler sourcemap handling to make it more flexible

* Partially fix subrequest profiler locations for Vite

* Refactor use workers option in Miniflare

* Rename worker

* Split plugins

* Transform SSR entry to accept import.meta.hot automatically

* Fix CSS flash in development

* Split middlewares in files, rename worker entry

* Make setupFunctions more robust

* Fix critical CSS crash for virtual routes

* Decouple Oxygen plugin from Hydrogen's subrequest profiler

* Rename internal option

* Move virtual routes logic to Hydrogen plugin

* Improve logs

* Improve --template flag errors

* Fix env variables in Vite

* Support creating examples from local repo using LOCAL_DEV=true

* Allow skipping skeleton files in diff examples

* Update Vite template

* Fix paths in JS Vite projects

* Add readme to Vite example

* Remove old build required plugin

* Fix timing issue in test

* Fix another timing issue

* Show virtual routes in a banner

* Update to Remix 2.7

* Dedupe package-lock.json

* Remove unneeded parameter after upgrading Remix

* Remove unneeded dependencies after upgrading Remix

* Fix set-cookie header

* Merge main

* Remove copied code

* Remove old code

* Cleanup build output

* Make Oxygen plugin agnostic from Remix

* Changesets
  • Loading branch information
frandiox authored Feb 29, 2024
1 parent 799f4cd commit e641255
Show file tree
Hide file tree
Showing 47 changed files with 3,640 additions and 2,055 deletions.
27 changes: 27 additions & 0 deletions .changeset/nasty-files-reply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
'@shopify/cli-hydrogen': minor
---

Add experimental support for Vite projects.

In the Vite config of you Vite<>Remix project, import and use the new experimental Hydrogen and Oxygen plugins, and include them before Remix:

```ts
import {defineConfig} from 'vite';
import {hydrogen, oxygen} from '@shopify/cli-hydrogen/experimental-vite';
import {vitePlugin as remix} from '@remix-run/dev';
import tsconfigPaths from 'vite-tsconfig-paths';

export default defineConfig({
plugins: [
hydrogen(), // Adds utilities like GraphiQL and Subrequest Profiler
oxygen(), // Runs your app using the MiniOxygen runtime (closer to production)
remix({buildDirectory: 'dist'}), // Use `dist` to be compatible with `h2 deploy`
tsconfigPaths(),
],
});
```

Then, run `h2 dev-vite` and `h2 build-vite` commands to start and build your app.

Please report any issue with this new feature, and let us know if you have any feedback or suggestions.
64 changes: 64 additions & 0 deletions examples/vite/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Hydrogen template: Experimental Vite

Hydrogen is Shopify’s stack for headless commerce. Hydrogen is designed to dovetail with [Remix](https://remix.run/), Shopify’s full stack web framework. This template contains a **minimal setup** of components, queries and tooling to get started with Hydrogen and Vite.

[Check out Hydrogen docs](https://shopify.dev/custom-storefronts/hydrogen)
[Get familiar with Remix](https://remix.run/docs/en/v1)

## What's included

- Remix
- Hydrogen
- Oxygen
- Vite
- Shopify CLI
- ESLint
- Prettier
- GraphQL generator
- TypeScript and JavaScript flavors
- Minimal setup of components and routes

## Getting started

**Requirements:**

- Node.js version 18.0.0 or higher

```bash
npm create @shopify/hydrogen@latest -- --template vite
```

## Building for production

```bash
npm run build
```

## Local development

```bash
npm run dev
```

## Setup for using Customer Account API (`/account` section)

### Setup public domain using ngrok

1. Setup a [ngrok](https://ngrok.com/) account and add a permanent domain (ie. `https://<your-ngrok-domain>.app`).
1. Install the [ngrok CLI](https://ngrok.com/download) to use in terminal
1. Start ngrok using `ngrok http --domain=<your-ngrok-domain>.app 3000`

### Include public domain in Customer Account API settings

1. Go to your Shopify admin => `Hydrogen` or `Headless` app/channel => Customer Account API => Application setup
1. Edit `Callback URI(s)` to include `https://<your-ngrok-domain>.app/account/authorize`
1. Edit `Javascript origin(s)` to include your public domain `https://<your-ngrok-domain>.app` or keep it blank
1. Edit `Logout URI` to include your public domain `https://<your-ngrok-domain>.app` or keep it blank

### Prepare Environment variables

Run [`npx shopify hydrogen link`](https://shopify.dev/docs/custom-storefronts/hydrogen/cli#link) or [`npx shopify hydrogen env pull`](https://shopify.dev/docs/custom-storefronts/hydrogen/cli#env-pull) to link this app to your own test shop.

Alternatly, the values of the required environment varaibles "PUBLIC_CUSTOMER_ACCOUNT_API_CLIENT_ID" and "PUBLIC_CUSTOMER_ACCOUNT_API_URL" can be found in customer account api settings in the Hydrogen admin channel.

🗒️ Note that mock.shop doesn't supply these variables automatically.
240 changes: 240 additions & 0 deletions examples/vite/app/root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
import {useNonce} from '@shopify/hydrogen';
import {
defer,
type SerializeFrom,
type LoaderFunctionArgs,
} from '@shopify/remix-oxygen';
import {
Links,
Meta,
Outlet,
Scripts,
useMatches,
useRouteError,
useLoaderData,
ScrollRestoration,
isRouteErrorResponse,
type ShouldRevalidateFunction,
} from '@remix-run/react';
import {Layout} from '~/components/Layout';

import './styles/reset.css';
import './styles/app.css';

/**
* 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: 'preconnect',
href: 'https://cdn.shopify.com',
},
{
rel: 'preconnect',
href: 'https://shop.app',
},
{rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg'},
];
}

/**
* Access the result of the root loader from a React component.
*/
export const useRootLoaderData = () => {
const [root] = useMatches();
return root?.data as SerializeFrom<typeof loader>;
};

export async function loader({context}: LoaderFunctionArgs) {
const {storefront, customerAccount, cart} = context;
const publicStoreDomain = context.env.PUBLIC_STORE_DOMAIN;

const isLoggedInPromise = customerAccount.isLoggedIn();

// defer the cart query by not awaiting it
const cartPromise = cart.get();

// defer the footer query (below the fold)
const footerPromise = storefront.query(FOOTER_QUERY, {
cache: storefront.CacheLong(),
variables: {
footerMenuHandle: 'footer', // Adjust to your footer menu handle
},
});

// await the header query (above the fold)
const headerPromise = storefront.query(HEADER_QUERY, {
cache: storefront.CacheLong(),
variables: {
headerMenuHandle: 'main-menu', // Adjust to your header menu handle
},
});

return defer(
{
cart: cartPromise,
footer: footerPromise,
header: await headerPromise,
isLoggedIn: isLoggedInPromise,
publicStoreDomain,
},
{
headers: {
'Set-Cookie': await context.session.commit(),
},
},
);
}

export default function App() {
const nonce = useNonce();
const data = useLoaderData<typeof loader>();

return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<Meta />
<Links />
</head>
<body>
<Layout {...data}>
<Outlet />
</Layout>
<ScrollRestoration nonce={nonce} />
<Scripts nonce={nonce} />
</body>
</html>
);
}

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 (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<Meta />
<Links />
</head>
<body>
<Layout {...rootData}>
<div className="route-error">
<h1>Oops</h1>
<h2>{errorStatus}</h2>
{errorMessage && (
<fieldset>
<pre>{errorMessage}</pre>
</fieldset>
)}
</div>
</Layout>
<ScrollRestoration nonce={nonce} />
<Scripts nonce={nonce} />
</body>
</html>
);
}

const MENU_FRAGMENT = `#graphql
fragment MenuItem on MenuItem {
id
resourceId
tags
title
type
url
}
fragment ChildMenuItem on MenuItem {
...MenuItem
}
fragment ParentMenuItem on MenuItem {
...MenuItem
items {
...ChildMenuItem
}
}
fragment Menu on Menu {
id
items {
...ParentMenuItem
}
}
` as const;

const HEADER_QUERY = `#graphql
fragment Shop on Shop {
id
name
description
primaryDomain {
url
}
brand {
logo {
image {
url
}
}
}
}
query Header(
$country: CountryCode
$headerMenuHandle: String!
$language: LanguageCode
) @inContext(language: $language, country: $country) {
shop {
...Shop
}
menu(handle: $headerMenuHandle) {
...Menu
}
}
${MENU_FRAGMENT}
` as const;

const FOOTER_QUERY = `#graphql
query Footer(
$country: CountryCode
$footerMenuHandle: String!
$language: LanguageCode
) @inContext(language: $language, country: $country) {
menu(handle: $footerMenuHandle) {
...Menu
}
}
${MENU_FRAGMENT}
` as const;
47 changes: 47 additions & 0 deletions examples/vite/env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/// <reference types="vite/client" />
/// <reference types="@shopify/remix-oxygen" />
/// <reference types="@shopify/oxygen-workers-types" />

// Enhance TypeScript's built-in typings.
import '@total-typescript/ts-reset';

import type {
Storefront,
CustomerAccount,
HydrogenCart,
} from '@shopify/hydrogen';
import type {AppSession} from '~/lib/session';

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;
customerAccount: CustomerAccount;
session: AppSession;
waitUntil: ExecutionContext['waitUntil'];
}
}
Loading

0 comments on commit e641255

Please sign in to comment.