-
Notifications
You must be signed in to change notification settings - Fork 28
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
11 changed files
with
314 additions
and
41 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,33 +1,62 @@ | ||
/** | ||
* In webpack terminology the 'entry point' | ||
* The 'entry point' (in webpack terminology) | ||
* of the First SPA. | ||
* | ||
* SSR has been disabled for this entry point. | ||
* To enable SSR: | ||
* - uncomment import of renderToString | ||
* - replace ReactDOM.render with ReactDOM.hydrate (see comments below), | ||
* - uncomment the SSR block at the bottom. | ||
*/ | ||
import * as React from "react"; | ||
import * as ReactDOM from "react-dom"; | ||
import { Helmet } from "react-helmet"; | ||
import { BrowserRouter as Router, Route, Switch } from "react-router-dom"; | ||
import { Router, Route, Switch } from "react-router-dom"; | ||
import { ComponentA } from "../components/ComponentA"; | ||
import { ComponentB } from "../components/ComponentB"; | ||
import { Overview } from "../components/Overview"; | ||
import { NameLookup } from "../components/NameLookup"; | ||
import { ErrorBoundary } from "../components/ErrorBoundary"; | ||
// import { renderToString } from "react-dom/server"; // used for SSR | ||
import * as SPAs from "../../config/spa.config"; | ||
import { isServer, getHistory } from "../utils/ssr/misc"; | ||
|
||
ReactDOM.render( | ||
<Router> | ||
<ErrorBoundary> | ||
<Helmet title={SPAs.appTitle} /> | ||
<div style={{ textAlign: "center", marginTop: "2rem", marginBottom: "3rem" }}> | ||
<h2>Welcome to {SPAs.appTitle}</h2> | ||
</div> | ||
<Switch> | ||
<Route exact path="/" component={Overview} /> | ||
<Route path="/a" component={ComponentA} /> | ||
<Route path="/b" component={ComponentB} /> | ||
<Route path="/namelookup" component={NameLookup} /> | ||
<Route component={Overview} /> | ||
</Switch> | ||
</ErrorBoundary> | ||
</Router>, | ||
document.getElementById("react-root") | ||
); | ||
const First: React.FC = _props => { | ||
return ( | ||
<> | ||
<Router history={getHistory()}> | ||
<ErrorBoundary> | ||
<Helmet title={SPAs.appTitle} /> | ||
<div style={{ textAlign: "center", marginTop: "2rem", marginBottom: "3rem" }}> | ||
<h2>Welcome to {SPAs.appTitle}</h2> | ||
</div> | ||
<Switch> | ||
<Route exact path="/" component={Overview} /> | ||
<Route path="/a" component={ComponentA} /> | ||
<Route path="/b" component={ComponentB} /> | ||
<Route path="/namelookup" component={NameLookup} /> | ||
<Route component={Overview} /> | ||
</Switch> | ||
</ErrorBoundary> | ||
</Router> | ||
</> | ||
) | ||
}; | ||
|
||
if (!isServer()) { | ||
ReactDOM.render( // .render(...) is used without SSR | ||
// ReactDOM.hydrate( // .hydrate(...) is used with SSR | ||
<First />, | ||
document.getElementById("react-root") | ||
); | ||
} | ||
|
||
/****************** SSR block start ******************/ | ||
/* | ||
const asString = () => { | ||
return renderToString(<First />) | ||
} | ||
export default asString; | ||
*/ | ||
/****************** SSR block end ******************/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,21 +1,51 @@ | ||
/** | ||
* In webpack terminology the 'entry point' | ||
* The 'entry point' (in webpack terminology) | ||
* of the Second SPA. | ||
* | ||
* SSR has been enabled for this entry point. | ||
* To disable SSR: | ||
* - comment out import of renderToString | ||
* - replace ReactDOM.hydrate with ReactDOM.render (see comments below), | ||
* - comment out the SSR block at the bottom. | ||
*/ | ||
import * as React from "react"; | ||
import * as ReactDOM from "react-dom"; | ||
import { Helmet } from "react-helmet"; | ||
import { ComponentC } from "../components/ComponentC"; | ||
import { ErrorBoundary } from "../components/ErrorBoundary"; | ||
import { renderToString } from "react-dom/server"; | ||
import * as SPAs from "../../config/spa.config"; | ||
import { isServer } from "../utils/ssr/misc"; | ||
|
||
const Second: React.FC = _props => { | ||
return ( | ||
<> | ||
<ErrorBoundary> | ||
<Helmet title={SPAs.appTitle} /> | ||
<div style={{ textAlign: "center", marginTop: "2rem", marginBottom: "3rem" }}> | ||
<h2>Welcome to {SPAs.appTitle}</h2> | ||
</div> | ||
<ComponentC /> | ||
</ErrorBoundary> | ||
</> | ||
) | ||
}; | ||
|
||
if (!isServer()) { | ||
// ReactDOM.render( // .render(...) is used without SSR | ||
ReactDOM.hydrate( // .hydrate(...) is used with SSR | ||
<Second />, | ||
document.getElementById("react-root") | ||
); | ||
} | ||
|
||
/****************** SSR block start ******************/ | ||
|
||
const asString = () => { | ||
return renderToString(<Second />) | ||
} | ||
|
||
export default asString; | ||
|
||
/****************** SSR block end ******************/ | ||
|
||
ReactDOM.render( | ||
<ErrorBoundary> | ||
<Helmet title={SPAs.appTitle} /> | ||
<div style={{ textAlign: "center", marginTop: "2rem", marginBottom: "3rem" }}> | ||
<h2>Welcome to {SPAs.appTitle}</h2> | ||
</div> | ||
<ComponentC /> | ||
</ErrorBoundary>, | ||
document.getElementById("react-root") | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import * as fs from "fs"; | ||
import { promisify } from "util"; | ||
import { postProcess } from "./postProcess"; | ||
|
||
export async function renderToString() { | ||
type SSRTuple = [string, () => string]; | ||
type SSRArray = Array<SSRTuple>; | ||
|
||
const ar: SSRArray = new Array(); | ||
|
||
const getEntrypoints = require("../../../config/spa.config").getEntrypoints; | ||
|
||
for (const [key, value] of Object.entries(getEntrypoints())) { | ||
const ssrFileName = `${key}-SSR.txt`; | ||
const entryPointPath = (value as string).replace(/^\.\/src/, "../..").replace(/\.\w+$/, ""); | ||
const { default: renderAsString } = await import(entryPointPath); | ||
!!renderAsString && ar.push([ssrFileName, renderAsString] as SSRTuple); | ||
} | ||
|
||
const writeFile = promisify(fs.writeFile); | ||
|
||
try { | ||
await Promise.all(ar.map(entry => { | ||
return writeFile('./dist/' + entry[0], entry[1]()); | ||
})); | ||
await postProcess(); | ||
} catch (e) { | ||
// Using console at build time is acceptable. | ||
// tslint:disable-next-line:no-console | ||
console.error(`Failed to create pre-built SSR file, exception: ${e}`); | ||
process.exit(1); | ||
} | ||
}; | ||
|
||
renderToString().catch(e => { | ||
// Using console at build time is acceptable. | ||
// tslint:disable-next-line:no-console | ||
console.error(`SSR processing failed, error: ${e}`); | ||
process.exit(2); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { createMemoryHistory, createBrowserHistory } from "history"; | ||
|
||
export const isServer = () => { | ||
return typeof window === 'undefined' | ||
} | ||
|
||
// https://stackoverflow.com/a/51511967/12005425 | ||
export const getHistory = (url = '/') => { | ||
const history = isServer() ? | ||
createMemoryHistory({ | ||
initialEntries: [url] | ||
}) : createBrowserHistory(); | ||
|
||
return history; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import * as fs from 'fs'; | ||
import * as path from 'path'; | ||
import { promisify } from 'util'; | ||
|
||
const workDir = './dist/'; | ||
|
||
export async function postProcess(): Promise<void> { | ||
const readdir = promisify(fs.readdir); | ||
const files = await readdir(workDir); | ||
const txtFiles = files.filter(file => path.extname(file) === '.txt'); | ||
const htmlFiles = files.filter(file => path.extname(file) === '.html'); | ||
const ar = new Array<[string, string]>(); | ||
|
||
htmlFiles.forEach(file => { | ||
const fileFound = txtFiles.find(txt => txt.startsWith(file.replace(/\.[^/.]+$/, ""))); | ||
if (fileFound) { | ||
ar.push([file, fileFound]); | ||
} | ||
}); | ||
|
||
await Promise.all(ar.map(([k, v]) => { | ||
return postProcessFile(k, v); | ||
})); | ||
|
||
// Using console at build time is acceptable. | ||
// tslint:disable-next-line:no-console | ||
console.log("Finished SSR post-processing") | ||
} | ||
|
||
async function postProcessFile(htmlFile: string, ssrFile: string): Promise<void> { | ||
const readFile = promisify(fs.readFile); | ||
const htmlFilePath = path.join(workDir, htmlFile); | ||
const ssrFilePath = path.join(workDir, ssrFile); | ||
|
||
const dataHtml = await readFile(htmlFilePath); | ||
const dataSsr = (await readFile(ssrFilePath)).toString(); | ||
const reReact = /^\s*<div\s+id="react-root">/; | ||
const ar: string[] = dataHtml.toString().replace(/\r\n?/g, '\n').split('\n'); | ||
|
||
const out = ar.map(str => { | ||
if (reReact.test(str)) { | ||
str += '\n'; | ||
str += dataSsr; | ||
} | ||
str += '\n'; | ||
return str; | ||
}); | ||
|
||
const stream = fs.createWriteStream(htmlFilePath); | ||
stream.on('error', err => { | ||
// Using console at build time is acceptable. | ||
// tslint:disable-next-line:no-console | ||
console.error(`Failed to write to file ${htmlFilePath}, error: ${err}`) | ||
}); | ||
out.forEach(str => { stream.write(str); }); | ||
stream.end(); | ||
} |
Oops, something went wrong.