Skip to content

Commit

Permalink
feat: implement RSC using react-server and react-client (#317)
Browse files Browse the repository at this point in the history
* wip: Create custom flight renderer with local version of react-server

* wip: mock client manifest and add module reference to client components

* wip: hack RSC resolution

* wip: wrap component in Proxy to access original properties

* refactor: rename RSC server files

* wip: add official RSC hydrator

* fix: Rename response.readRoot and remove explicit hydration

* wip: remove RR and Helmet providers from the server

* wip: Add test app

* wip: remove hydration providers

* wip: add renderToReadableStream for RSC

* refactor: cleanup custom RSC code

* refactor: move and rename files

* wip: fix test app

* refactor: cleanup custom RSC code

* refactor: do not pass named boolean in RSC

* refactor: simplify ClientMarker

* test: fix ClientMarker tests

* fix: delay throwing error when client component is missing

* refactor: simplify import globs code

* feat: provide request object to rendering tree

* wip: example of useServerRequest

* feat: inline RSC response in the SSR HTML response

* fix: add default value to the SSR provider

* feat: stream rsc in script tags

* feat: update the starter template to work

* fix: lint errors

* refactor: move code around and cleanup

* chore: upgrade Vite to 2.7.0

* chore: add react-server-dom-vite as vendor

* feat: replace local react-server and react-client with react-server-dom-vite vendor

* fix: react-server-dom-vite allow exporting hooks from client components temporarily

* fix: move the server logging locations

* feat: Implement ReadableStream branch in SSR and RSC

* test: fix playground apps

* fix: avoid importing undefined exports depending on the running environment

* feat: Buffer RSC response if it cannot be streamed

* fix: tests

* fix: e2e test

* fix: linting

* fix: rename test helper

* fix: update hydrogen template files

* feat: remove react-ssr-prepass. RSC already makes ReadableStream required

* refactor: simplify hydration calls

* feat: remove old dependencies

* feat: enable tree shaking in worker build

* fix: Release stream lock before piping

* chore: fix formatting

* refactor: move code to entry-server to be consistent

* refactor: remove HydrationWriter in favor of Node native PassThrough

* fix: move constant to a separate file to avoid importing app logic in GraphiQL

* refactor: remove old code

* fix: maybe fix tests for windows

* fix: stream import in workers

* fix: flush RSC right after writing head

* feat: replace RenderCacheProvider with new ServerRequestProvider cache

* refactor: cleanup

* fix: suspense breaking hydration

* fix: normalize RSC chunks

* refactor: simplify customBody check

* feat: minor tree-shaking improvements

* fix: enable browser hydration

* fix: replace TransformStream with ReadableStream to support Firefox

* refactor: cleanup and rename variables

* fix: normalizePath

* fix: better regex

* fix: request.context property is reserved in CFW

* perf: Do not clone request to avoid extra memory and warnings in CFW

* chore: add TODO to remove weird Suspense boundary

* fix: Add back ShopifyProvider; call setShopConfig as part of renderHydrogen

* chore: remove RSCTest demo code

* fix: useMemo is allowed in RSC

* fix: Dot not create Response with streams to support CFW

Co-authored-by: Bret Little <[email protected]>
Co-authored-by: M. Bagher Abiat <[email protected]>
Co-authored-by: Josh Larson <[email protected]>
  • Loading branch information
4 people authored Jan 19, 2022
1 parent 9ac6d03 commit d8c87eb
Show file tree
Hide file tree
Showing 132 changed files with 8,313 additions and 2,413 deletions.
6 changes: 6 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ module.exports = {
allowModules: ['@shopify/hydrogen', '@shopify/react-testing'],
},
],
'node/no-extraneous-require': [
'error',
{
allowModules: ['@shopify/hydrogen'],
},
],
'node/no-unpublished-import': 'off',
'node/no-unsupported-features/es-syntax': 'off',
'node/no-unsupported-features/es-builtins': [
Expand Down
29 changes: 11 additions & 18 deletions examples/template-hydrogen-default/src/App.server.jsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,25 @@
import {ShopifyServerProvider, DefaultRoutes} from '@shopify/hydrogen';
import {Switch} from 'react-router-dom';
import {DefaultRoutes} from '@shopify/hydrogen';
import {Suspense} from 'react';

import shopifyConfig from '../shopify.config';

import DefaultSeo from './components/DefaultSeo.server';
import NotFound from './components/NotFound.server';
import CartProvider from './components/CartProvider.client';
import AppClient from './App.client';
import LoadingFallback from './components/LoadingFallback';

export default function App({log, ...serverState}) {
const pages = import.meta.globEager('./pages/**/*.server.[jt]sx');

return (
<Suspense fallback={<LoadingFallback />}>
<ShopifyServerProvider shopifyConfig={shopifyConfig} {...serverState}>
<CartProvider>
<DefaultSeo />
<Switch>
<DefaultRoutes
pages={pages}
serverState={serverState}
log={log}
fallback={<NotFound />}
/>
</Switch>
</CartProvider>
</ShopifyServerProvider>
<AppClient helmetContext={serverState.helmetContext}>
<DefaultSeo />
<DefaultRoutes
pages={pages}
serverState={serverState}
log={log}
fallback={<NotFound />}
/>
</AppClient>
</Suspense>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import gql from 'graphql-tag';

import Header from './Header.client';
import Footer from './Footer.server';
import {useCartUI} from './CartUIProvider.client';
import Cart from './Cart.client';

/**
Expand All @@ -25,7 +24,6 @@ export default function Layout({children, hero}) {
staleWhileRevalidate: 60 * 10,
},
});
const {isCartOpen, closeCart} = useCartUI();
const collections = data ? flattenConnection(data.collections) : null;
const products = data ? flattenConnection(data.products) : null;
const storeName = data ? data.shop.name : '';
Expand All @@ -42,16 +40,7 @@ export default function Layout({children, hero}) {
</div>
<div className="min-h-screen max-w-screen text-gray-700 font-sans">
<Header collections={collections} storeName={storeName} />
<div>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<div
className={`z-50 fixed top-0 bottom-0 left-0 right-0 bg-black transition-opacity duration-400 ${
isCartOpen ? 'opacity-20' : 'opacity-0 pointer-events-none'
}`}
onClick={isCartOpen ? closeCart : null}
/>
<Cart />
</div>
<Cart />
<main role="main" id="mainContent" className="relative bg-gray-50">
{hero}
<div className="mx-auto max-w-7xl p-4 md:py-5 md:px-8">
Expand Down
8 changes: 2 additions & 6 deletions examples/template-hydrogen-default/src/entry-client.jsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import renderHydrogen from '@shopify/hydrogen/entry-client';
import {ShopifyProvider} from '@shopify/hydrogen/client';

import shopifyConfig from '../shopify.config';

function ClientApp({children}) {
return (
<ShopifyProvider shopifyConfig={shopifyConfig}>{children}</ShopifyProvider>
);
return children;
}

export default renderHydrogen(ClientApp);
export default renderHydrogen(ClientApp, {shopifyConfig});
5 changes: 2 additions & 3 deletions examples/template-hydrogen-default/src/entry-server.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import renderHydrogen from '@shopify/hydrogen/entry-server';
import shopifyConfig from '../shopify.config';

import App from './App.server';

export default renderHydrogen(App, () => {
// Custom hook
});
export default renderHydrogen(App, {shopifyConfig});
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
flattenConnection,
RawHtml,
} from '@shopify/hydrogen';
import {useParams} from 'react-router-dom';
import gql from 'graphql-tag';

import LoadMoreProducts from '../../components/LoadMoreProducts.client';
Expand All @@ -16,8 +15,9 @@ import NotFound from '../../components/NotFound.server';
export default function Collection({
country = {isoCode: 'US'},
collectionProductCount = 24,
params,
}) {
const {handle} = useParams();
const {handle} = params;
const {data} = useShopQuery({
query: QUERY,
variables: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import {useParams} from 'react-router-dom';
import {useShopQuery, RawHtml} from '@shopify/hydrogen';
import gql from 'graphql-tag';

import Layout from '../../components/Layout.server';
import NotFound from '../../components/NotFound.server';

export default function Page() {
const {handle} = useParams();
export default function Page({params}) {
const {handle} = params;
const {data} = useShopQuery({query: QUERY, variables: {handle}});

if (!data.pageByHandle) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import {useShopQuery, ProductProviderFragment} from '@shopify/hydrogen';
import {useParams} from 'react-router-dom';
import gql from 'graphql-tag';

import ProductDetails from '../../components/ProductDetails.client';
import NotFound from '../../components/NotFound.server';
import Layout from '../../components/Layout.server';

export default function Product({country = {isoCode: 'US'}}) {
const {handle} = useParams();
export default function Product({country = {isoCode: 'US'}, params}) {
const {handle} = params;

const {data} = useShopQuery({
query: QUERY,
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
"ts-jest": "^26.5.4",
"ts-node": "^10.2.1",
"typescript": "^4.2.3",
"vite": "^2.7.0",
"vite": "^2.7.1",
"yorkie": "^2.0.0",
"glob": "^7.2.0",
"shelljs": "^0.8.4"
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
"semver": "^7.3.5",
"sirv": "^1.0.14",
"typescript": "^4.2.3",
"vite": "^2.7.0"
"vite": "^2.7.1"
},
"files": [
"dist",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,13 @@ export default function ({ifFeature}: TemplateOptions) {
return `
import renderHydrogen from '@shopify/hydrogen/entry-client';
${ifFeature(Feature.Pwa, `import {registerSW} from 'virtual:pwa-register';`)}
import {ShopifyProvider} from '@shopify/hydrogen/client';
import shopifyConfig from '../shopify.config';
function ClientApp({children}) {
${ifFeature(Feature.Pwa, 'registerSW()')}
return (
<ShopifyProvider shopifyConfig={shopifyConfig}>{children}</ShopifyProvider>
);
return children;
}
export default renderHydrogen(ClientApp);
export default renderHydrogen(ClientApp, {shopifyConfig});
`;
}
2 changes: 1 addition & 1 deletion packages/dev/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"prettier": "^2.3.2",
"stylelint": "^13.13.0",
"tailwindcss": "^3.0.0",
"vite": "^2.7.0"
"vite": "^2.7.1"
},
"dependencies": {
"@headlessui/react": "^1.4.1",
Expand Down
13 changes: 13 additions & 0 deletions packages/dev/src/App.client.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {HelmetProvider} from '@shopify/hydrogen/client';
import CartProvider from './components/CartProvider.client';

/**
* Setup client context, though the children are most likely server components
*/
export default function ClientApp({helmetContext, children}) {
return (
<HelmetProvider helmetContext={helmetContext}>
<CartProvider>{children}</CartProvider>
</HelmetProvider>
);
}
29 changes: 11 additions & 18 deletions packages/dev/src/App.server.jsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,25 @@
import {ShopifyServerProvider, DefaultRoutes} from '@shopify/hydrogen';
import {Switch} from 'react-router-dom';
import {DefaultRoutes} from '@shopify/hydrogen';
import {Suspense} from 'react';

import shopifyConfig from '../shopify.config';

import DefaultSeo from './components/DefaultSeo.server';
import NotFound from './components/NotFound.server';
import CartProvider from './components/CartProvider.client';
import AppClient from './App.client';
import LoadingFallback from './components/LoadingFallback';

export default function App({log, ...serverState}) {
const pages = import.meta.globEager('./pages/**/*.server.[jt]sx');

return (
<Suspense fallback={<LoadingFallback />}>
<ShopifyServerProvider shopifyConfig={shopifyConfig} {...serverState}>
<CartProvider>
<DefaultSeo />
<Switch>
<DefaultRoutes
pages={pages}
serverState={serverState}
log={log}
fallback={<NotFound />}
/>
</Switch>
</CartProvider>
</ShopifyServerProvider>
<AppClient helmetContext={serverState.helmetContext}>
<DefaultSeo />
<DefaultRoutes
pages={pages}
serverState={serverState}
log={log}
fallback={<NotFound />}
/>
</AppClient>
</Suspense>
);
}
41 changes: 25 additions & 16 deletions packages/dev/src/components/Cart.client.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,33 @@ export default function Cart() {
const itemCount = useCartLinesTotalQuantity();

return (
<Dialog open={isCartOpen} onClose={closeCart}>
<Dialog.Overlay className="fixed z-20 inset-0 bg-gray-50 opacity-75" />
<div>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<div
className={`absolute flex flex-col md:block z-20 top-0 left-0 right-0 bottom-0 md:top-7 h-full md:left-auto md:right-7 md:bottom-auto md:h-auto md:max-h-[calc(100vh-56px)] bg-gray-50 w-full md:w-[470px] rounded-b-lg shadow-2xl ${
itemCount === 0 ? 'overflow-hidden' : 'overflow-y-scroll'
className={`z-20 fixed top-0 bottom-0 left-0 right-0 bg-black transition-opacity duration-400 ${
isCartOpen ? 'opacity-20' : 'opacity-0 pointer-events-none'
}`}
>
<CartHeader />
{itemCount === 0 ? (
<CartEmpty />
) : (
<>
<CartItems />
<CartFooter />
</>
)}
</div>
</Dialog>
onClick={isCartOpen ? closeCart : null}
/>
<Dialog open={isCartOpen} onClose={closeCart}>
<Dialog.Overlay className="fixed z-20 inset-0 bg-gray-50 opacity-75" />
<div
className={`absolute flex flex-col md:block z-20 top-0 left-0 right-0 bottom-0 md:top-7 h-full md:left-auto md:right-7 md:bottom-auto md:h-auto md:max-h-[calc(100vh-56px)] bg-gray-50 w-full md:w-[470px] rounded-b-lg shadow-2xl ${
itemCount === 0 ? 'overflow-hidden' : 'overflow-y-scroll'
}`}
>
<CartHeader />
{itemCount === 0 ? (
<CartEmpty />
) : (
<>
<CartItems />
<CartFooter />
</>
)}
</div>
</Dialog>
</div>
);
}

Expand Down
2 changes: 1 addition & 1 deletion packages/dev/src/components/DefaultSeo.server.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Seo from './Seo.client';
/**
* A server component that fetches a `shop.name` and sets default values and templates for every page on a website
*/
export default function SeoServer() {
export default function DefaultSeo() {
const {
data: {
shop: {name: shopName},
Expand Down
17 changes: 5 additions & 12 deletions packages/dev/src/components/Layout.server.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import gql from 'graphql-tag';

import Header from './Header.client';
import Footer from './Footer.server';
import {useCartUI} from './CartUIProvider.client';
import Cart from './Cart.client';
import {Suspense} from 'react';

/**
* A server component that defines a structure and organization of a page that can be used in different parts of the Hydrogen app
Expand All @@ -25,7 +25,6 @@ export default function Layout({children, hero}) {
staleWhileRevalidate: 60 * 10,
},
});
const {isCartOpen, closeCart} = useCartUI();
const collections = data ? flattenConnection(data.collections) : null;
const products = data ? flattenConnection(data.products) : null;
const storeName = data ? data.shop.name : '';
Expand All @@ -41,17 +40,11 @@ export default function Layout({children, hero}) {
</a>
</div>
<div className="min-h-screen max-w-screen text-gray-700 font-sans">
<Header collections={collections} storeName={storeName} />
<div>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<div
className={`z-50 fixed top-0 bottom-0 left-0 right-0 bg-black transition-opacity duration-400 ${
isCartOpen ? 'opacity-20' : 'opacity-0 pointer-events-none'
}`}
onClick={isCartOpen ? closeCart : null}
/>
{/* TODO: Find out why Suspense needs to be here to prevent hydration errors. */}
<Suspense fallback={null}>
<Header collections={collections} storeName={storeName} />
<Cart />
</div>
</Suspense>
<main role="main" id="mainContent" className="relative bg-gray-50">
{hero}
<div className="mx-auto max-w-7xl p-4 md:py-5 md:px-8">
Expand Down
8 changes: 2 additions & 6 deletions packages/dev/src/entry-client.jsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import renderHydrogen from '@shopify/hydrogen/entry-client';
import {ShopifyProvider} from '@shopify/hydrogen/client';

import shopifyConfig from '../shopify.config';

function ClientApp({children}) {
return (
<ShopifyProvider shopifyConfig={shopifyConfig}>{children}</ShopifyProvider>
);
return children;
}

export default renderHydrogen(ClientApp);
export default renderHydrogen(ClientApp, {shopifyConfig});
5 changes: 2 additions & 3 deletions packages/dev/src/entry-server.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import renderHydrogen from '@shopify/hydrogen/entry-server';
import shopifyConfig from '../shopify.config';

import App from './App.server';

export default renderHydrogen(App, () => {
// Custom hook
});
export default renderHydrogen(App, {shopifyConfig});
Loading

0 comments on commit d8c87eb

Please sign in to comment.