hadars 0.1.40 → 0.2.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.
Files changed (42) hide show
  1. package/README.md +85 -70
  2. package/cli-lib.ts +89 -12
  3. package/dist/chunk-HWOLYLPF.js +332 -0
  4. package/dist/{chunk-2ENP7IAW.js → chunk-LY5MTHFV.js} +360 -203
  5. package/dist/cli.js +506 -274
  6. package/dist/cloudflare.cjs +1394 -0
  7. package/dist/cloudflare.d.cts +64 -0
  8. package/dist/cloudflare.d.ts +64 -0
  9. package/dist/cloudflare.js +68 -0
  10. package/dist/{hadars-Bh-V5YXg.d.cts → hadars-DEBSYAQl.d.cts} +1 -36
  11. package/dist/{hadars-Bh-V5YXg.d.ts → hadars-DEBSYAQl.d.ts} +1 -36
  12. package/dist/index.cjs +129 -156
  13. package/dist/index.d.cts +5 -11
  14. package/dist/index.d.ts +5 -11
  15. package/dist/index.js +129 -155
  16. package/dist/lambda.cjs +391 -229
  17. package/dist/lambda.d.cts +1 -2
  18. package/dist/lambda.d.ts +1 -2
  19. package/dist/lambda.js +18 -307
  20. package/dist/slim-react/index.cjs +361 -203
  21. package/dist/slim-react/index.d.cts +24 -8
  22. package/dist/slim-react/index.d.ts +24 -8
  23. package/dist/slim-react/index.js +3 -1
  24. package/dist/ssr-render-worker.js +352 -221
  25. package/dist/utils/Head.tsx +132 -187
  26. package/package.json +7 -2
  27. package/src/build.ts +7 -6
  28. package/src/cloudflare.ts +139 -0
  29. package/src/index.tsx +0 -3
  30. package/src/lambda.ts +6 -2
  31. package/src/slim-react/context.ts +2 -1
  32. package/src/slim-react/index.ts +21 -18
  33. package/src/slim-react/render.ts +379 -240
  34. package/src/slim-react/renderContext.ts +105 -45
  35. package/src/ssr-render-worker.ts +14 -44
  36. package/src/types/hadars.ts +0 -1
  37. package/src/utils/Head.tsx +132 -187
  38. package/src/utils/cookies.ts +1 -1
  39. package/src/utils/response.tsx +68 -33
  40. package/src/utils/serve.ts +29 -27
  41. package/src/utils/ssrHandler.ts +54 -25
  42. package/src/utils/staticFile.ts +2 -7
@@ -1,5 +1,6 @@
1
1
  import { parseRequest } from './request';
2
- import type { HadarsOptions } from '../types/hadars';
2
+ import { buildHeadHtml } from './response';
3
+ import type { AppHead, HadarsOptions } from '../types/hadars';
3
4
 
4
5
  export const HEAD_MARKER = '<meta name="HADARS_HEAD">';
5
6
  export const BODY_MARKER = '<meta name="HADARS_BODY">';
@@ -8,28 +9,50 @@ const encoder = new TextEncoder();
8
9
 
9
10
  // ── HTML response assembly ────────────────────────────────────────────────────
10
11
 
11
- export async function buildSsrResponse(
12
- bodyHtml: string,
13
- clientProps: Record<string, unknown>,
14
- headHtml: string,
12
+ export function buildSsrResponse(
13
+ head: AppHead,
15
14
  status: number,
16
- getPrecontentHtml: (headHtml: string) => Promise<[string, string]>,
17
- ): Promise<Response> {
15
+ getAppBody: () => Promise<string>,
16
+ finalize: () => Promise<{ clientProps: Record<string, unknown> }>,
17
+ getPrecontentHtml: (headHtml: string) => [string, string] | Promise<[string, string]>,
18
+ ): Response {
19
+ const headHtml = buildHeadHtml(head);
20
+ const precontentResult = getPrecontentHtml(headHtml);
21
+
18
22
  const responseStream = new ReadableStream({
19
23
  async start(controller) {
20
- const [precontentHtml, postContent] = await getPrecontentHtml(headHtml);
21
- // Flush the shell (precontentHtml) immediately so the browser can
22
- // start loading CSS/fonts before the body is assembled.
23
- controller.enqueue(encoder.encode(precontentHtml));
24
-
25
- const scriptContent = JSON.stringify({ hadars: { props: clientProps } }).replace(/</g, '\\u003c');
26
- controller.enqueue(encoder.encode(
27
- `<div id="app">${bodyHtml}</div><script id="hadars" type="application/json">${scriptContent}</script>` + postContent,
28
- ));
29
- controller.close();
24
+ try {
25
+ // Resolve the template — sync on the hot path (every request after the first).
26
+ const [precontentHtml, postContent] = precontentResult instanceof Promise
27
+ ? await precontentResult
28
+ : precontentResult;
29
+
30
+ // Chunk 1 — flush the full <head> shell immediately so the browser
31
+ // can start loading CSS / fonts / preload hints before the body arrives.
32
+ controller.enqueue(encoder.encode(precontentHtml));
33
+
34
+ // Chunk 2 — body HTML. getAppBody() triggers the actual renderToString
35
+ // now that head has been flushed. All data is cached from the preflight
36
+ // so this pass is fast (no async waits).
37
+ const bodyHtml = await getAppBody();
38
+ controller.enqueue(encoder.encode(`<div id="app">${bodyHtml}</div>`));
39
+
40
+ // Chunk 3 — JSON props script + post-content. Separated so the browser
41
+ // can parse/render the body while getFinalProps is still completing.
42
+ const { clientProps } = await finalize();
43
+ const scriptContent = JSON.stringify({ hadars: { props: clientProps } }).replace(/</g, '\\u003c');
44
+ controller.enqueue(encoder.encode(
45
+ `<script id="hadars" type="application/json">${scriptContent}</script>` +
46
+ postContent,
47
+ ));
48
+ controller.close();
49
+ } catch (err) {
50
+ // Head chunk may already be sent; signal a stream error so the
51
+ // connection is closed cleanly rather than hanging.
52
+ controller.error(err);
53
+ }
30
54
  },
31
55
  });
32
-
33
56
  return new Response(responseStream, {
34
57
  headers: { 'Content-Type': 'text/html; charset=utf-8' },
35
58
  status,
@@ -46,9 +69,9 @@ export async function buildSsrHtml(
46
69
  bodyHtml: string,
47
70
  clientProps: Record<string, unknown>,
48
71
  headHtml: string,
49
- getPrecontentHtml: (headHtml: string) => Promise<[string, string]>,
72
+ getPrecontentHtml: (headHtml: string) => [string, string] | Promise<[string, string]>,
50
73
  ): Promise<string> {
51
- const [precontentHtml, postContent] = await getPrecontentHtml(headHtml);
74
+ const [precontentHtml, postContent] = await Promise.resolve(getPrecontentHtml(headHtml));
52
75
  const scriptContent = JSON.stringify({ hadars: { props: clientProps } }).replace(/</g, '\\u003c');
53
76
  return (
54
77
  precontentHtml +
@@ -66,16 +89,22 @@ export const makePrecontentHtmlGetter = (htmlFilePromise: Promise<string>) => {
66
89
  let preHead: string | null = null;
67
90
  let postHead: string | null = null;
68
91
  let postContent: string | null = null;
69
- return async (headHtml: string): Promise<[string, string]> => {
70
- if (preHead === null || postHead === null || postContent === null) {
71
- const html = await htmlFilePromise;
92
+ // Returns synchronously once the template has been loaded and parsed
93
+ // (every request after the first). Callers can check `instanceof Promise`
94
+ // to take a zero-await hot path.
95
+ return (headHtml: string): [string, string] | Promise<[string, string]> => {
96
+ if (preHead !== null) {
97
+ // Hot path — sync return, no Promise allocation.
98
+ return [preHead + headHtml + postHead!, postContent!];
99
+ }
100
+ return htmlFilePromise.then(html => {
72
101
  const headEnd = html.indexOf(HEAD_MARKER);
73
102
  const contentStart = html.indexOf(BODY_MARKER);
74
103
  preHead = html.slice(0, headEnd);
75
104
  postHead = html.slice(headEnd + HEAD_MARKER.length, contentStart);
76
105
  postContent = html.slice(contentStart + BODY_MARKER.length);
77
- }
78
- return [preHead! + headHtml + postHead!, postContent!];
106
+ return [preHead + headHtml + postHead, postContent];
107
+ });
79
108
  };
80
109
  };
81
110
 
@@ -1,4 +1,4 @@
1
- import { readFile, stat } from 'node:fs/promises';
1
+ import { readFile } from 'node:fs/promises';
2
2
 
3
3
  /** MIME type map keyed by lowercase file extension. */
4
4
  const MIME: Record<string, string> = {
@@ -32,16 +32,11 @@ const MIME: Record<string, string> = {
32
32
  * not exist or cannot be read.
33
33
  */
34
34
  export async function tryServeFile(filePath: string): Promise<Response | null> {
35
- try {
36
- await stat(filePath);
37
- } catch {
38
- return null;
39
- }
40
35
  try {
41
36
  const data = await readFile(filePath);
42
37
  const ext = filePath.split('.').pop()?.toLowerCase() ?? '';
43
38
  const contentType = MIME[ext] ?? 'application/octet-stream';
44
- return new Response(data.buffer as ArrayBuffer, { headers: { 'Content-Type': contentType } });
39
+ return new Response(data as BodyInit, { headers: { 'Content-Type': contentType } });
45
40
  } catch {
46
41
  return null;
47
42
  }