hadars 0.1.8 → 0.1.10
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 +21 -14
- package/dist/cli.js +206 -249
- package/dist/index.cjs +2 -1
- package/dist/index.d.ts +7 -6
- package/dist/index.js +2 -1
- package/dist/loader.cjs +126 -5
- package/dist/ssr-render-worker.js +12 -40
- package/dist/ssr-watch.js +16 -0
- package/dist/utils/Head.tsx +4 -2
- package/dist/utils/clientScript.tsx +6 -2
- package/package.json +4 -3
- package/src/build.ts +105 -217
- package/src/ssr-render-worker.ts +13 -48
- package/src/types/ninety.ts +5 -5
- package/src/utils/Head.tsx +4 -2
- package/src/utils/clientScript.tsx +6 -2
- package/src/utils/loader.ts +193 -11
- package/src/utils/response.tsx +51 -55
- package/src/utils/rspack.ts +22 -0
- package/src/utils/serve.ts +40 -0
- package/src/utils/loadModule.ts +0 -4
package/README.md
CHANGED
|
@@ -66,12 +66,32 @@ hadars run # multi-core when workers > 1
|
|
|
66
66
|
|
|
67
67
|
- **React Fast Refresh** — full HMR via rspack-dev-server, module-level patches
|
|
68
68
|
- **True SSR** — components render on the server with your data, then hydrate on the client
|
|
69
|
+
- **Shell streaming** — HTML shell is flushed immediately so browsers can start loading assets before the body arrives
|
|
69
70
|
- **Code splitting** — `loadModule('./Comp')` splits on the browser, bundles statically on the server
|
|
70
71
|
- **Head management** — `HadarsHead` controls `<title>`, `<meta>`, `<link>` on server and client
|
|
71
72
|
- **Cross-runtime** — Bun, Node.js, Deno; uses the standard Fetch API throughout
|
|
72
73
|
- **Multi-core** — `workers: os.cpus().length` forks a process per CPU core via `node:cluster`
|
|
73
74
|
- **TypeScript-first** — full types for props, lifecycle hooks, config, and the request object
|
|
74
75
|
|
|
76
|
+
## useServerData
|
|
77
|
+
|
|
78
|
+
Fetch async data inside a component during SSR. The framework's render loop awaits the promise and re-renders until all values are resolved, then serialises them into the page for zero-cost client hydration.
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
import { useServerData } from 'hadars';
|
|
82
|
+
|
|
83
|
+
const UserCard = ({ userId }: { userId: string }) => {
|
|
84
|
+
const user = useServerData(['user', userId], () => db.getUser(userId));
|
|
85
|
+
if (!user) return null; // undefined while pending on the first SSR pass
|
|
86
|
+
return <p>{user.name}</p>;
|
|
87
|
+
};
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
- **`key`** — string or string array; must be stable and unique within the page
|
|
91
|
+
- **Server** — calls `fn()`, awaits the result across render iterations, returns `undefined` until resolved
|
|
92
|
+
- **Client** — reads the pre-resolved value from the hydration cache serialised by the server; `fn()` is never called in the browser
|
|
93
|
+
- **Suspense libraries** — also works when `fn()` throws a thenable (e.g. Relay `useLazyLoadQuery` with `suspense: true`); the thrown promise is awaited and the next render re-calls `fn()` synchronously
|
|
94
|
+
|
|
75
95
|
## Data lifecycle hooks
|
|
76
96
|
|
|
77
97
|
| Hook | Runs on | Purpose |
|
|
@@ -97,20 +117,7 @@ hadars run # multi-core when workers > 1
|
|
|
97
117
|
| `fetch` | `function` | — | Custom fetch handler; return a `Response` to short-circuit SSR |
|
|
98
118
|
| `websocket` | `object` | — | WebSocket handler (Bun only) |
|
|
99
119
|
| `wsPath` | `string` | `"/ws"` | Path that triggers WebSocket upgrade |
|
|
100
|
-
| `
|
|
101
|
-
|
|
102
|
-
## Local build
|
|
103
|
-
|
|
104
|
-
```bash
|
|
105
|
-
npm install
|
|
106
|
-
npm run build:all
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
## Publishing
|
|
110
|
-
|
|
111
|
-
1. Update `version`, `repository`, `license` in `package.json`
|
|
112
|
-
2. `npm login`
|
|
113
|
-
3. `npm publish`
|
|
120
|
+
| `optimization` | `object` | — | Override rspack `optimization` for production client builds (merged on top of defaults) |
|
|
114
121
|
|
|
115
122
|
## License
|
|
116
123
|
|
package/dist/cli.js
CHANGED
|
@@ -149,48 +149,48 @@ async function getStaticMarkupRenderer() {
|
|
|
149
149
|
}
|
|
150
150
|
return _renderToStaticMarkup;
|
|
151
151
|
}
|
|
152
|
-
var
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
152
|
+
var ESC = { "&": "&", "<": "<", ">": ">", '"': """ };
|
|
153
|
+
var escAttr = (s) => s.replace(/[&<>"]/g, (c) => ESC[c]);
|
|
154
|
+
var escText = (s) => s.replace(/[&<>]/g, (c) => ESC[c]);
|
|
155
|
+
var ATTR = {
|
|
156
|
+
className: "class",
|
|
157
|
+
htmlFor: "for",
|
|
158
|
+
httpEquiv: "http-equiv",
|
|
159
|
+
charSet: "charset",
|
|
160
|
+
crossOrigin: "crossorigin",
|
|
161
|
+
noModule: "nomodule",
|
|
162
|
+
referrerPolicy: "referrerpolicy",
|
|
163
|
+
fetchPriority: "fetchpriority"
|
|
164
|
+
};
|
|
165
|
+
function renderHeadTag(tag, id, opts, selfClose = false) {
|
|
166
|
+
let attrs = ` id="${escAttr(id)}"`;
|
|
167
|
+
let inner = "";
|
|
168
|
+
for (const [k, v] of Object.entries(opts)) {
|
|
169
|
+
if (k === "key" || k === "children")
|
|
170
|
+
continue;
|
|
171
|
+
if (k === "dangerouslySetInnerHTML") {
|
|
172
|
+
inner = v.__html ?? "";
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const attr = ATTR[k] ?? k;
|
|
176
|
+
if (v === true)
|
|
177
|
+
attrs += ` ${attr}`;
|
|
178
|
+
else if (v !== false && v != null)
|
|
179
|
+
attrs += ` ${attr}="${escAttr(String(v))}"`;
|
|
180
|
+
}
|
|
181
|
+
return selfClose ? `<${tag}${attrs}>` : `<${tag}${attrs}>${inner}</${tag}>`;
|
|
182
|
+
}
|
|
183
|
+
var getHeadHtml = (seoData) => {
|
|
184
|
+
let html = `<title>${escText(seoData.title ?? "")}</title>`;
|
|
185
|
+
for (const [id, opts] of Object.entries(seoData.meta))
|
|
186
|
+
html += renderHeadTag("meta", id, opts, true);
|
|
187
|
+
for (const [id, opts] of Object.entries(seoData.link))
|
|
188
|
+
html += renderHeadTag("link", id, opts, true);
|
|
189
|
+
for (const [id, opts] of Object.entries(seoData.style))
|
|
190
|
+
html += renderHeadTag("style", id, opts);
|
|
191
|
+
for (const [id, opts] of Object.entries(seoData.script))
|
|
192
|
+
html += renderHeadTag("script", id, opts);
|
|
193
|
+
return html;
|
|
194
194
|
};
|
|
195
195
|
var getReactResponse = async (req, opts) => {
|
|
196
196
|
const App = opts.document.body;
|
|
@@ -233,18 +233,17 @@ var getReactResponse = async (req, opts) => {
|
|
|
233
233
|
if (unsuspend.hasPending)
|
|
234
234
|
await processUnsuspend();
|
|
235
235
|
} while (unsuspend.hasPending && ++iters < 25);
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
globalThis.__hadarsUnsuspend = null;
|
|
236
|
+
if (unsuspend.hasPending) {
|
|
237
|
+
console.warn("[hadars] SSR render loop hit the 25-iteration cap \u2014 some useServerData values may not be resolved. Check for data dependencies that are never fulfilled.");
|
|
238
|
+
}
|
|
239
|
+
if (getAfterRenderProps) {
|
|
240
|
+
props = await getAfterRenderProps(props, html);
|
|
241
|
+
try {
|
|
242
|
+
globalThis.__hadarsUnsuspend = unsuspend;
|
|
243
|
+
renderToStaticMarkup(/* @__PURE__ */ jsx(App, { ...{ ...props, location: req.location, context } }));
|
|
244
|
+
} finally {
|
|
245
|
+
globalThis.__hadarsUnsuspend = null;
|
|
246
|
+
}
|
|
248
247
|
}
|
|
249
248
|
const serverData = {};
|
|
250
249
|
for (const [k, v] of unsuspend.cache) {
|
|
@@ -268,7 +267,7 @@ var getReactResponse = async (req, opts) => {
|
|
|
268
267
|
return {
|
|
269
268
|
ReactPage,
|
|
270
269
|
status: context.head.status,
|
|
271
|
-
headHtml: getHeadHtml(context.head
|
|
270
|
+
headHtml: getHeadHtml(context.head),
|
|
272
271
|
renderPayload: {
|
|
273
272
|
appProps: { ...props, location: req.location, context },
|
|
274
273
|
clientProps
|
|
@@ -486,6 +485,21 @@ var buildCompilerConfig = (entry, opts, includeHotPlugin) => {
|
|
|
486
485
|
// for server builds prefer the package "main"/"module" fields and avoid "browser" so we don't pick browser-specific entrypoints
|
|
487
486
|
mainFields: isServerBuild ? ["main", "module"] : ["browser", "module", "main"]
|
|
488
487
|
};
|
|
488
|
+
const optimization = !isServerBuild && !isDev ? {
|
|
489
|
+
moduleIds: "deterministic",
|
|
490
|
+
splitChunks: {
|
|
491
|
+
chunks: "all",
|
|
492
|
+
cacheGroups: {
|
|
493
|
+
react: {
|
|
494
|
+
test: /[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/,
|
|
495
|
+
name: "vendor-react",
|
|
496
|
+
chunks: "all",
|
|
497
|
+
priority: 20
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
},
|
|
501
|
+
...opts.optimization ?? {}
|
|
502
|
+
} : opts.optimization ? { ...opts.optimization } : void 0;
|
|
489
503
|
return {
|
|
490
504
|
entry,
|
|
491
505
|
output: {
|
|
@@ -494,6 +508,7 @@ var buildCompilerConfig = (entry, opts, includeHotPlugin) => {
|
|
|
494
508
|
},
|
|
495
509
|
mode: opts.mode,
|
|
496
510
|
externals,
|
|
511
|
+
...optimization !== void 0 ? { optimization } : {},
|
|
497
512
|
plugins: [
|
|
498
513
|
new rspack.HtmlRspackPlugin({
|
|
499
514
|
publicPath: base || "/",
|
|
@@ -603,7 +618,45 @@ function nodeReadableToWebStream(readable) {
|
|
|
603
618
|
});
|
|
604
619
|
}
|
|
605
620
|
var noopCtx = { upgrade: () => false };
|
|
621
|
+
var COMPRESSIBLE_RE = /\b(?:text\/|application\/(?:json|javascript|xml)|image\/svg\+xml)/;
|
|
622
|
+
function withCompression(handler) {
|
|
623
|
+
return async (req, ctx) => {
|
|
624
|
+
const res = await handler(req, ctx);
|
|
625
|
+
if (!res?.body)
|
|
626
|
+
return res;
|
|
627
|
+
if (!COMPRESSIBLE_RE.test(res.headers.get("Content-Type") ?? ""))
|
|
628
|
+
return res;
|
|
629
|
+
if (res.headers.has("Content-Encoding"))
|
|
630
|
+
return res;
|
|
631
|
+
const accept = req.headers.get("Accept-Encoding") ?? "";
|
|
632
|
+
const encoding = accept.includes("br") ? "br" : accept.includes("gzip") ? "gzip" : null;
|
|
633
|
+
if (!encoding)
|
|
634
|
+
return res;
|
|
635
|
+
try {
|
|
636
|
+
const compressed = res.body.pipeThrough(new globalThis.CompressionStream(encoding));
|
|
637
|
+
const headers = new Headers(res.headers);
|
|
638
|
+
headers.set("Content-Encoding", encoding);
|
|
639
|
+
headers.delete("Content-Length");
|
|
640
|
+
return new Response(compressed, { status: res.status, statusText: res.statusText, headers });
|
|
641
|
+
} catch {
|
|
642
|
+
return res;
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
function withRequestLogging(handler) {
|
|
647
|
+
return async (req, ctx) => {
|
|
648
|
+
const start = performance.now();
|
|
649
|
+
const res = await handler(req, ctx);
|
|
650
|
+
const ms = Math.round(performance.now() - start);
|
|
651
|
+
const status = res?.status ?? 404;
|
|
652
|
+
const path2 = new URL(req.url).pathname;
|
|
653
|
+
console.log(`[hadars] ${req.method} ${path2} ${status} ${ms}ms`);
|
|
654
|
+
return res;
|
|
655
|
+
};
|
|
656
|
+
}
|
|
606
657
|
async function serve(port, fetchHandler, websocket) {
|
|
658
|
+
fetchHandler = withCompression(fetchHandler);
|
|
659
|
+
fetchHandler = withRequestLogging(fetchHandler);
|
|
607
660
|
if (isBun) {
|
|
608
661
|
globalThis.Bun.serve({
|
|
609
662
|
port,
|
|
@@ -733,16 +786,6 @@ import cluster from "node:cluster";
|
|
|
733
786
|
var encoder = new TextEncoder();
|
|
734
787
|
var HEAD_MARKER = '<meta name="NINETY_HEAD">';
|
|
735
788
|
var BODY_MARKER = '<meta name="NINETY_BODY">';
|
|
736
|
-
var _renderToReadableStream = null;
|
|
737
|
-
async function getReadableStreamRenderer() {
|
|
738
|
-
if (!_renderToReadableStream) {
|
|
739
|
-
const req = createRequire2(pathMod3.resolve(process.cwd(), "__hadars_fake__.js"));
|
|
740
|
-
const resolved = req.resolve("react-dom/server.browser");
|
|
741
|
-
const mod = await import(pathToFileURL2(resolved).href);
|
|
742
|
-
_renderToReadableStream = mod.renderToReadableStream;
|
|
743
|
-
}
|
|
744
|
-
return _renderToReadableStream;
|
|
745
|
-
}
|
|
746
789
|
var _renderToString = null;
|
|
747
790
|
async function getRenderToString() {
|
|
748
791
|
if (!_renderToString) {
|
|
@@ -761,64 +804,52 @@ var RenderWorkerPool = class {
|
|
|
761
804
|
workerPending = /* @__PURE__ */ new Map();
|
|
762
805
|
nextId = 0;
|
|
763
806
|
rrIndex = 0;
|
|
807
|
+
_Worker = null;
|
|
808
|
+
_workerPath = "";
|
|
809
|
+
_ssrBundlePath = "";
|
|
764
810
|
constructor(workerPath, size, ssrBundlePath) {
|
|
765
811
|
this._init(workerPath, size, ssrBundlePath);
|
|
766
812
|
}
|
|
767
813
|
_init(workerPath, size, ssrBundlePath) {
|
|
814
|
+
this._workerPath = workerPath;
|
|
815
|
+
this._ssrBundlePath = ssrBundlePath;
|
|
768
816
|
import("node:worker_threads").then(({ Worker }) => {
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
this.
|
|
772
|
-
w.on("message", (msg) => {
|
|
773
|
-
const { id, type, html, headHtml, status, error, chunk } = msg;
|
|
774
|
-
const p = this.pending.get(id);
|
|
775
|
-
if (!p)
|
|
776
|
-
return;
|
|
777
|
-
if (p.kind === "renderFullStream") {
|
|
778
|
-
if (type === "head") {
|
|
779
|
-
p.headSettled = true;
|
|
780
|
-
p.headResolve({ headHtml, status });
|
|
781
|
-
return;
|
|
782
|
-
}
|
|
783
|
-
if (type === "chunk") {
|
|
784
|
-
p.controller.enqueue(chunk);
|
|
785
|
-
return;
|
|
786
|
-
}
|
|
787
|
-
this.pending.delete(id);
|
|
788
|
-
this.workerPending.get(w)?.delete(id);
|
|
789
|
-
if (type === "done")
|
|
790
|
-
p.controller.close();
|
|
791
|
-
else {
|
|
792
|
-
const err = new Error(error ?? "Stream error");
|
|
793
|
-
if (!p.headSettled)
|
|
794
|
-
p.headReject(err);
|
|
795
|
-
p.controller.error(err);
|
|
796
|
-
}
|
|
797
|
-
return;
|
|
798
|
-
}
|
|
799
|
-
this.pending.delete(id);
|
|
800
|
-
this.workerPending.get(w)?.delete(id);
|
|
801
|
-
if (error)
|
|
802
|
-
p.reject(new Error(error));
|
|
803
|
-
else
|
|
804
|
-
p.resolve({ html, headHtml, status });
|
|
805
|
-
});
|
|
806
|
-
w.on("error", (err) => {
|
|
807
|
-
console.error("[hadars] Render worker error:", err);
|
|
808
|
-
this._handleWorkerDeath(w, err);
|
|
809
|
-
});
|
|
810
|
-
w.on("exit", (code) => {
|
|
811
|
-
if (code !== 0) {
|
|
812
|
-
console.error(`[hadars] Render worker exited with code ${code}`);
|
|
813
|
-
this._handleWorkerDeath(w, new Error(`Render worker exited with code ${code}`));
|
|
814
|
-
}
|
|
815
|
-
});
|
|
816
|
-
this.workers.push(w);
|
|
817
|
-
}
|
|
817
|
+
this._Worker = Worker;
|
|
818
|
+
for (let i = 0; i < size; i++)
|
|
819
|
+
this._spawnWorker();
|
|
818
820
|
}).catch((err) => {
|
|
819
821
|
console.error("[hadars] Failed to initialise render worker pool:", err);
|
|
820
822
|
});
|
|
821
823
|
}
|
|
824
|
+
_spawnWorker() {
|
|
825
|
+
if (!this._Worker)
|
|
826
|
+
return;
|
|
827
|
+
const w = new this._Worker(this._workerPath, { workerData: { ssrBundlePath: this._ssrBundlePath } });
|
|
828
|
+
this.workerPending.set(w, /* @__PURE__ */ new Set());
|
|
829
|
+
w.on("message", (msg) => {
|
|
830
|
+
const { id, html, headHtml, status, error } = msg;
|
|
831
|
+
const p = this.pending.get(id);
|
|
832
|
+
if (!p)
|
|
833
|
+
return;
|
|
834
|
+
this.pending.delete(id);
|
|
835
|
+
this.workerPending.get(w)?.delete(id);
|
|
836
|
+
if (error)
|
|
837
|
+
p.reject(new Error(error));
|
|
838
|
+
else
|
|
839
|
+
p.resolve({ html, headHtml, status });
|
|
840
|
+
});
|
|
841
|
+
w.on("error", (err) => {
|
|
842
|
+
console.error("[hadars] Render worker error:", err);
|
|
843
|
+
this._handleWorkerDeath(w, err);
|
|
844
|
+
});
|
|
845
|
+
w.on("exit", (code) => {
|
|
846
|
+
if (code !== 0) {
|
|
847
|
+
console.error(`[hadars] Render worker exited with code ${code}`);
|
|
848
|
+
this._handleWorkerDeath(w, new Error(`Render worker exited with code ${code}`));
|
|
849
|
+
}
|
|
850
|
+
});
|
|
851
|
+
this.workers.push(w);
|
|
852
|
+
}
|
|
822
853
|
_handleWorkerDeath(w, err) {
|
|
823
854
|
const idx = this.workers.indexOf(w);
|
|
824
855
|
if (idx !== -1)
|
|
@@ -829,17 +860,13 @@ var RenderWorkerPool = class {
|
|
|
829
860
|
const p = this.pending.get(id);
|
|
830
861
|
if (p) {
|
|
831
862
|
this.pending.delete(id);
|
|
832
|
-
|
|
833
|
-
p.reject(err);
|
|
834
|
-
else {
|
|
835
|
-
if (!p.headSettled)
|
|
836
|
-
p.headReject(err);
|
|
837
|
-
p.controller.error(err);
|
|
838
|
-
}
|
|
863
|
+
p.reject(err);
|
|
839
864
|
}
|
|
840
865
|
}
|
|
841
866
|
this.workerPending.delete(w);
|
|
842
867
|
}
|
|
868
|
+
console.log("[hadars] Spawning replacement render worker");
|
|
869
|
+
this._spawnWorker();
|
|
843
870
|
}
|
|
844
871
|
nextWorker() {
|
|
845
872
|
if (this.workers.length === 0)
|
|
@@ -868,82 +895,25 @@ var RenderWorkerPool = class {
|
|
|
868
895
|
}
|
|
869
896
|
});
|
|
870
897
|
}
|
|
871
|
-
/** Run the full SSR lifecycle in a worker thread, streaming the response. */
|
|
872
|
-
renderFullStream(req) {
|
|
873
|
-
let headResolve;
|
|
874
|
-
let headReject;
|
|
875
|
-
const head = new Promise((res, rej) => {
|
|
876
|
-
headResolve = res;
|
|
877
|
-
headReject = rej;
|
|
878
|
-
});
|
|
879
|
-
let controller;
|
|
880
|
-
const stream = new ReadableStream({ start: (ctrl) => {
|
|
881
|
-
controller = ctrl;
|
|
882
|
-
} });
|
|
883
|
-
const w = this.nextWorker();
|
|
884
|
-
if (!w) {
|
|
885
|
-
queueMicrotask(() => {
|
|
886
|
-
headReject(new Error("[hadars] No render workers available"));
|
|
887
|
-
controller.error(new Error("[hadars] No render workers available"));
|
|
888
|
-
});
|
|
889
|
-
return { head, stream };
|
|
890
|
-
}
|
|
891
|
-
const id = this.nextId++;
|
|
892
|
-
this.pending.set(id, { kind: "renderFullStream", headSettled: false, headResolve, headReject, controller });
|
|
893
|
-
this.workerPending.get(w)?.add(id);
|
|
894
|
-
try {
|
|
895
|
-
w.postMessage({ id, type: "renderFull", streaming: true, request: req });
|
|
896
|
-
} catch (err) {
|
|
897
|
-
this.pending.delete(id);
|
|
898
|
-
this.workerPending.get(w)?.delete(id);
|
|
899
|
-
queueMicrotask(() => {
|
|
900
|
-
headReject(err);
|
|
901
|
-
controller.error(err);
|
|
902
|
-
});
|
|
903
|
-
}
|
|
904
|
-
return { head, stream };
|
|
905
|
-
}
|
|
906
898
|
async terminate() {
|
|
907
899
|
await Promise.all(this.workers.map((w) => w.terminate()));
|
|
908
900
|
}
|
|
909
901
|
};
|
|
910
|
-
async function buildSsrResponse(ReactPage, headHtml, status, getPrecontentHtml,
|
|
911
|
-
const renderToString =
|
|
912
|
-
const renderReadableStream = streaming ? await getReadableStreamRenderer() : null;
|
|
913
|
-
if (!streaming) {
|
|
914
|
-
const [precontentHtml, postContent] = await getPrecontentHtml(headHtml);
|
|
915
|
-
let bodyHtml;
|
|
916
|
-
try {
|
|
917
|
-
globalThis.__hadarsUnsuspend = unsuspendForRender;
|
|
918
|
-
bodyHtml = renderToString(ReactPage);
|
|
919
|
-
} finally {
|
|
920
|
-
globalThis.__hadarsUnsuspend = null;
|
|
921
|
-
}
|
|
922
|
-
return new Response(precontentHtml + bodyHtml + postContent, {
|
|
923
|
-
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
924
|
-
status
|
|
925
|
-
});
|
|
926
|
-
}
|
|
902
|
+
async function buildSsrResponse(ReactPage, headHtml, status, getPrecontentHtml, unsuspendForRender) {
|
|
903
|
+
const renderToString = await getRenderToString();
|
|
927
904
|
const responseStream = new ReadableStream({
|
|
928
905
|
async start(controller) {
|
|
929
906
|
const [precontentHtml, postContent] = await getPrecontentHtml(headHtml);
|
|
930
907
|
controller.enqueue(encoder.encode(precontentHtml));
|
|
931
|
-
|
|
908
|
+
await Promise.resolve();
|
|
909
|
+
let bodyHtml;
|
|
932
910
|
try {
|
|
933
911
|
globalThis.__hadarsUnsuspend = unsuspendForRender;
|
|
934
|
-
|
|
912
|
+
bodyHtml = renderToString(ReactPage);
|
|
935
913
|
} finally {
|
|
936
914
|
globalThis.__hadarsUnsuspend = null;
|
|
937
915
|
}
|
|
938
|
-
|
|
939
|
-
const reader = bodyStream.getReader();
|
|
940
|
-
while (true) {
|
|
941
|
-
const { done, value } = await reader.read();
|
|
942
|
-
if (done)
|
|
943
|
-
break;
|
|
944
|
-
controller.enqueue(value);
|
|
945
|
-
}
|
|
946
|
-
controller.enqueue(encoder.encode(postContent));
|
|
916
|
+
controller.enqueue(encoder.encode(bodyHtml + postContent));
|
|
947
917
|
controller.close();
|
|
948
918
|
}
|
|
949
919
|
});
|
|
@@ -1048,7 +1018,7 @@ var dev = async (options) => {
|
|
|
1048
1018
|
}
|
|
1049
1019
|
const tmpFilePath = pathMod3.join(os.tmpdir(), `hadars-client-${Date.now()}.tsx`);
|
|
1050
1020
|
await fs.writeFile(tmpFilePath, clientScript);
|
|
1051
|
-
let ssrBuildId =
|
|
1021
|
+
let ssrBuildId = crypto.randomBytes(4).toString("hex");
|
|
1052
1022
|
const clientCompiler = createClientCompiler(tmpFilePath, {
|
|
1053
1023
|
target: "web",
|
|
1054
1024
|
output: {
|
|
@@ -1163,7 +1133,7 @@ var dev = async (options) => {
|
|
|
1163
1133
|
} catch (e) {
|
|
1164
1134
|
}
|
|
1165
1135
|
if (chunk.includes(rebuildMarker)) {
|
|
1166
|
-
ssrBuildId =
|
|
1136
|
+
ssrBuildId = crypto.randomBytes(4).toString("hex");
|
|
1167
1137
|
console.log("[hadars] SSR bundle updated, build id:", ssrBuildId);
|
|
1168
1138
|
}
|
|
1169
1139
|
}
|
|
@@ -1217,23 +1187,32 @@ var dev = async (options) => {
|
|
|
1217
1187
|
return projectRes;
|
|
1218
1188
|
const ssrComponentPath = pathMod3.join(__dirname2, HadarsFolder, SSR_FILENAME);
|
|
1219
1189
|
const importPath = pathToFileURL2(ssrComponentPath).href + `?t=${ssrBuildId}`;
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
getAfterRenderProps,
|
|
1224
|
-
getFinalProps
|
|
1225
|
-
} = await import(importPath);
|
|
1226
|
-
const { ReactPage, status, headHtml, renderPayload } = await getReactResponse(request, {
|
|
1227
|
-
document: {
|
|
1228
|
-
body: Component,
|
|
1229
|
-
lang: "en",
|
|
1190
|
+
try {
|
|
1191
|
+
const {
|
|
1192
|
+
default: Component,
|
|
1230
1193
|
getInitProps,
|
|
1231
1194
|
getAfterRenderProps,
|
|
1232
1195
|
getFinalProps
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1196
|
+
} = await import(importPath);
|
|
1197
|
+
const { ReactPage, status, headHtml, renderPayload } = await getReactResponse(request, {
|
|
1198
|
+
document: {
|
|
1199
|
+
body: Component,
|
|
1200
|
+
lang: "en",
|
|
1201
|
+
getInitProps,
|
|
1202
|
+
getAfterRenderProps,
|
|
1203
|
+
getFinalProps
|
|
1204
|
+
}
|
|
1205
|
+
});
|
|
1206
|
+
const unsuspend = renderPayload.appProps.context?._unsuspend ?? null;
|
|
1207
|
+
return buildSsrResponse(ReactPage, headHtml, status, getPrecontentHtml, unsuspend);
|
|
1208
|
+
} catch (err) {
|
|
1209
|
+
console.error("[hadars] SSR render error:", err);
|
|
1210
|
+
const msg = (err?.stack ?? err?.message ?? String(err)).replace(/</g, "<");
|
|
1211
|
+
return new Response(`<!doctype html><pre style="white-space:pre-wrap">${msg}</pre>`, {
|
|
1212
|
+
status: 500,
|
|
1213
|
+
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1237
1216
|
}, options.websocket);
|
|
1238
1217
|
};
|
|
1239
1218
|
var build = async (options) => {
|
|
@@ -1250,19 +1229,20 @@ var build = async (options) => {
|
|
|
1250
1229
|
}
|
|
1251
1230
|
const tmpFilePath = pathMod3.join(os.tmpdir(), `hadars-client-${Date.now()}.tsx`);
|
|
1252
1231
|
await fs.writeFile(tmpFilePath, clientScript);
|
|
1253
|
-
const randomStr = crypto.randomBytes(6).toString("hex");
|
|
1254
1232
|
console.log("Building client and server bundles in parallel...");
|
|
1255
1233
|
await Promise.all([
|
|
1256
1234
|
compileEntry(tmpFilePath, {
|
|
1257
1235
|
target: "web",
|
|
1258
1236
|
output: {
|
|
1259
|
-
|
|
1237
|
+
// Content hash: filename is stable when code is unchanged → better browser/CDN cache.
|
|
1238
|
+
filename: "index.[contenthash:8].js",
|
|
1260
1239
|
path: pathMod3.resolve(__dirname2, StaticPath)
|
|
1261
1240
|
},
|
|
1262
1241
|
base: options.baseURL,
|
|
1263
1242
|
mode: "production",
|
|
1264
1243
|
swcPlugins: options.swcPlugins,
|
|
1265
|
-
define: options.define
|
|
1244
|
+
define: options.define,
|
|
1245
|
+
optimization: options.optimization
|
|
1266
1246
|
}),
|
|
1267
1247
|
compileEntry(pathMod3.resolve(__dirname2, options.entry), {
|
|
1268
1248
|
output: {
|
|
@@ -1280,10 +1260,6 @@ var build = async (options) => {
|
|
|
1280
1260
|
})
|
|
1281
1261
|
]);
|
|
1282
1262
|
await fs.rm(tmpFilePath);
|
|
1283
|
-
await fs.writeFile(
|
|
1284
|
-
pathMod3.join(__dirname2, HadarsFolder, "hadars.json"),
|
|
1285
|
-
JSON.stringify({ buildId: randomStr })
|
|
1286
|
-
);
|
|
1287
1263
|
console.log("Build complete.");
|
|
1288
1264
|
};
|
|
1289
1265
|
var run = async (options) => {
|
|
@@ -1356,37 +1332,15 @@ var run = async (options) => {
|
|
|
1356
1332
|
const componentPath = pathToFileURL2(
|
|
1357
1333
|
pathMod3.resolve(__dirname2, HadarsFolder, SSR_FILENAME)
|
|
1358
1334
|
).href;
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
const { head, stream } = renderPool.renderFullStream(serialReq);
|
|
1369
|
-
const { headHtml: wHead, status: wStatus } = await head;
|
|
1370
|
-
const [precontentHtml, postContent] = await getPrecontentHtml(wHead);
|
|
1371
|
-
const responseStream = new ReadableStream({
|
|
1372
|
-
async start(controller) {
|
|
1373
|
-
controller.enqueue(encoder.encode(precontentHtml));
|
|
1374
|
-
const reader = stream.getReader();
|
|
1375
|
-
while (true) {
|
|
1376
|
-
const { done, value } = await reader.read();
|
|
1377
|
-
if (done)
|
|
1378
|
-
break;
|
|
1379
|
-
controller.enqueue(value);
|
|
1380
|
-
}
|
|
1381
|
-
controller.enqueue(encoder.encode(postContent));
|
|
1382
|
-
controller.close();
|
|
1383
|
-
}
|
|
1384
|
-
});
|
|
1385
|
-
return new Response(responseStream, {
|
|
1386
|
-
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
1387
|
-
status: wStatus
|
|
1388
|
-
});
|
|
1389
|
-
} else {
|
|
1335
|
+
try {
|
|
1336
|
+
const {
|
|
1337
|
+
default: Component,
|
|
1338
|
+
getInitProps,
|
|
1339
|
+
getAfterRenderProps,
|
|
1340
|
+
getFinalProps
|
|
1341
|
+
} = await import(componentPath);
|
|
1342
|
+
if (renderPool) {
|
|
1343
|
+
const serialReq = await serializeRequest(request);
|
|
1390
1344
|
const { html, headHtml: wHead, status: wStatus } = await renderPool.renderFull(serialReq);
|
|
1391
1345
|
const [precontentHtml, postContent] = await getPrecontentHtml(wHead);
|
|
1392
1346
|
return new Response(precontentHtml + html + postContent, {
|
|
@@ -1394,18 +1348,21 @@ var run = async (options) => {
|
|
|
1394
1348
|
status: wStatus
|
|
1395
1349
|
});
|
|
1396
1350
|
}
|
|
1351
|
+
const { ReactPage, status, headHtml, renderPayload } = await getReactResponse(request, {
|
|
1352
|
+
document: {
|
|
1353
|
+
body: Component,
|
|
1354
|
+
lang: "en",
|
|
1355
|
+
getInitProps,
|
|
1356
|
+
getAfterRenderProps,
|
|
1357
|
+
getFinalProps
|
|
1358
|
+
}
|
|
1359
|
+
});
|
|
1360
|
+
const unsuspend = renderPayload.appProps.context?._unsuspend ?? null;
|
|
1361
|
+
return buildSsrResponse(ReactPage, headHtml, status, getPrecontentHtml, unsuspend);
|
|
1362
|
+
} catch (err) {
|
|
1363
|
+
console.error("[hadars] SSR render error:", err);
|
|
1364
|
+
return new Response("Internal Server Error", { status: 500 });
|
|
1397
1365
|
}
|
|
1398
|
-
const { ReactPage, status, headHtml, renderPayload } = await getReactResponse(request, {
|
|
1399
|
-
document: {
|
|
1400
|
-
body: Component,
|
|
1401
|
-
lang: "en",
|
|
1402
|
-
getInitProps,
|
|
1403
|
-
getAfterRenderProps,
|
|
1404
|
-
getFinalProps
|
|
1405
|
-
}
|
|
1406
|
-
});
|
|
1407
|
-
const unsuspend = renderPayload.appProps.context?._unsuspend ?? null;
|
|
1408
|
-
return buildSsrResponse(ReactPage, headHtml, status, getPrecontentHtml, options.streaming === true, unsuspend);
|
|
1409
1366
|
}, options.websocket);
|
|
1410
1367
|
};
|
|
1411
1368
|
|
package/dist/index.cjs
CHANGED
|
@@ -172,12 +172,13 @@ var AppProviderCSR = import_react.default.memo(({ children }) => {
|
|
|
172
172
|
var useApp = () => import_react.default.useContext(AppContext);
|
|
173
173
|
var clientServerDataCache = /* @__PURE__ */ new Map();
|
|
174
174
|
function initServerDataCache(data) {
|
|
175
|
+
clientServerDataCache.clear();
|
|
175
176
|
for (const [k, v] of Object.entries(data)) {
|
|
176
177
|
clientServerDataCache.set(k, v);
|
|
177
178
|
}
|
|
178
179
|
}
|
|
179
180
|
function useServerData(key, fn) {
|
|
180
|
-
const cacheKey = Array.isArray(key) ?
|
|
181
|
+
const cacheKey = Array.isArray(key) ? JSON.stringify(key) : key;
|
|
181
182
|
if (typeof window !== "undefined") {
|
|
182
183
|
if (clientServerDataCache.has(cacheKey)) {
|
|
183
184
|
return clientServerDataCache.get(cacheKey);
|