Skip to content

Commit

Permalink
Merge pull request #390 from Shopify/fd-vite-rsc-request-provider
Browse files Browse the repository at this point in the history
Combine SSR and RSC responses + Request Provider
  • Loading branch information
frandiox authored Jan 12, 2022
2 parents 737944b + c56d374 commit 2548619
Show file tree
Hide file tree
Showing 19 changed files with 305 additions and 170 deletions.
2 changes: 2 additions & 0 deletions packages/dev/src/RSCTest/App.server.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {Suspense} from 'react';
import C1 from './C1.client';
import C2 from './C2.client';
import CShared from './CShared';
import CAsync1 from './CAsync1.server';

export default function App() {
// const pages = import.meta.globEager('./pages/**/*.server.[jt]sx');
Expand All @@ -15,6 +16,7 @@ export default function App() {
<C1 />
<C2 myProp2={true} />
<CShared myPropShared={true} />
<CAsync1 />
</div>
</Suspense>
);
Expand Down
15 changes: 15 additions & 0 deletions packages/dev/src/RSCTest/CAsync1.server.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import runAsync from './async-stuff';
import CAsync2 from './CAsync2.server';

export default function CAsync1() {
const result = runAsync('CAsync1');

return (
<div>
c-async-1 {String(result)}
<div>
<CAsync2></CAsync2>
</div>
</div>
);
}
7 changes: 7 additions & 0 deletions packages/dev/src/RSCTest/CAsync2.server.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import runAsync from './async-stuff';

export default function CAsync2() {
const result = runAsync('CAsync2');

return <div>c-async-2 {String(result)}</div>;
}
25 changes: 25 additions & 0 deletions packages/dev/src/RSCTest/async-stuff.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {useServerRequest} from '@shopify/hydrogen';

export default function (key, ms = 1000) {
const request = useServerRequest();
const cache = request.context.cache;

const cached = cache.get(key);

if (cached) {
if (cached instanceof Promise) {
throw cached;
}

return cached;
}

console.log('---FETCHING', key, request?.id || 'undefined');
const promise = new Promise((r) => setTimeout(() => r(true), ms));
cache.set(key, promise);
promise.then((result) => {
cache.set(key, result);
});

throw promise;
}
2 changes: 0 additions & 2 deletions packages/hydrogen/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,6 @@
"vite": "^2.7.1"
},
"dependencies": {
"react-client": "link:../../../react/build/node_modules/react-client",
"react-server": "link:../../../react/build/node_modules/react-server",
"path-to-regexp": "^6.2.0",
"@vitejs/plugin-react": "^1.1.1",
"connect": "^3.7.0",
Expand Down
99 changes: 89 additions & 10 deletions packages/hydrogen/src/entry-server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ import {Renderer, Hydrator, Streamer} from './types';
import {ServerComponentResponse} from './framework/Hydration/ServerComponentResponse.server';
import {ServerComponentRequest} from './framework/Hydration/ServerComponentRequest.server';
import {getCacheControlHeader} from './framework/cache';
import {ServerResponse} from 'http';
import {ServerRequestProvider} from './foundation/ServerRequestProvider';
import type {ServerResponse} from 'http';
import type {PassThrough as PassThroughType} from 'stream';

// @ts-ignore
Expand All @@ -37,6 +38,22 @@ declare global {
var __WORKER__: boolean;
}

function flightContainer({
init,
chunk,
nonce,
}: {
init?: boolean;
chunk?: string;
nonce?: string;
}) {
const normalizedChunk = chunk?.replace(/\\/g, String.raw`\\`);

return `<script${nonce ? ` nonce="${nonce}"` : ''}>window.__flight${
init ? '=[]' : `.push(\`${normalizedChunk}\`)`
}</script>`;
}

/**
* If a query is taking too long, or something else went wrong,
* send back a response containing the Suspense fallback and rely
Expand Down Expand Up @@ -134,6 +151,51 @@ const renderHydrogen: ServerHandler = (App, hook) => {
const log = getLoggerFromContext(request);
const state = {pathname: url.pathname, search: url.search};

// App for RSC rendering
const {ReactApp: ReactAppRSC} = buildReactApp({
App,
state,
context,
request,
dev,
log,
isRSC: true,
});

let flightResponseBuffer = '';
const flush = (writable: {write: (chunk: string) => void}, chunk = '') => {
if (flightResponseBuffer) {
chunk = flightResponseBuffer + chunk;
flightResponseBuffer = '';
}

if (chunk) {
writable.write(flightContainer({chunk}));
}
};

if (__WORKER__) {
// Worker branch
// TODO implement RSC with TransformStream?
} else {
// Node.js branch

const {pipe} = rscRenderToPipeableStream(<ReactAppRSC />);

const writer = await createNodeWriter();
writer.setEncoding('utf-8');
writer.on('data', (chunk: string) => {
if (response.headersSent) {
flush(response, chunk);
} else {
flightResponseBuffer += chunk;
}
});

pipe(writer);
}

// App for SSR rendering
const {ReactApp, componentResponse} = buildReactApp({
App,
state,
Expand All @@ -152,7 +214,11 @@ const renderHydrogen: ServerHandler = (App, hook) => {
let didError: Error | undefined;

const ReactAppSSR = (
<Html template={template} htmlAttrs={{lang: 'en'}}>
<Html
template={template}
htmlAttrs={{lang: 'en'}}
headSuffix={flightContainer({init: true})}
>
<ReactApp />
</Html>
);
Expand Down Expand Up @@ -278,6 +344,7 @@ const renderHydrogen: ServerHandler = (App, hook) => {
startWritingHtmlToServerResponse(
response,
pipe,
flush,
dev ? didError : undefined
);
},
Expand Down Expand Up @@ -305,6 +372,7 @@ const renderHydrogen: ServerHandler = (App, hook) => {
startWritingHtmlToServerResponse(
response,
pipe,
flush,
dev ? didError : undefined
);
}
Expand Down Expand Up @@ -347,6 +415,7 @@ const renderHydrogen: ServerHandler = (App, hook) => {
request,
dev,
log,
isRSC: true,
});

if (!__WORKER__ && response) {
Expand Down Expand Up @@ -404,30 +473,38 @@ function buildReactApp({
request,
dev,
log,
isRSC = false,
}: {
App: ComponentType;
state?: object | null;
context: any;
request: ServerComponentRequest;
dev: boolean | undefined;
log: Logger;
isRSC?: boolean;
}) {
const renderCache = {};
const helmetContext = {} as FilledContext;
const componentResponse = new ServerComponentResponse();
const hydrogenServerProps = {
request,
response: componentResponse,
helmetContext: helmetContext,
cache: renderCache,
log,
};

const ReactApp = (props: any) => (
// <RenderCacheProvider cache={renderCache}>
<App {...state} {...props} {...hydrogenServerProps} />
// </RenderCacheProvider>
);
const ReactApp = (props: any) => {
const AppContent = (
<ServerRequestProvider request={request} isRSC={isRSC}>
<App {...state} {...props} {...hydrogenServerProps} />
</ServerRequestProvider>
);

if (isRSC) return AppContent;

// Note: The <Suspense> wrapper in SSR is
// required to match hydration in browser
return <React.Suspense fallback={null}>{AppContent}</React.Suspense>;
};

return {helmetContext, ReactApp, componentResponse};
}
Expand Down Expand Up @@ -517,14 +594,16 @@ export default renderHydrogen;
function startWritingHtmlToServerResponse(
response: ServerResponse,
pipe: (r: ServerResponse) => void,
flush: (w: ServerResponse) => void,
error?: Error
) {
if (!response.headersSent) {
response.setHeader('Content-type', 'text/html');
response.write('<!DOCTYPE html>');
}

pipe(response);
pipe(response); // Writes <head> synchronously
flush(response); // Uses the <head> written

if (error) {
// This error was delayed until the headers were properly sent.
Expand Down

This file was deleted.

This file was deleted.

50 changes: 0 additions & 50 deletions packages/hydrogen/src/foundation/RenderCacheProvider/hook.ts

This file was deleted.

48 changes: 0 additions & 48 deletions packages/hydrogen/src/foundation/RenderCacheProvider/hook.tsx

This file was deleted.

This file was deleted.

Loading

0 comments on commit 2548619

Please sign in to comment.