waku 0.12.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -29,9 +29,8 @@ pnpm create waku # pnpm not working for now
29
29
  - [x] `waku start` (production server)
30
30
  - [x] Exportable build
31
31
  - [x] Static site generation
32
- - [ ] Client rendering fallback
33
32
  - [x] Opt-in router (reference implementation)
34
- - [ ] Opt-in SSR (HTML generation & hydration)
33
+ - [x] Opt-in SSR (HTML generation only)
35
34
 
36
35
  ## Tweets
37
36
 
@@ -72,6 +71,7 @@ pnpm create waku # pnpm not working for now
72
71
  - https://twitter.com/dai_shi/status/1664286329763684353
73
72
  - https://twitter.com/dai_shi/status/1664989534889861123
74
73
  - https://twitter.com/dai_shi/status/1667545252654366721
74
+ - https://twitter.com/dai_shi/status/1670650381762961408
75
75
 
76
76
  </details>
77
77
 
@@ -272,7 +272,7 @@ const emitHtmlFiles = async (config, buildConfig, getClientModules)=>{
272
272
  const code = (0, _utils.generatePrefetchCode)(basePrefix, elementsForPrefetch, moduleIdsForPrefetch) + (customCode || "");
273
273
  if (code) {
274
274
  // HACK is this too naive to inject script code?
275
- data = data.replace(/<\/body>/, `<script>${code}</script></body>`);
275
+ data = data.replace(/<\/head>/, `<script>${code}</script></head>`);
276
276
  }
277
277
  const htmlReadable = !skipSsr && await renderHtml(config, pathStr, data);
278
278
  if (htmlReadable) {
@@ -18,16 +18,21 @@ _export(exports, {
18
18
  });
19
19
  const _vite = require("vite");
20
20
  const splitHTML = (htmlStr)=>{
21
- const startStr = "<!--placeholder-->";
22
- const endStr = "<!--/placeholder-->";
23
- const splitted = htmlStr.split(new RegExp(startStr + "[\\s\\S]*" + endStr));
24
- if (splitted.length !== 2) {
21
+ const P1 = [
22
+ "<!--placeholder1-->\\s*<div[^>]*>",
23
+ "</div>\\s*<!--/placeholder1-->"
24
+ ];
25
+ const P2 = [
26
+ "<!--placeholder2-->",
27
+ "<!--/placeholder2-->"
28
+ ];
29
+ const anyRE = "[\\s\\S]*";
30
+ const match = htmlStr.match(new RegExp(// prettier-ignore
31
+ "^(" + anyRE + P1[0] + ")" + anyRE + "(" + P1[1] + anyRE + P2[0] + ")" + anyRE + "(" + P2[1] + anyRE + ")$"));
32
+ if (match?.length !== 1 + 3) {
25
33
  throw new Error("Failed to split HTML");
26
34
  }
27
- return [
28
- splitted[0] + startStr,
29
- endStr + splitted[1]
30
- ];
35
+ return match.slice(1);
31
36
  };
32
37
  const getFallback = (id)=>{
33
38
  if (id.endsWith("#Waku_SSR_Capable_Link")) {
@@ -19,6 +19,27 @@ function _interop_require_default(obj) {
19
19
  // eslint-disable-next-line import/no-named-as-default-member
20
20
  const { renderToPipeableStream } = _server.default;
21
21
  const { createFromNodeStream } = _clientnodeunbundled.default;
22
+ const interleaveHtmlSnippets = (preamble, intermediate, postamble)=>{
23
+ let initialized = false;
24
+ return new _nodestream.Transform({
25
+ transform (chunk, encoding, callback) {
26
+ if (encoding !== "buffer") {
27
+ throw new Error("Unknown encoding");
28
+ }
29
+ if (!initialized) {
30
+ initialized = true;
31
+ const data = chunk.toString();
32
+ callback(null, preamble + data + intermediate);
33
+ } else {
34
+ callback(null, chunk);
35
+ }
36
+ },
37
+ final (callback) {
38
+ this.push(postamble);
39
+ callback();
40
+ }
41
+ });
42
+ };
22
43
  const renderHtmlToReadable = (htmlStr, rscStream, splitHTML, getFallback)=>{
23
44
  const bundlerConfig = new Proxy({}, {
24
45
  get (_target, filePath) {
@@ -34,16 +55,8 @@ const renderHtmlToReadable = (htmlStr, rscStream, splitHTML, getFallback)=>{
34
55
  });
35
56
  }
36
57
  });
37
- const passthrough = new _nodestream.PassThrough();
38
- const [preamble, postamble] = splitHTML(htmlStr);
39
- passthrough.write(preamble, "utf8");
40
58
  const data = createFromNodeStream(rscStream, bundlerConfig);
41
- const origEnd = passthrough.end;
42
- passthrough.end = (...args)=>{
43
- passthrough.write(postamble, "utf8");
44
- return origEnd.apply(passthrough, args); // FIXME how to avoid any?
45
- };
46
- renderToPipeableStream(data, {
59
+ return renderToPipeableStream(data, {
47
60
  onError (err) {
48
61
  if (err instanceof Error && err.message.startsWith("Client-only component")) {
49
62
  // ignore
@@ -51,6 +64,5 @@ const renderHtmlToReadable = (htmlStr, rscStream, splitHTML, getFallback)=>{
51
64
  }
52
65
  console.error(err);
53
66
  }
54
- }).pipe(passthrough);
55
- return passthrough;
67
+ }).pipe(interleaveHtmlSnippets(...splitHTML(htmlStr)));
56
68
  };
@@ -17,7 +17,7 @@ function rscIndexPlugin() {
17
17
  {
18
18
  tag: "script",
19
19
  children: _utils.codeToInject,
20
- injectTo: "body"
20
+ injectTo: "head"
21
21
  }
22
22
  ];
23
23
  }
package/dist/config.d.ts CHANGED
@@ -6,7 +6,7 @@ export interface FrameworkConfig {
6
6
  rscPrefix?: string;
7
7
  ssr?: {
8
8
  rscServer?: string;
9
- splitHTML?: (htmlStr: string) => readonly [string, string];
9
+ splitHTML?: (htmlStr: string) => readonly [string, string, string];
10
10
  getFallback?: (id: string) => string;
11
11
  };
12
12
  }
@@ -257,7 +257,7 @@ const emitHtmlFiles = async (config, buildConfig, getClientModules)=>{
257
257
  const code = generatePrefetchCode(basePrefix, elementsForPrefetch, moduleIdsForPrefetch) + (customCode || "");
258
258
  if (code) {
259
259
  // HACK is this too naive to inject script code?
260
- data = data.replace(/<\/body>/, `<script>${code}</script></body>`);
260
+ data = data.replace(/<\/head>/, `<script>${code}</script></head>`);
261
261
  }
262
262
  const htmlReadable = !skipSsr && await renderHtml(config, pathStr, data);
263
263
  if (htmlReadable) {
@@ -11,7 +11,7 @@ export declare function resolveConfig(command: "build" | "serve"): Promise<{
11
11
  rscPrefix: string;
12
12
  ssr: {
13
13
  rscServer: string;
14
- splitHTML: (htmlStr: string) => readonly [string, string];
14
+ splitHTML: (htmlStr: string) => readonly [string, string, string];
15
15
  getFallback: (id: string) => string;
16
16
  };
17
17
  };
@@ -1,15 +1,20 @@
1
1
  import { resolveConfig as viteResolveConfig } from "vite";
2
2
  const splitHTML = (htmlStr)=>{
3
- const startStr = "<!--placeholder-->";
4
- const endStr = "<!--/placeholder-->";
5
- const splitted = htmlStr.split(new RegExp(startStr + "[\\s\\S]*" + endStr));
6
- if (splitted.length !== 2) {
3
+ const P1 = [
4
+ "<!--placeholder1-->\\s*<div[^>]*>",
5
+ "</div>\\s*<!--/placeholder1-->"
6
+ ];
7
+ const P2 = [
8
+ "<!--placeholder2-->",
9
+ "<!--/placeholder2-->"
10
+ ];
11
+ const anyRE = "[\\s\\S]*";
12
+ const match = htmlStr.match(new RegExp(// prettier-ignore
13
+ "^(" + anyRE + P1[0] + ")" + anyRE + "(" + P1[1] + anyRE + P2[0] + ")" + anyRE + "(" + P2[1] + anyRE + ")$"));
14
+ if (match?.length !== 1 + 3) {
7
15
  throw new Error("Failed to split HTML");
8
16
  }
9
- return [
10
- splitted[0] + startStr,
11
- endStr + splitted[1]
12
- ];
17
+ return match.slice(1);
13
18
  };
14
19
  const getFallback = (id)=>{
15
20
  if (id.endsWith("#Waku_SSR_Capable_Link")) {
@@ -1,2 +1,2 @@
1
1
  import type { Readable } from "node:stream";
2
- export declare const renderHtmlToReadable: (htmlStr: string, rscStream: Readable, splitHTML: (htmlStr: string) => readonly [string, string], getFallback: (id: string) => string) => Readable;
2
+ export declare const renderHtmlToReadable: (htmlStr: string, rscStream: Readable, splitHTML: (htmlStr: string) => readonly [string, string, string], getFallback: (id: string) => string) => Readable;
@@ -1,9 +1,30 @@
1
- import { PassThrough } from "node:stream";
1
+ import { Transform } from "node:stream";
2
2
  import RDServer from "react-dom/server";
3
3
  import RSDWClient from "react-server-dom-webpack/client.node.unbundled";
4
4
  // eslint-disable-next-line import/no-named-as-default-member
5
5
  const { renderToPipeableStream } = RDServer;
6
6
  const { createFromNodeStream } = RSDWClient;
7
+ const interleaveHtmlSnippets = (preamble, intermediate, postamble)=>{
8
+ let initialized = false;
9
+ return new Transform({
10
+ transform (chunk, encoding, callback) {
11
+ if (encoding !== "buffer") {
12
+ throw new Error("Unknown encoding");
13
+ }
14
+ if (!initialized) {
15
+ initialized = true;
16
+ const data = chunk.toString();
17
+ callback(null, preamble + data + intermediate);
18
+ } else {
19
+ callback(null, chunk);
20
+ }
21
+ },
22
+ final (callback) {
23
+ this.push(postamble);
24
+ callback();
25
+ }
26
+ });
27
+ };
7
28
  export const renderHtmlToReadable = (htmlStr, rscStream, splitHTML, getFallback)=>{
8
29
  const bundlerConfig = new Proxy({}, {
9
30
  get (_target, filePath) {
@@ -19,16 +40,8 @@ export const renderHtmlToReadable = (htmlStr, rscStream, splitHTML, getFallback)
19
40
  });
20
41
  }
21
42
  });
22
- const passthrough = new PassThrough();
23
- const [preamble, postamble] = splitHTML(htmlStr);
24
- passthrough.write(preamble, "utf8");
25
43
  const data = createFromNodeStream(rscStream, bundlerConfig);
26
- const origEnd = passthrough.end;
27
- passthrough.end = (...args)=>{
28
- passthrough.write(postamble, "utf8");
29
- return origEnd.apply(passthrough, args); // FIXME how to avoid any?
30
- };
31
- renderToPipeableStream(data, {
44
+ return renderToPipeableStream(data, {
32
45
  onError (err) {
33
46
  if (err instanceof Error && err.message.startsWith("Client-only component")) {
34
47
  // ignore
@@ -36,6 +49,5 @@ export const renderHtmlToReadable = (htmlStr, rscStream, splitHTML, getFallback)
36
49
  }
37
50
  console.error(err);
38
51
  }
39
- }).pipe(passthrough);
40
- return passthrough;
52
+ }).pipe(interleaveHtmlSnippets(...splitHTML(htmlStr)));
41
53
  };
@@ -8,7 +8,7 @@ export function rscIndexPlugin() {
8
8
  {
9
9
  tag: "script",
10
10
  children: codeToInject,
11
- injectTo: "body"
11
+ injectTo: "head"
12
12
  }
13
13
  ];
14
14
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "waku",
3
3
  "description": "Minimalistic React Framework",
4
- "version": "0.12.0",
4
+ "version": "0.12.1",
5
5
  "type": "module",
6
6
  "author": "Daishi Kato",
7
7
  "repository": {
package/src/config.ts CHANGED
@@ -7,7 +7,7 @@ export interface FrameworkConfig {
7
7
  rscPrefix?: string; // defaults to "RSC/"
8
8
  ssr?: {
9
9
  rscServer?: string;
10
- splitHTML?: (htmlStr: string) => readonly [string, string];
10
+ splitHTML?: (htmlStr: string) => readonly [string, string, string];
11
11
  getFallback?: (id: string) => string;
12
12
  };
13
13
  }
@@ -332,7 +332,7 @@ const emitHtmlFiles = async (
332
332
  ) + (customCode || "");
333
333
  if (code) {
334
334
  // HACK is this too naive to inject script code?
335
- data = data.replace(/<\/body>/, `<script>${code}</script></body>`);
335
+ data = data.replace(/<\/head>/, `<script>${code}</script></head>`);
336
336
  }
337
337
  const htmlReadable =
338
338
  !skipSsr && (await renderHtml(config, pathStr, data));
package/src/lib/config.ts CHANGED
@@ -8,14 +8,23 @@ type DeepRequired<T> = T extends (...args: any[]) => any
8
8
  ? { [P in keyof T]-?: DeepRequired<T[P]> }
9
9
  : T;
10
10
 
11
- const splitHTML = (htmlStr: string): readonly [string, string] => {
12
- const startStr = "<!--placeholder-->";
13
- const endStr = "<!--/placeholder-->";
14
- const splitted = htmlStr.split(new RegExp(startStr + "[\\s\\S]*" + endStr));
15
- if (splitted.length !== 2) {
11
+ const splitHTML = (htmlStr: string): readonly [string, string, string] => {
12
+ const P1 = [
13
+ "<!--placeholder1-->\\s*<div[^>]*>",
14
+ "</div>\\s*<!--/placeholder1-->",
15
+ ] as const;
16
+ const P2 = ["<!--placeholder2-->", "<!--/placeholder2-->"] as const;
17
+ const anyRE = "[\\s\\S]*";
18
+ const match = htmlStr.match(
19
+ new RegExp(
20
+ // prettier-ignore
21
+ "^(" + anyRE + P1[0] + ")" + anyRE + "(" + P1[1] + anyRE + P2[0] + ")" + anyRE + "(" + P2[1] + anyRE + ")$"
22
+ )
23
+ );
24
+ if (match?.length !== 1 + 3) {
16
25
  throw new Error("Failed to split HTML");
17
26
  }
18
- return [splitted[0] + startStr, endStr + splitted[1]];
27
+ return match.slice(1) as [string, string, string];
19
28
  };
20
29
 
21
30
  const getFallback = (id: string) => {
@@ -1,4 +1,4 @@
1
- import { PassThrough } from "node:stream";
1
+ import { Transform } from "node:stream";
2
2
  import type { Readable } from "node:stream";
3
3
 
4
4
  import RDServer from "react-dom/server";
@@ -8,10 +8,36 @@ import RSDWClient from "react-server-dom-webpack/client.node.unbundled";
8
8
  const { renderToPipeableStream } = RDServer;
9
9
  const { createFromNodeStream } = RSDWClient;
10
10
 
11
+ const interleaveHtmlSnippets = (
12
+ preamble: string,
13
+ intermediate: string,
14
+ postamble: string
15
+ ) => {
16
+ let initialized = false;
17
+ return new Transform({
18
+ transform(chunk, encoding, callback) {
19
+ if (encoding !== ("buffer" as any)) {
20
+ throw new Error("Unknown encoding");
21
+ }
22
+ if (!initialized) {
23
+ initialized = true;
24
+ const data = chunk.toString();
25
+ callback(null, preamble + data + intermediate);
26
+ } else {
27
+ callback(null, chunk);
28
+ }
29
+ },
30
+ final(callback) {
31
+ this.push(postamble);
32
+ callback();
33
+ },
34
+ });
35
+ };
36
+
11
37
  export const renderHtmlToReadable = (
12
38
  htmlStr: string, // Hope stream works, but it'd be too tricky
13
39
  rscStream: Readable,
14
- splitHTML: (htmlStr: string) => readonly [string, string],
40
+ splitHTML: (htmlStr: string) => readonly [string, string, string],
15
41
  getFallback: (id: string) => string
16
42
  ): Readable => {
17
43
  const bundlerConfig = new Proxy(
@@ -31,16 +57,8 @@ export const renderHtmlToReadable = (
31
57
  },
32
58
  }
33
59
  );
34
- const passthrough = new PassThrough();
35
- const [preamble, postamble] = splitHTML(htmlStr);
36
- passthrough.write(preamble, "utf8");
37
60
  const data = createFromNodeStream(rscStream, bundlerConfig);
38
- const origEnd = passthrough.end;
39
- passthrough.end = (...args) => {
40
- passthrough.write(postamble, "utf8");
41
- return origEnd.apply(passthrough, args as any); // FIXME how to avoid any?
42
- };
43
- renderToPipeableStream(data, {
61
+ return renderToPipeableStream(data, {
44
62
  onError(err) {
45
63
  if (
46
64
  err instanceof Error &&
@@ -51,6 +69,5 @@ export const renderHtmlToReadable = (
51
69
  }
52
70
  console.error(err);
53
71
  },
54
- }).pipe(passthrough);
55
- return passthrough;
72
+ }).pipe(interleaveHtmlSnippets(...splitHTML(htmlStr)));
56
73
  };
@@ -26,7 +26,7 @@ const renderHTML = async (
26
26
  rscServer: URL,
27
27
  config: Awaited<ReturnType<typeof resolveConfig>>,
28
28
  ssrConfig: NonNullable<Awaited<ReturnType<GetSsrConfig>>>
29
- ): Promise<Readable> => {
29
+ ) => {
30
30
  const rscPrefix = config.framework.rscPrefix;
31
31
  const { splitHTML, getFallback } = config.framework.ssr;
32
32
  const htmlResPromise = fetch(rscServer + pathStr.slice(1), {
@@ -11,7 +11,7 @@ export function rscIndexPlugin(): Plugin {
11
11
  {
12
12
  tag: "script",
13
13
  children: codeToInject,
14
- injectTo: "body",
14
+ injectTo: "head",
15
15
  },
16
16
  ];
17
17
  },