Compare commits

...

2 commits

Author SHA1 Message Date
Fran Dios 3fdcf6815b feat: SPA redirect 2021-11-09 18:22:24 +09:00
Fran Dios 293a065ff7 feat: basic redirect support 2021-11-09 17:30:11 +09:00
4 changed files with 81 additions and 8 deletions

View file

@ -1,7 +1,7 @@
import React, {Suspense, useState} from 'react'; import React, {Suspense, useState} from 'react';
// @ts-ignore // @ts-ignore
import {createRoot} from 'react-dom'; import {createRoot} from 'react-dom';
import {BrowserRouter} from 'react-router-dom'; import {BrowserRouter, Redirect} from 'react-router-dom';
import type {ClientHandler} from './types'; import type {ClientHandler} from './types';
import {ErrorBoundary} from 'react-error-boundary'; import {ErrorBoundary} from 'react-error-boundary';
import {HelmetProvider} from 'react-helmet-async'; import {HelmetProvider} from 'react-helmet-async';
@ -35,7 +35,8 @@ function Content({clientWrapper: ClientWrapper}: {clientWrapper: any}) {
pathname: window.location.pathname, pathname: window.location.pathname,
search: window.location.search, search: window.location.search,
}); });
const response = useServerResponse(serverState);
const response = useServerResponse(serverState).read();
return ( return (
<ServerStateProvider <ServerStateProvider
@ -46,8 +47,12 @@ function Content({clientWrapper: ClientWrapper}: {clientWrapper: any}) {
<HelmetProvider> <HelmetProvider>
<BrowserRouter> <BrowserRouter>
<ServerStateRouter /> <ServerStateRouter />
{/* @ts-ignore */} {response.redirect ? (
<ClientWrapper>{response.read()}</ClientWrapper> <Redirect to={response.redirect} />
) : (
/* @ts-ignore */
<ClientWrapper>{response}</ClientWrapper>
)}
</BrowserRouter> </BrowserRouter>
</HelmetProvider> </HelmetProvider>
</QueryProvider> </QueryProvider>

View file

@ -20,6 +20,7 @@ import {ServerComponentResponse} from './framework/Hydration/ServerComponentResp
import {ServerComponentRequest} from './framework/Hydration/ServerComponentRequest.server'; import {ServerComponentRequest} from './framework/Hydration/ServerComponentRequest.server';
import {dehydrate} from 'react-query/hydration'; import {dehydrate} from 'react-query/hydration';
import {getCacheControlHeader} from './framework/cache'; import {getCacheControlHeader} from './framework/cache';
import type {ServerResponse} from 'http';
/** /**
* react-dom/unstable-fizz provides different entrypoints based on runtime: * react-dom/unstable-fizz provides different entrypoints based on runtime:
@ -118,6 +119,14 @@ const renderHydrogen: ServerHandler = (App, hook) => {
componentResponse.cacheControlHeader componentResponse.cacheControlHeader
); );
if (componentResponse.customHead) {
writeHeadToServerResponse(componentResponse, response);
if (response.statusCode >= 300 && response.statusCode < 400) {
// Redirect
return response.end();
}
}
if (!componentResponse.canStream()) return; if (!componentResponse.canStream()) return;
response.statusCode = didError ? 500 : 200; response.statusCode = didError ? 500 : 200;
@ -205,6 +214,15 @@ const renderHydrogen: ServerHandler = (App, hook) => {
* `template` and `script` tags inserted and rendered as part of the hydration response. * `template` and `script` tags inserted and rendered as part of the hydration response.
*/ */
onCompleteAll() { onCompleteAll() {
if (componentResponse.customHead) {
writeHeadToServerResponse(componentResponse, response);
if (response.statusCode >= 300 && response.statusCode < 400) {
// Redirect: mock status to bypass fetch opaque responses
response.statusCode = 299;
return response.end();
}
}
// Tell React to start writing to the writer // Tell React to start writing to the writer
startWriting(); startWriting();
@ -432,4 +450,23 @@ async function renderAppFromStringWithPrepass(
: body; : body;
} }
export function writeHeadToServerResponse(
{customHead = {}}: ServerComponentResponse,
serverResponse: ServerResponse
) {
if (customHead.headers) {
for (const [key, value] of Object.entries(customHead.headers)) {
serverResponse.setHeader(key, value);
}
}
if (customHead.statusText) {
serverResponse.statusMessage = customHead.statusText;
}
if (customHead.status) {
serverResponse.statusCode = customHead.status;
}
}
export default renderHydrogen; export default renderHydrogen;

View file

@ -35,10 +35,13 @@ function createFromFetch(fetchPromise: Promise<any>) {
if (!response.ok) { if (!response.ok) {
throw new Error(`Hydration request failed: ${response.statusText}`); throw new Error(`Hydration request failed: ${response.statusText}`);
} }
return response.text();
}) // Mocked status to bypass fetch opaque responses
.then((payload) => { if (response.status === 299) {
return convertHydrationResponseToReactComponents(payload); return {redirect: response.headers.get('location')};
}
return response.text().then(convertHydrationResponseToReactComponents);
}) })
.catch((e) => { .catch((e) => {
console.error(e); console.error(e);

View file

@ -2,10 +2,18 @@ import {renderToString} from 'react-dom/server';
import {CacheOptions} from '../../types'; import {CacheOptions} from '../../types';
import {generateCacheControlHeader} from '../cache'; import {generateCacheControlHeader} from '../cache';
type Head = {
status?: number;
statusText?: string;
headers?: Record<string, any>;
};
export class ServerComponentResponse extends Response { export class ServerComponentResponse extends Response {
private wait = false; private wait = false;
private cacheOptions?: CacheOptions; private cacheOptions?: CacheOptions;
public customHead: Head | undefined;
/** /**
* Allow custom body to be a string or a Promise. * Allow custom body to be a string or a Promise.
*/ */
@ -36,6 +44,26 @@ export class ServerComponentResponse extends Response {
return generateCacheControlHeader(options); return generateCacheControlHeader(options);
} }
writeHead({status, statusText, headers}: Head = {}) {
this.customHead = this.customHead || {};
if (status) {
this.customHead.status = status;
}
if (statusText) {
this.customHead.statusText = statusText;
}
if (headers) {
this.customHead.headers = {...this.customHead.headers, ...headers};
}
}
redirect(status: number, location: string) {
this.writeHead({status, headers: {location}});
}
/** /**
* Send the response from a Server Component. Renders React components to string, * Send the response from a Server Component. Renders React components to string,
* and returns `null` to make React happy. * and returns `null` to make React happy.