Skip to content

Commit bf50eef

Browse files
committed
simplify build process by not requiring an external api anymore
1 parent acafa30 commit bf50eef

20 files changed

+376
-187
lines changed

.vscode/settings.json

Lines changed: 0 additions & 9 deletions
This file was deleted.

mock/api/index.json

Lines changed: 0 additions & 1 deletion
This file was deleted.

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"eslint-plugin-react": "^7.33.2",
5050
"file-loader": "^6.2.0",
5151
"html-webpack-plugin": "^5.5.3",
52+
"http-proxy-middleware": "^2.0.6",
5253
"identity-obj-proxy": "^3.0.0",
5354
"jest": "^29.7.0",
5455
"mini-css-extract-plugin": "^2.7.6",

readme.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,31 @@ This boilerplate is extremely minimal but gives you complete control over how yo
88

99
You can follow scripts configured in `packages.json` to serve, test, build, and prerender.
1010

11+
### Start the web app locally
12+
13+
Run
14+
15+
```sh
16+
npm start
17+
```
18+
19+
Use the `--help` option to see more options. Note when passing options via npm to the underlying program, `--` is required before those options.
20+
21+
```sh
22+
npm start -- --help
23+
```
24+
25+
#### Proxying
26+
27+
Two packages inside the `static/` directory will be served on `/data/` and `/media/` path respectively when the app is running locally.
28+
It will also be copied during the build process and become part of the final bundle.
29+
30+
For convenience, when the `--api` option is provided, the given endpoint will be proxied on the `/api/` path and become accessible during local development. e.g.
31+
32+
```sh
33+
npm start -- --api https://checkip.amazonaws.com/
34+
```
35+
1136
## Prerender support
1237

1338
Scripts are included to render a static web app from optional static APIs.

scripts/dev-server-utils.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import WebpackDevServer from "webpack-dev-server";
2+
import serveStatic from "serve-static";
3+
import { createProxyMiddleware } from "http-proxy-middleware";
4+
5+
export function createStaticMiddleware(
6+
path: string,
7+
dir: string,
8+
): WebpackDevServer.Middleware {
9+
const middleware = serveStatic(dir, {
10+
fallthrough: false,
11+
index: "index.json",
12+
});
13+
return {
14+
path,
15+
middleware,
16+
};
17+
}
18+
19+
/**
20+
* Setup services for local development by proxying requests to the given endpoint.
21+
*/
22+
export function createLocalServices(
23+
target: string,
24+
): WebpackDevServer.ExpressRequestHandler {
25+
return createProxyMiddleware({
26+
target,
27+
changeOrigin: true,
28+
pathRewrite: { "^/api": "" },
29+
});
30+
}

scripts/webpack.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ export function createAppConfig(
130130
plugins: [
131131
...(devServer ? [] : [new CleanWebpackPlugin()]),
132132
new MiniCssExtractPlugin({
133-
filename: "styles.css",
133+
filename: "styles.[contenthash].css",
134134
}),
135135
new HtmlWebpackPlugin({
136136
template: devServer ? "index.html" : "index.server.html",

src/hydrate.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { hydrateRoot } from "react-dom/client";
22
import { createApp } from "app/create";
3+
import { Bootstrap } from "types";
34
import "./styles.css";
45

56
window.addEventListener("DOMContentLoaded", () => {
67
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
7-
const bootstrap = (window as any)["bootstrap"] as { content: string };
8+
const bootstrap = (window as any)["bootstrap"] as Bootstrap;
89
/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */
910
hydrateRoot(document.getElementById("root")!, createApp(bootstrap));
1011
});

src/index.server.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<title>$title</title>
77
</head>
88
<body>
9-
<div id="root">$content</div>
9+
<div id="root"></div>
1010
<script>
1111
window["bootstrap"] = "$bootstrap";
1212
</script>

src/main.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,17 @@
11
import { createApp } from "app/create";
22
import * as React from "react";
33
import { createRoot } from "react-dom/client";
4+
import { Bootstrap } from "types";
45
import "./styles.css";
56

6-
class AppLoader extends React.Component<
7-
object,
8-
{ bootstrap?: { content: string } }
9-
> {
7+
class AppLoader extends React.Component<object, { bootstrap?: Bootstrap }> {
108
constructor(props: object) {
119
super(props);
1210
this.state = { bootstrap: undefined };
1311
}
1412

1513
componentDidMount() {
16-
fetch("api")
14+
fetch("data/")
1715
.then((res) => res.json())
1816
.then(({ content }) => {
1917
if (content) {

src/prerender.ts

Lines changed: 5 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -1,129 +1,6 @@
1-
import { createApp } from "app/create";
2-
import {
3-
existsSync,
4-
mkdirSync,
5-
readFileSync,
6-
writeFileSync,
7-
readdirSync,
8-
copyFileSync,
9-
} from "fs";
10-
import { dirname, join } from "path";
11-
import { Writable } from "stream";
12-
import * as ReactDOMServer from "react-dom/server";
13-
import yargs from "yargs";
14-
import { hideBin } from "yargs/helpers";
1+
import { prerender } from "prerenderer";
152

16-
async function parseArgs() {
17-
const args = await yargs(hideBin(process.argv))
18-
.scriptName("prerender")
19-
.usage("$0", "prerender the webapp into static files")
20-
.option("api-dir", {
21-
alias: "api",
22-
demandOption: true,
23-
describe: "path to static api files",
24-
type: "string",
25-
})
26-
.help().argv;
27-
return {
28-
api: join(process.cwd(), args["api-dir"]),
29-
};
30-
}
31-
32-
async function copyDirSync(src: string, dest: string) {
33-
mkdirSync(dest, { recursive: true });
34-
const entries = readdirSync(src, { withFileTypes: true });
35-
36-
for (const entry of entries) {
37-
const srcPath = join(src, entry.name);
38-
const destPath = join(dest, entry.name);
39-
40-
if (entry.isDirectory()) {
41-
copyDirSync(srcPath, destPath);
42-
} else {
43-
copyFileSync(srcPath, destPath);
44-
}
45-
}
46-
}
47-
48-
async function renderToString(content: React.ReactNode) {
49-
const { pipe } = ReactDOMServer.renderToPipeableStream(content);
50-
return new Promise<string>((res) => {
51-
const chunks: Buffer[] = [];
52-
pipe(
53-
new Writable({
54-
write(chunk, encoding, callback) {
55-
chunks.push(chunk);
56-
callback();
57-
},
58-
final(callback) {
59-
res(Buffer.concat(chunks).toString());
60-
callback();
61-
},
62-
}),
63-
);
64-
});
65-
}
66-
67-
// Update your prerender and path discovery logic accordingly
68-
async function prerender({ api }: { api: string }) {
69-
process.chdir(__dirname);
70-
const WEB_ROOT = join(process.cwd(), "html");
71-
const htmlTemplate = readFileSync(join(WEB_ROOT, "index.html"), "utf8");
72-
73-
// This function works with static API files for simplicity, you can extend it to make network requests to your services.
74-
function readAndExtractStaticAPI(path: string[]) {
75-
const resource = readFileSync(join(api, ...path, "index.json"), "utf8");
76-
const json = JSON.parse(resource);
77-
78-
const filename = join(WEB_ROOT, ...path, "index.json");
79-
const dir = dirname(filename);
80-
if (!existsSync(dir)) {
81-
mkdirSync(dir, { recursive: true });
82-
}
83-
writeFileSync(filename, resource);
84-
return json;
85-
}
86-
87-
async function render(bootstrap: { content: string }) {
88-
const content = await renderToString(createApp(bootstrap));
89-
return htmlTemplate
90-
.replace('"$bootstrap"', JSON.stringify(bootstrap))
91-
.replace("$content", content)
92-
.replace("$title", "Page");
93-
}
94-
95-
function write(route: string, html: string, name = "index.html") {
96-
const filename = join(WEB_ROOT, route, name);
97-
const dir = dirname(filename);
98-
if (!existsSync(dir)) {
99-
mkdirSync(dir, { recursive: true });
100-
}
101-
writeFileSync(filename, html);
102-
}
103-
104-
const startTime = new Date().getTime();
105-
106-
// collecte media
107-
console.log("collecting media files...");
108-
copyDirSync(join(api, "media"), join(WEB_ROOT, "media"));
109-
110-
// prerender
111-
const routes = ["/"];
112-
113-
for (let i = 0; i < routes.length; i++) {
114-
const route = routes[i];
115-
console.log(`Rendering ${route}`);
116-
const bootstrap = readAndExtractStaticAPI(["api"]);
117-
write(route, await render(bootstrap));
118-
}
119-
120-
console.log(`Rendering 404 page`);
121-
const bootstrap = { content: "not found 🔍" };
122-
write("/", await render(bootstrap), "404.html");
123-
124-
const timeTaken = new Date().getTime() - startTime;
125-
console.log("--------------------------------");
126-
console.log(`Prerender finished in ${timeTaken / 1000}s.`);
127-
}
128-
129-
parseArgs().then(prerender);
3+
/**
4+
* Edit `prerender` function to tailor it to more use cases.
5+
*/
6+
prerender(["/"]);

src/prerenderer/index.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { createApp } from "app/create";
2+
import { promises as fs } from "fs";
3+
import { join, dirname } from "path";
4+
import { Prerenderer } from "./prerenderer";
5+
import { renderToString } from "./render";
6+
7+
/**
8+
* Function that renders a list of routes into html files
9+
*/
10+
export async function prerender(routes: string[]) {
11+
const staticDir = join(dirname(__dirname), "static");
12+
const htmlRoot = join(__dirname, "html");
13+
const htmlTemplate = await fs.readFile(join(htmlRoot, "index.html"), "utf-8");
14+
15+
/**
16+
* Function to map routes into strings.
17+
*
18+
* Modify this function to support more capabilities such as prefetch data or adding react router.
19+
*/
20+
async function renderToHtml(route: string): Promise<string> {
21+
const bootstrap = await fs.readFile(
22+
join(htmlRoot, "data", route, "index.json"),
23+
"utf8",
24+
);
25+
const content = await renderToString(createApp(JSON.parse(bootstrap)));
26+
return htmlTemplate
27+
.replace('"$bootstrap"', bootstrap)
28+
.replace(`<div id="root"></div>`, `<div id="root">${content}</div>`)
29+
.replace("$title", "Page");
30+
}
31+
32+
const prerenderer = new Prerenderer({
33+
render: (route) => {
34+
return renderToHtml(route);
35+
},
36+
copy: [
37+
{
38+
message: "collecting media files...",
39+
from: join(staticDir, "media"),
40+
to: join(htmlRoot, "media"),
41+
},
42+
{
43+
message: "copying static data files...",
44+
from: join(staticDir, "data"),
45+
to: join(htmlRoot, "data"),
46+
override: {
47+
test(file) {
48+
return file.endsWith(".json");
49+
},
50+
async copy(src, dest) {
51+
fs.writeFile(
52+
dest,
53+
JSON.stringify(JSON.parse(await fs.readFile(src, "utf8"))),
54+
);
55+
},
56+
},
57+
},
58+
],
59+
});
60+
61+
return prerenderer.render(routes);
62+
}

0 commit comments

Comments
 (0)