Skip to content

Commit f768a56

Browse files
authored
feat: server context (#86)
* wip: context with async local storage * use examples scripts * wip: 08_cookies example with dev * remove unnecessary vite config files * fix lint * hack to make store stable * example/08 start script * follow new example structure * fix types * add unstable prefix * example/08 src * fix examples/08 scripts * disable ssr for examples/08
1 parent df6c88d commit f768a56

19 files changed

+411
-55
lines changed

.eslintrc.json

+7
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@
2828
"rules": {
2929
"@typescript-eslint/no-var-requires": "off"
3030
}
31+
},
32+
{
33+
"files": ["examples/08_cookies/*.js"],
34+
"env": { "node": true },
35+
"rules": {
36+
"import/no-named-as-default-member": "off"
37+
}
3138
}
3239
]
3340
}

examples/08_cookies/dev.js

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import express from "express";
2+
import cookieParser from "cookie-parser";
3+
import {
4+
rsc,
5+
//ssr,
6+
devServer,
7+
} from "waku";
8+
9+
const app = express();
10+
app.use(cookieParser());
11+
app.use(
12+
rsc({
13+
command: "dev",
14+
unstable_prehook: (req) => {
15+
return { count: Number(req.cookies.count) || 0 };
16+
},
17+
unstable_posthook: (req, res, ctx) => {
18+
res.cookie("count", String(ctx.count));
19+
},
20+
})
21+
);
22+
// Passing cookies through SSR server isn't supported (yet).
23+
// app.use(ssr({ command: "dev" }));
24+
app.use(devServer());
25+
26+
const port = process.env.PORT || 3000;
27+
app.listen(port, () => {
28+
console.info("Listening on", port);
29+
});

examples/08_cookies/package.json

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"name": "waku-example",
3+
"version": "0.1.0",
4+
"type": "module",
5+
"private": true,
6+
"scripts": {
7+
"dev": "node dev.js",
8+
"build": "waku build",
9+
"start": "node start.js"
10+
},
11+
"dependencies": {
12+
"cookie-parser": "^1.4.6",
13+
"express": "^4.18.2",
14+
"react": "18.3.0-canary-613e6f5fc-20230616",
15+
"react-dom": "18.3.0-canary-613e6f5fc-20230616",
16+
"react-server-dom-webpack": "18.3.0-canary-613e6f5fc-20230616",
17+
"waku": "0.12.1"
18+
},
19+
"devDependencies": {
20+
"@types/react": "^18.2.8",
21+
"@types/react-dom": "^18.2.4",
22+
"typescript": "^5.1.3"
23+
}
24+
}
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { getContext } from "waku/server";
2+
import { Counter } from "./Counter.js";
3+
4+
const App = ({ name = "Anonymous" }) => {
5+
const ctx = getContext<{ count: number }>();
6+
return (
7+
<div style={{ border: "3px red dashed", margin: "1em", padding: "1em" }}>
8+
<h1>Hello {name}!!</h1>
9+
<h3>This is a server component.</h3>
10+
<p>Cookie count: {ctx.count}</p>
11+
<Counter />
12+
</div>
13+
);
14+
};
15+
16+
export default App;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
5+
export const Counter = () => {
6+
const [count, setCount] = useState(0);
7+
return (
8+
<div style={{ border: "3px blue dashed", margin: "1em", padding: "1em" }}>
9+
<p>Count: {count}</p>
10+
<button onClick={() => setCount((c) => c + 1)}>Increment</button>
11+
<h3>This is a client component.</h3>
12+
</div>
13+
);
14+
};

examples/08_cookies/src/entries.ts

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { defineEntries, getContext } from "waku/server";
2+
3+
export default defineEntries(
4+
// getEntry
5+
async (id) => {
6+
const ctx = getContext<{ count: number }>();
7+
++ctx.count;
8+
switch (id) {
9+
case "App":
10+
return import("./components/App.js");
11+
default:
12+
return null;
13+
}
14+
},
15+
// getBuildConfig
16+
async () => {
17+
return {
18+
"/": {
19+
elements: [["App", { name: "Waku" }]],
20+
ctx: { count: 0 },
21+
},
22+
};
23+
}
24+
// getSsrConfig
25+
// Passing cookies through SSR server isn't supported (yet).
26+
// async (pathStr) => {
27+
// switch (pathStr) {
28+
// case "/":
29+
// return { element: ["App", { name: "Waku" }] };
30+
// default:
31+
// return null;
32+
// }
33+
// }
34+
);

examples/08_cookies/src/index.html

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<title>Waku example</title>
6+
<style>
7+
@keyframes spinner {
8+
to {
9+
transform: rotate(360deg);
10+
}
11+
}
12+
.spinner {
13+
width: 36px;
14+
height: 36px;
15+
margin: auto;
16+
border: 2px solid #ddd;
17+
border-top-color: #222;
18+
border-radius: 50%;
19+
animation: spinner 1s linear infinite;
20+
}
21+
#root > .spinner {
22+
margin-top: calc(50% - 18px);
23+
}
24+
</style>
25+
</head>
26+
<body>
27+
<!--placeholder1-->
28+
<div id="root">
29+
<div class="spinner"></div>
30+
</div>
31+
<!--/placeholder1-->
32+
<script src="/main.tsx" async type="module"></script>
33+
<!--placeholder2-->
34+
<!--/placeholder2-->
35+
</body>
36+
</html>

examples/08_cookies/src/main.tsx

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { StrictMode } from "react";
2+
import { createRoot } from "react-dom/client";
3+
import { serve } from "waku/client";
4+
5+
const App = serve<{ name: string }>("App");
6+
const rootElement = (
7+
<StrictMode>
8+
<App name="Waku" />
9+
</StrictMode>
10+
);
11+
12+
createRoot(document.getElementById("root")!).render(rootElement);

examples/08_cookies/start.js

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import url from "node:url";
2+
import path from "node:path";
3+
import express from "express";
4+
import cookieParser from "cookie-parser";
5+
import {
6+
rsc,
7+
// ssr,
8+
} from "waku";
9+
10+
const root = path.join(
11+
path.dirname(url.fileURLToPath(import.meta.url)),
12+
"dist"
13+
);
14+
process.env.CONFIG_FILE = "vite.prd.config.ts";
15+
16+
const app = express();
17+
app.use(cookieParser());
18+
app.use(
19+
rsc({
20+
command: "start",
21+
unstable_prehook: (req) => {
22+
return { count: Number(req.cookies.count) || 0 };
23+
},
24+
unstable_posthook: (req, res, ctx) => {
25+
res.cookie("count", String(ctx.count));
26+
},
27+
})
28+
);
29+
// Passing cookies through SSR server isn't supported (yet).
30+
// app.use(ssr({ command: "start" }));
31+
app.use(express.static(path.join(root, "public")));
32+
express.static.mime.default_type = "";
33+
34+
const port = process.env.PORT || 8080;
35+
app.listen(port, () => {
36+
console.info("Listening on", port);
37+
});

examples/08_cookies/tsconfig.json

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"compilerOptions": {
3+
"strict": true,
4+
"target": "esnext",
5+
"downlevelIteration": true,
6+
"esModuleInterop": true,
7+
"module": "nodenext",
8+
"skipLibCheck": true,
9+
"noUncheckedIndexedAccess": true,
10+
"exactOptionalPropertyTypes": true,
11+
"jsx": "react-jsx"
12+
}
13+
}

package.json

+3
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
"examples:dev:05_mutation": "NAME=05_mutation npm run examples:dev",
6262
"examples:dev:06_nesting": "NAME=06_nesting npm run examples:dev",
6363
"examples:dev:07_router": "NAME=07_router npm run examples:dev",
64+
"examples:dev:08_cookies": "NAME=08_cookies npm run examples:dev",
6465
"examples:build": "npm run compile:code && (cd ./examples/${NAME} && npm run build)",
6566
"examples:prd": "npm run examples:build && (cd ./examples/${NAME} && npm start)",
6667
"examples:prd:01_counter": "NAME=01_counter npm run examples:prd",
@@ -70,6 +71,7 @@
7071
"examples:prd:05_mutation": "NAME=05_mutation npm run examples:prd",
7172
"examples:prd:06_nesting": "NAME=06_nesting npm run examples:prd",
7273
"examples:prd:07_router": "NAME=07_router npm run examples:prd",
74+
"examples:prd:08_cookies": "NAME=08_cookies npm run examples:prd",
7375
"website:dev": "npm run compile:code && ./dist/cli.js dev --config ./website/vite.config.ts",
7476
"website:build": "npm run compile:code && ./dist/cli.js build --config ./website/vite.config.ts",
7577
"website:vercel": "npm run website:build && cp -Lr ./website/dist/.vercel/output ./.vercel/",
@@ -99,6 +101,7 @@
99101
"@typescript-eslint/parser": "^5.59.11",
100102
"autoprefixer": "^10.4.14",
101103
"bright": "^0.8.2",
104+
"cookie-parser": "^1.4.6",
102105
"eslint": "^8.43.0",
103106
"eslint-config-prettier": "^8.8.0",
104107
"eslint-import-resolver-typescript": "^3.5.5",

pnpm-lock.yaml

+45
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/lib/builder.ts

+11-6
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ const emitRscFiles = async (
237237
};
238238
const rscFileSet = new Set<string>(); // XXX could be implemented better
239239
await Promise.all(
240-
Object.entries(buildConfig).map(async ([, { elements }]) => {
240+
Object.entries(buildConfig).map(async ([, { elements, ctx }]) => {
241241
for (const [rscId, props] of elements || []) {
242242
// FIXME we blindly expect JSON.stringify usage is deterministic
243243
const serializedProps = JSON.stringify(props);
@@ -253,10 +253,11 @@ const emitRscFiles = async (
253253
if (!rscFileSet.has(destFile)) {
254254
rscFileSet.add(destFile);
255255
fs.mkdirSync(path.dirname(destFile), { recursive: true });
256-
const pipeable = renderRSC(
256+
const [pipeable] = await renderRSC(
257257
{ rscId, props },
258258
{
259259
command: "build",
260+
ctx,
260261
moduleIdCallback: (id) =>
261262
addClientModule(rscId, serializedProps, id),
262263
}
@@ -277,15 +278,19 @@ const emitRscFiles = async (
277278
const renderHtml = async (
278279
config: Awaited<ReturnType<typeof resolveConfig>>,
279280
pathStr: string,
280-
htmlStr: string
281+
htmlStr: string,
282+
ctx: unknown
281283
) => {
282284
const ssrConfig = await getSsrConfigRSC(pathStr, "build");
283285
if (!ssrConfig) {
284286
return null;
285287
}
286288
const { splitHTML, getFallback } = config.framework.ssr;
287289
const [rscId, props] = ssrConfig.element;
288-
const pipeable = renderRSC({ rscId, props }, { command: "build" });
290+
const [pipeable] = await renderRSC(
291+
{ rscId, props },
292+
{ command: "build", ctx }
293+
);
289294
return renderHtmlToReadable(htmlStr, pipeable, splitHTML, getFallback);
290295
};
291296

@@ -306,7 +311,7 @@ const emitHtmlFiles = async (
306311
});
307312
const htmlFiles = await Promise.all(
308313
Object.entries(buildConfig).map(
309-
async ([pathStr, { elements, customCode, skipSsr }]) => {
314+
async ([pathStr, { elements, customCode, ctx, skipSsr }]) => {
310315
const destFile = path.join(
311316
config.root,
312317
config.framework.distDir,
@@ -346,7 +351,7 @@ const emitHtmlFiles = async (
346351
data = data.replace(/<\/head>/, `<script>${code}</script></head>`);
347352
}
348353
const htmlReadable =
349-
!skipSsr && (await renderHtml(config, pathStr, data));
354+
!skipSsr && (await renderHtml(config, pathStr, data, ctx));
350355
if (htmlReadable) {
351356
await new Promise<void>((resolve, reject) => {
352357
const stream = fs.createWriteStream(destFile);

0 commit comments

Comments
 (0)