hadars 0.1.5 → 0.1.6
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 +1 -2
- package/cli.ts +33 -10
- package/dist/cli.js +132 -18
- package/dist/index.cjs +13 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +12 -1
- package/dist/ssr-render-worker.js +1 -1
- package/dist/ssr-watch.js +7 -1
- package/dist/utils/Head.tsx +14 -4
- package/dist/utils/clientScript.tsx +1 -1
- package/package.json +2 -4
- package/src/build.ts +72 -13
- package/src/index.tsx +1 -1
- package/src/ssr-render-worker.ts +1 -1
- package/src/types/ninety.ts +1 -0
- package/src/utils/Head.tsx +14 -4
- package/src/utils/clientScript.tsx +1 -1
- package/src/utils/response.tsx +1 -1
- package/src/utils/rspack.ts +7 -1
- package/cli-bun.ts +0 -13
package/README.md
CHANGED
|
@@ -49,12 +49,11 @@ export default App;
|
|
|
49
49
|
|
|
50
50
|
## CLI
|
|
51
51
|
|
|
52
|
-
After installing hadars the `hadars`
|
|
52
|
+
After installing hadars the `hadars` binary is available. It works on Node.js, Bun, and Deno — the runtime is auto-detected:
|
|
53
53
|
|
|
54
54
|
```bash
|
|
55
55
|
# Development server with React Fast Refresh HMR
|
|
56
56
|
hadars dev
|
|
57
|
-
hadars-bun dev # Bun — runs TypeScript directly, no build step
|
|
58
57
|
|
|
59
58
|
# Production build (client + SSR bundles compiled in parallel)
|
|
60
59
|
hadars build
|
package/cli.ts
CHANGED
|
@@ -1,13 +1,36 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
2
3
|
import { runCli } from './cli-lib'
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
5
|
+
// When the #!/usr/bin/env node shebang forces Node.js as the interpreter,
|
|
6
|
+
// try to re-exec with Bun so the server runs under Bun's runtime
|
|
7
|
+
// (native Bun.serve, WebSocket support, etc.).
|
|
8
|
+
// Falls back to Node.js silently if bun is not in PATH.
|
|
9
|
+
if (typeof (globalThis as any).Bun === 'undefined' && typeof (globalThis as any).Deno === 'undefined') {
|
|
10
|
+
const child = spawn('bun', [process.argv[1], ...process.argv.slice(2)], {
|
|
11
|
+
stdio: 'inherit',
|
|
12
|
+
env: process.env,
|
|
13
|
+
});
|
|
14
|
+
const sigs = ['SIGINT', 'SIGTERM', 'SIGHUP'] as const;
|
|
15
|
+
const fwd = (sig: string) => () => { try { child.kill(sig); } catch {} };
|
|
16
|
+
for (const sig of sigs) process.on(sig, fwd(sig));
|
|
17
|
+
child.on('error', (err: any) => {
|
|
18
|
+
for (const sig of sigs) process.removeAllListeners(sig);
|
|
19
|
+
if (err.code !== 'ENOENT') { console.error(err); process.exit(1); }
|
|
20
|
+
// bun not found — fall through and run with Node.js
|
|
21
|
+
runCli(process.argv).catch((e) => {
|
|
22
|
+
console.error(e);
|
|
23
|
+
try { process.exit(1); } catch (_) { throw e; }
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
child.on('exit', (code: number | null) => process.exit(code ?? 0));
|
|
27
|
+
} else {
|
|
28
|
+
runCli(process.argv).catch((err) => {
|
|
29
|
+
console.error(err)
|
|
30
|
+
try {
|
|
31
|
+
process.exit(1)
|
|
32
|
+
} catch (_) {
|
|
33
|
+
throw err
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// cli.ts
|
|
4
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
5
|
+
|
|
3
6
|
// cli-lib.ts
|
|
4
7
|
import { existsSync as existsSync3 } from "node:fs";
|
|
5
8
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
@@ -260,7 +263,7 @@ var getReactResponse = async (req, opts) => {
|
|
|
260
263
|
location: req.location,
|
|
261
264
|
context
|
|
262
265
|
} }) }),
|
|
263
|
-
/* @__PURE__ */ jsx("script", { id: "hadars", type: "application/json", dangerouslySetInnerHTML: { __html: JSON.stringify({ hadars: { props: clientProps } }) } })
|
|
266
|
+
/* @__PURE__ */ jsx("script", { id: "hadars", type: "application/json", dangerouslySetInnerHTML: { __html: JSON.stringify({ hadars: { props: clientProps } }).replace(/</g, "\\u003c") } })
|
|
264
267
|
] });
|
|
265
268
|
return {
|
|
266
269
|
ReactPage,
|
|
@@ -485,7 +488,6 @@ var buildCompilerConfig = (entry, opts, includeHotPlugin) => {
|
|
|
485
488
|
};
|
|
486
489
|
return {
|
|
487
490
|
entry,
|
|
488
|
-
resolve: resolveConfig,
|
|
489
491
|
output: {
|
|
490
492
|
...opts.output,
|
|
491
493
|
clean: false
|
|
@@ -505,6 +507,13 @@ var buildCompilerConfig = (entry, opts, includeHotPlugin) => {
|
|
|
505
507
|
...extraPlugins
|
|
506
508
|
],
|
|
507
509
|
...localConfig,
|
|
510
|
+
// Merge base resolve (modules, tsConfig, extensions) with per-build resolve
|
|
511
|
+
// (alias, mainFields). The spread order matters: resolveConfig wins for keys
|
|
512
|
+
// it defines, localConfig.resolve wins for keys it defines exclusively.
|
|
513
|
+
resolve: {
|
|
514
|
+
...localConfig.resolve,
|
|
515
|
+
...resolveConfig
|
|
516
|
+
},
|
|
508
517
|
// HMR is not implemented for module chunk format, so disable outputModule
|
|
509
518
|
// for client builds. SSR builds still need it for dynamic import() of exports.
|
|
510
519
|
experiments: {
|
|
@@ -747,6 +756,9 @@ async function getRenderToString() {
|
|
|
747
756
|
var RenderWorkerPool = class {
|
|
748
757
|
workers = [];
|
|
749
758
|
pending = /* @__PURE__ */ new Map();
|
|
759
|
+
// Track which pending IDs were dispatched to each worker so we can reject
|
|
760
|
+
// them when that worker crashes.
|
|
761
|
+
workerPending = /* @__PURE__ */ new Map();
|
|
750
762
|
nextId = 0;
|
|
751
763
|
rrIndex = 0;
|
|
752
764
|
constructor(workerPath, size, ssrBundlePath) {
|
|
@@ -756,6 +768,7 @@ var RenderWorkerPool = class {
|
|
|
756
768
|
import("node:worker_threads").then(({ Worker }) => {
|
|
757
769
|
for (let i = 0; i < size; i++) {
|
|
758
770
|
const w = new Worker(workerPath, { workerData: { ssrBundlePath } });
|
|
771
|
+
this.workerPending.set(w, /* @__PURE__ */ new Set());
|
|
759
772
|
w.on("message", (msg) => {
|
|
760
773
|
const { id, type, html, error, chunk } = msg;
|
|
761
774
|
const p = this.pending.get(id);
|
|
@@ -767,6 +780,7 @@ var RenderWorkerPool = class {
|
|
|
767
780
|
return;
|
|
768
781
|
}
|
|
769
782
|
this.pending.delete(id);
|
|
783
|
+
this.workerPending.get(w)?.delete(id);
|
|
770
784
|
if (type === "done")
|
|
771
785
|
p.controller.close();
|
|
772
786
|
else
|
|
@@ -774,6 +788,7 @@ var RenderWorkerPool = class {
|
|
|
774
788
|
return;
|
|
775
789
|
}
|
|
776
790
|
this.pending.delete(id);
|
|
791
|
+
this.workerPending.get(w)?.delete(id);
|
|
777
792
|
if (error)
|
|
778
793
|
p.reject(new Error(error));
|
|
779
794
|
else
|
|
@@ -781,6 +796,13 @@ var RenderWorkerPool = class {
|
|
|
781
796
|
});
|
|
782
797
|
w.on("error", (err) => {
|
|
783
798
|
console.error("[hadars] Render worker error:", err);
|
|
799
|
+
this._handleWorkerDeath(w, err);
|
|
800
|
+
});
|
|
801
|
+
w.on("exit", (code) => {
|
|
802
|
+
if (code !== 0) {
|
|
803
|
+
console.error(`[hadars] Render worker exited with code ${code}`);
|
|
804
|
+
this._handleWorkerDeath(w, new Error(`Render worker exited with code ${code}`));
|
|
805
|
+
}
|
|
784
806
|
});
|
|
785
807
|
this.workers.push(w);
|
|
786
808
|
}
|
|
@@ -788,7 +810,28 @@ var RenderWorkerPool = class {
|
|
|
788
810
|
console.error("[hadars] Failed to initialise render worker pool:", err);
|
|
789
811
|
});
|
|
790
812
|
}
|
|
813
|
+
_handleWorkerDeath(w, err) {
|
|
814
|
+
const idx = this.workers.indexOf(w);
|
|
815
|
+
if (idx !== -1)
|
|
816
|
+
this.workers.splice(idx, 1);
|
|
817
|
+
const ids = this.workerPending.get(w);
|
|
818
|
+
if (ids) {
|
|
819
|
+
for (const id of ids) {
|
|
820
|
+
const p = this.pending.get(id);
|
|
821
|
+
if (p) {
|
|
822
|
+
this.pending.delete(id);
|
|
823
|
+
if (p.kind === "renderString")
|
|
824
|
+
p.reject(err);
|
|
825
|
+
else
|
|
826
|
+
p.controller.error(err);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
this.workerPending.delete(w);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
791
832
|
nextWorker() {
|
|
833
|
+
if (this.workers.length === 0)
|
|
834
|
+
return void 0;
|
|
792
835
|
const w = this.workers[this.rrIndex % this.workers.length];
|
|
793
836
|
this.rrIndex++;
|
|
794
837
|
return w;
|
|
@@ -796,9 +839,21 @@ var RenderWorkerPool = class {
|
|
|
796
839
|
/** Offload a full renderToString call. Returns the HTML string. */
|
|
797
840
|
renderString(appProps, clientProps) {
|
|
798
841
|
return new Promise((resolve2, reject) => {
|
|
842
|
+
const w = this.nextWorker();
|
|
843
|
+
if (!w) {
|
|
844
|
+
reject(new Error("[hadars] No render workers available"));
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
799
847
|
const id = this.nextId++;
|
|
800
848
|
this.pending.set(id, { kind: "renderString", resolve: resolve2, reject });
|
|
801
|
-
this.
|
|
849
|
+
this.workerPending.get(w)?.add(id);
|
|
850
|
+
try {
|
|
851
|
+
w.postMessage({ id, type: "renderString", appProps, clientProps });
|
|
852
|
+
} catch (err) {
|
|
853
|
+
this.pending.delete(id);
|
|
854
|
+
this.workerPending.get(w)?.delete(id);
|
|
855
|
+
reject(err);
|
|
856
|
+
}
|
|
802
857
|
});
|
|
803
858
|
}
|
|
804
859
|
/** Offload a renderToReadableStream call. Returns a ReadableStream fed by
|
|
@@ -810,9 +865,21 @@ var RenderWorkerPool = class {
|
|
|
810
865
|
controller = ctrl;
|
|
811
866
|
}
|
|
812
867
|
});
|
|
868
|
+
const w = this.nextWorker();
|
|
869
|
+
if (!w) {
|
|
870
|
+
queueMicrotask(() => controller.error(new Error("[hadars] No render workers available")));
|
|
871
|
+
return stream;
|
|
872
|
+
}
|
|
813
873
|
const id = this.nextId++;
|
|
814
874
|
this.pending.set(id, { kind: "renderStream", controller });
|
|
815
|
-
this.
|
|
875
|
+
this.workerPending.get(w)?.add(id);
|
|
876
|
+
try {
|
|
877
|
+
w.postMessage({ id, type: "renderStream", appProps, clientProps });
|
|
878
|
+
} catch (err) {
|
|
879
|
+
this.pending.delete(id);
|
|
880
|
+
this.workerPending.get(w)?.delete(id);
|
|
881
|
+
queueMicrotask(() => controller.error(err));
|
|
882
|
+
}
|
|
816
883
|
return stream;
|
|
817
884
|
}
|
|
818
885
|
async terminate() {
|
|
@@ -943,10 +1010,9 @@ var dev = async (options) => {
|
|
|
943
1010
|
const hmrPort = options.hmrPort ?? port + 1;
|
|
944
1011
|
const packageDir2 = pathMod3.dirname(fileURLToPath2(import.meta.url));
|
|
945
1012
|
const clientScriptPath2 = pathMod3.resolve(packageDir2, "utils", "clientScript.tsx");
|
|
946
|
-
const headPath = pathMod3.resolve(packageDir2, "utils", "Head");
|
|
947
1013
|
let clientScript = "";
|
|
948
1014
|
try {
|
|
949
|
-
clientScript = (await fs.readFile(clientScriptPath2, "utf-8")).replace("$_MOD_PATH$", entry + getSuffix(options.mode))
|
|
1015
|
+
clientScript = (await fs.readFile(clientScriptPath2, "utf-8")).replace("$_MOD_PATH$", entry + getSuffix(options.mode));
|
|
950
1016
|
} catch (err) {
|
|
951
1017
|
console.error("Failed to read client script from package dist, falling back to src", err);
|
|
952
1018
|
throw err;
|
|
@@ -1002,6 +1068,22 @@ var dev = async (options) => {
|
|
|
1002
1068
|
...options.define ? [`--define=${JSON.stringify(options.define)}`] : []
|
|
1003
1069
|
], { stdio: "pipe" });
|
|
1004
1070
|
child.stdin?.end();
|
|
1071
|
+
const cleanupChild = () => {
|
|
1072
|
+
try {
|
|
1073
|
+
if (!child.killed)
|
|
1074
|
+
child.kill();
|
|
1075
|
+
} catch {
|
|
1076
|
+
}
|
|
1077
|
+
};
|
|
1078
|
+
process.once("exit", cleanupChild);
|
|
1079
|
+
process.once("SIGINT", () => {
|
|
1080
|
+
cleanupChild();
|
|
1081
|
+
process.exit(0);
|
|
1082
|
+
});
|
|
1083
|
+
process.once("SIGTERM", () => {
|
|
1084
|
+
cleanupChild();
|
|
1085
|
+
process.exit(0);
|
|
1086
|
+
});
|
|
1005
1087
|
const stdoutWebStream = nodeReadableToWebStream(child.stdout);
|
|
1006
1088
|
const stderrWebStream = nodeReadableToWebStream(child.stderr);
|
|
1007
1089
|
const marker = "ssr-watch: initial-build-complete";
|
|
@@ -1129,13 +1211,12 @@ var build = async (options) => {
|
|
|
1129
1211
|
const entry = pathMod3.resolve(__dirname2, options.entry);
|
|
1130
1212
|
const packageDir2 = pathMod3.dirname(fileURLToPath2(import.meta.url));
|
|
1131
1213
|
const clientScriptPath2 = pathMod3.resolve(packageDir2, "utils", "clientScript.js");
|
|
1132
|
-
const headPath = pathMod3.resolve(packageDir2, "utils", "Head");
|
|
1133
1214
|
let clientScript = "";
|
|
1134
1215
|
try {
|
|
1135
|
-
clientScript = (await fs.readFile(clientScriptPath2, "utf-8")).replace("$_MOD_PATH$", entry + getSuffix(options.mode))
|
|
1216
|
+
clientScript = (await fs.readFile(clientScriptPath2, "utf-8")).replace("$_MOD_PATH$", entry + getSuffix(options.mode));
|
|
1136
1217
|
} catch (err) {
|
|
1137
1218
|
const srcClientPath = pathMod3.resolve(packageDir2, "utils", "clientScript.tsx");
|
|
1138
|
-
clientScript = (await fs.readFile(srcClientPath, "utf-8")).replace("$_MOD_PATH$", entry + `?v=${Date.now()}`)
|
|
1219
|
+
clientScript = (await fs.readFile(srcClientPath, "utf-8")).replace("$_MOD_PATH$", entry + `?v=${Date.now()}`);
|
|
1139
1220
|
}
|
|
1140
1221
|
const tmpFilePath = pathMod3.join(os.tmpdir(), `hadars-client-${Date.now()}.tsx`);
|
|
1141
1222
|
await fs.writeFile(tmpFilePath, clientScript);
|
|
@@ -1199,7 +1280,7 @@ var run = async (options) => {
|
|
|
1199
1280
|
if (!isNode && workers > 1) {
|
|
1200
1281
|
const packageDir2 = pathMod3.dirname(fileURLToPath2(import.meta.url));
|
|
1201
1282
|
const workerJs = pathMod3.resolve(packageDir2, "ssr-render-worker.js");
|
|
1202
|
-
const workerTs = pathMod3.resolve(packageDir2, "
|
|
1283
|
+
const workerTs = pathMod3.resolve(packageDir2, "ssr-render-worker.ts");
|
|
1203
1284
|
const workerFile = existsSync2(workerJs) ? workerJs : workerTs;
|
|
1204
1285
|
const ssrBundlePath = pathMod3.resolve(__dirname2, HadarsFolder, SSR_FILENAME);
|
|
1205
1286
|
renderPool = new RenderWorkerPool(workerFile, workers, ssrBundlePath);
|
|
@@ -1436,11 +1517,44 @@ async function runCli(argv, cwd = process.cwd()) {
|
|
|
1436
1517
|
}
|
|
1437
1518
|
|
|
1438
1519
|
// cli.ts
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
process.
|
|
1443
|
-
}
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1520
|
+
if (typeof globalThis.Bun === "undefined" && typeof globalThis.Deno === "undefined") {
|
|
1521
|
+
const child = spawn2("bun", [process.argv[1], ...process.argv.slice(2)], {
|
|
1522
|
+
stdio: "inherit",
|
|
1523
|
+
env: process.env
|
|
1524
|
+
});
|
|
1525
|
+
const sigs = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
1526
|
+
const fwd = (sig) => () => {
|
|
1527
|
+
try {
|
|
1528
|
+
child.kill(sig);
|
|
1529
|
+
} catch {
|
|
1530
|
+
}
|
|
1531
|
+
};
|
|
1532
|
+
for (const sig of sigs)
|
|
1533
|
+
process.on(sig, fwd(sig));
|
|
1534
|
+
child.on("error", (err) => {
|
|
1535
|
+
for (const sig of sigs)
|
|
1536
|
+
process.removeAllListeners(sig);
|
|
1537
|
+
if (err.code !== "ENOENT") {
|
|
1538
|
+
console.error(err);
|
|
1539
|
+
process.exit(1);
|
|
1540
|
+
}
|
|
1541
|
+
runCli(process.argv).catch((e) => {
|
|
1542
|
+
console.error(e);
|
|
1543
|
+
try {
|
|
1544
|
+
process.exit(1);
|
|
1545
|
+
} catch (_) {
|
|
1546
|
+
throw e;
|
|
1547
|
+
}
|
|
1548
|
+
});
|
|
1549
|
+
});
|
|
1550
|
+
child.on("exit", (code) => process.exit(code ?? 0));
|
|
1551
|
+
} else {
|
|
1552
|
+
runCli(process.argv).catch((err) => {
|
|
1553
|
+
console.error(err);
|
|
1554
|
+
try {
|
|
1555
|
+
process.exit(1);
|
|
1556
|
+
} catch (_) {
|
|
1557
|
+
throw err;
|
|
1558
|
+
}
|
|
1559
|
+
});
|
|
1560
|
+
}
|
package/dist/index.cjs
CHANGED
|
@@ -32,6 +32,7 @@ var src_exports = {};
|
|
|
32
32
|
__export(src_exports, {
|
|
33
33
|
HadarsContext: () => HadarsContext,
|
|
34
34
|
HadarsHead: () => Head,
|
|
35
|
+
initServerDataCache: () => initServerDataCache,
|
|
35
36
|
loadModule: () => loadModule,
|
|
36
37
|
useServerData: () => useServerData
|
|
37
38
|
});
|
|
@@ -170,6 +171,11 @@ var AppProviderCSR = import_react.default.memo(({ children }) => {
|
|
|
170
171
|
});
|
|
171
172
|
var useApp = () => import_react.default.useContext(AppContext);
|
|
172
173
|
var clientServerDataCache = /* @__PURE__ */ new Map();
|
|
174
|
+
function initServerDataCache(data) {
|
|
175
|
+
for (const [k, v] of Object.entries(data)) {
|
|
176
|
+
clientServerDataCache.set(k, v);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
173
179
|
function useServerData(key, fn) {
|
|
174
180
|
const cacheKey = Array.isArray(key) ? key.join("\0") : key;
|
|
175
181
|
if (typeof window !== "undefined") {
|
|
@@ -184,11 +190,16 @@ function useServerData(key, fn) {
|
|
|
184
190
|
const existing = unsuspend.cache.get(cacheKey);
|
|
185
191
|
if (existing?.status === "suspense-resolved") {
|
|
186
192
|
try {
|
|
187
|
-
|
|
193
|
+
const value = fn();
|
|
194
|
+
unsuspend.cache.set(cacheKey, { status: "suspense-cached", value });
|
|
195
|
+
return value;
|
|
188
196
|
} catch {
|
|
189
197
|
return void 0;
|
|
190
198
|
}
|
|
191
199
|
}
|
|
200
|
+
if (existing?.status === "suspense-cached") {
|
|
201
|
+
return existing.value;
|
|
202
|
+
}
|
|
192
203
|
if (!existing) {
|
|
193
204
|
let result;
|
|
194
205
|
try {
|
|
@@ -298,6 +309,7 @@ function loadModule(path) {
|
|
|
298
309
|
0 && (module.exports = {
|
|
299
310
|
HadarsContext,
|
|
300
311
|
HadarsHead,
|
|
312
|
+
initServerDataCache,
|
|
301
313
|
loadModule,
|
|
302
314
|
useServerData
|
|
303
315
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -29,6 +29,9 @@ type UnsuspendEntry = {
|
|
|
29
29
|
value: unknown;
|
|
30
30
|
} | {
|
|
31
31
|
status: 'suspense-resolved';
|
|
32
|
+
} | {
|
|
33
|
+
status: 'suspense-cached';
|
|
34
|
+
value: unknown;
|
|
32
35
|
} | {
|
|
33
36
|
status: 'rejected';
|
|
34
37
|
reason: unknown;
|
|
@@ -102,6 +105,9 @@ interface HadarsRequest extends Request {
|
|
|
102
105
|
cookies: Record<string, string>;
|
|
103
106
|
}
|
|
104
107
|
|
|
108
|
+
/** Call this before hydrating to seed the client cache from the server's data.
|
|
109
|
+
* Invoked automatically by the hadars client bootstrap. */
|
|
110
|
+
declare function initServerDataCache(data: Record<string, unknown>): void;
|
|
105
111
|
/**
|
|
106
112
|
* Fetch async data on the server during SSR. Returns `undefined` on the first
|
|
107
113
|
* render pass(es) while the promise is in flight; returns the resolved value
|
|
@@ -157,4 +163,4 @@ declare const HadarsContext: React$1.FC<{
|
|
|
157
163
|
*/
|
|
158
164
|
declare function loadModule<T = any>(path: string): Promise<T>;
|
|
159
165
|
|
|
160
|
-
export { HadarsApp, HadarsContext, HadarsEntryModule, HadarsGetAfterRenderProps, HadarsGetClientProps, HadarsGetFinalProps, HadarsGetInitialProps, Head as HadarsHead, HadarsOptions, HadarsProps, HadarsRequest, loadModule, useServerData };
|
|
166
|
+
export { HadarsApp, HadarsContext, HadarsEntryModule, HadarsGetAfterRenderProps, HadarsGetClientProps, HadarsGetFinalProps, HadarsGetInitialProps, Head as HadarsHead, HadarsOptions, HadarsProps, HadarsRequest, initServerDataCache, loadModule, useServerData };
|
package/dist/index.js
CHANGED
|
@@ -131,6 +131,11 @@ var AppProviderCSR = React.memo(({ children }) => {
|
|
|
131
131
|
});
|
|
132
132
|
var useApp = () => React.useContext(AppContext);
|
|
133
133
|
var clientServerDataCache = /* @__PURE__ */ new Map();
|
|
134
|
+
function initServerDataCache(data) {
|
|
135
|
+
for (const [k, v] of Object.entries(data)) {
|
|
136
|
+
clientServerDataCache.set(k, v);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
134
139
|
function useServerData(key, fn) {
|
|
135
140
|
const cacheKey = Array.isArray(key) ? key.join("\0") : key;
|
|
136
141
|
if (typeof window !== "undefined") {
|
|
@@ -145,11 +150,16 @@ function useServerData(key, fn) {
|
|
|
145
150
|
const existing = unsuspend.cache.get(cacheKey);
|
|
146
151
|
if (existing?.status === "suspense-resolved") {
|
|
147
152
|
try {
|
|
148
|
-
|
|
153
|
+
const value = fn();
|
|
154
|
+
unsuspend.cache.set(cacheKey, { status: "suspense-cached", value });
|
|
155
|
+
return value;
|
|
149
156
|
} catch {
|
|
150
157
|
return void 0;
|
|
151
158
|
}
|
|
152
159
|
}
|
|
160
|
+
if (existing?.status === "suspense-cached") {
|
|
161
|
+
return existing.value;
|
|
162
|
+
}
|
|
153
163
|
if (!existing) {
|
|
154
164
|
let result;
|
|
155
165
|
try {
|
|
@@ -258,6 +268,7 @@ function loadModule(path) {
|
|
|
258
268
|
export {
|
|
259
269
|
HadarsContext,
|
|
260
270
|
Head as HadarsHead,
|
|
271
|
+
initServerDataCache,
|
|
261
272
|
loadModule,
|
|
262
273
|
useServerData
|
|
263
274
|
};
|
|
@@ -47,7 +47,7 @@ function buildReactPage(R, appProps, clientProps) {
|
|
|
47
47
|
id: "hadars",
|
|
48
48
|
type: "application/json",
|
|
49
49
|
dangerouslySetInnerHTML: {
|
|
50
|
-
__html: JSON.stringify({ hadars: { props: clientProps } })
|
|
50
|
+
__html: JSON.stringify({ hadars: { props: clientProps } }).replace(/</g, "\\u003c")
|
|
51
51
|
}
|
|
52
52
|
})
|
|
53
53
|
);
|
package/dist/ssr-watch.js
CHANGED
|
@@ -213,7 +213,6 @@ var buildCompilerConfig = (entry2, opts, includeHotPlugin) => {
|
|
|
213
213
|
};
|
|
214
214
|
return {
|
|
215
215
|
entry: entry2,
|
|
216
|
-
resolve: resolveConfig,
|
|
217
216
|
output: {
|
|
218
217
|
...opts.output,
|
|
219
218
|
clean: false
|
|
@@ -233,6 +232,13 @@ var buildCompilerConfig = (entry2, opts, includeHotPlugin) => {
|
|
|
233
232
|
...extraPlugins
|
|
234
233
|
],
|
|
235
234
|
...localConfig,
|
|
235
|
+
// Merge base resolve (modules, tsConfig, extensions) with per-build resolve
|
|
236
|
+
// (alias, mainFields). The spread order matters: resolveConfig wins for keys
|
|
237
|
+
// it defines, localConfig.resolve wins for keys it defines exclusively.
|
|
238
|
+
resolve: {
|
|
239
|
+
...localConfig.resolve,
|
|
240
|
+
...resolveConfig
|
|
241
|
+
},
|
|
236
242
|
// HMR is not implemented for module chunk format, so disable outputModule
|
|
237
243
|
// for client builds. SSR builds still need it for dynamic import() of exports.
|
|
238
244
|
experiments: {
|
package/dist/utils/Head.tsx
CHANGED
|
@@ -231,17 +231,27 @@ export function useServerData<T>(key: string | string[], fn: () => Promise<T> |
|
|
|
231
231
|
const existing = unsuspend.cache.get(cacheKey);
|
|
232
232
|
|
|
233
233
|
// Suspense promise has resolved — re-call fn() so the hook returns its value
|
|
234
|
-
// synchronously from its own internal cache.
|
|
235
|
-
//
|
|
236
|
-
//
|
|
234
|
+
// synchronously from its own internal cache. Cache the result as
|
|
235
|
+
// 'suspense-cached' so later renders (e.g. the final renderToString in
|
|
236
|
+
// buildSsrResponse, which runs after getFinalProps may have cleared the
|
|
237
|
+
// user's QueryClient) can return the value without calling fn() again.
|
|
238
|
+
// NOT stored as 'fulfilled' so it is never included in serverData sent to
|
|
239
|
+
// the client — the Suspense library owns its own hydration.
|
|
237
240
|
if (existing?.status === 'suspense-resolved') {
|
|
238
241
|
try {
|
|
239
|
-
|
|
242
|
+
const value = fn() as T;
|
|
243
|
+
unsuspend.cache.set(cacheKey, { status: 'suspense-cached', value });
|
|
244
|
+
return value;
|
|
240
245
|
} catch {
|
|
241
246
|
return undefined;
|
|
242
247
|
}
|
|
243
248
|
}
|
|
244
249
|
|
|
250
|
+
// Return the cached Suspense value on all subsequent renders.
|
|
251
|
+
if (existing?.status === 'suspense-cached') {
|
|
252
|
+
return existing.value as T;
|
|
253
|
+
}
|
|
254
|
+
|
|
245
255
|
if (!existing) {
|
|
246
256
|
// First encounter — call fn(), which may:
|
|
247
257
|
// (a) return a Promise<T> — normal async usage (serialised for the client)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { hydrateRoot, createRoot } from 'react-dom/client';
|
|
3
3
|
import type { HadarsEntryModule } from '../types/ninety';
|
|
4
|
-
import { initServerDataCache } from '
|
|
4
|
+
import { initServerDataCache } from 'hadars';
|
|
5
5
|
import * as _appMod from '$_MOD_PATH$';
|
|
6
6
|
|
|
7
7
|
const appMod = _appMod as HadarsEntryModule<{}>;
|
package/package.json
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hadars",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Minimal SSR framework for React — rspack, HMR, TypeScript, Bun/Node/Deno",
|
|
5
5
|
"module": "./dist/index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
|
-
"hadars": "dist/cli.js"
|
|
9
|
-
"hadars-bun": "cli-bun.ts"
|
|
8
|
+
"hadars": "dist/cli.js"
|
|
10
9
|
},
|
|
11
10
|
"main": "dist/index.js",
|
|
12
11
|
"types": "dist/index.d.ts",
|
|
@@ -15,7 +14,6 @@
|
|
|
15
14
|
"src",
|
|
16
15
|
"cli-lib.ts",
|
|
17
16
|
"cli.ts",
|
|
18
|
-
"cli-bun.ts",
|
|
19
17
|
"index.ts",
|
|
20
18
|
"LICENSE"
|
|
21
19
|
],
|
package/src/build.ts
CHANGED
|
@@ -72,6 +72,9 @@ type PendingEntry = PendingRenderString | PendingRenderStream;
|
|
|
72
72
|
class RenderWorkerPool {
|
|
73
73
|
private workers: any[] = [];
|
|
74
74
|
private pending = new Map<number, PendingEntry>();
|
|
75
|
+
// Track which pending IDs were dispatched to each worker so we can reject
|
|
76
|
+
// them when that worker crashes.
|
|
77
|
+
private workerPending = new Map<any, Set<number>>();
|
|
75
78
|
private nextId = 0;
|
|
76
79
|
private rrIndex = 0;
|
|
77
80
|
|
|
@@ -85,6 +88,7 @@ class RenderWorkerPool {
|
|
|
85
88
|
import('node:worker_threads').then(({ Worker }) => {
|
|
86
89
|
for (let i = 0; i < size; i++) {
|
|
87
90
|
const w = new Worker(workerPath, { workerData: { ssrBundlePath } });
|
|
91
|
+
this.workerPending.set(w, new Set());
|
|
88
92
|
w.on('message', (msg: any) => {
|
|
89
93
|
const { id, type, html, error, chunk } = msg;
|
|
90
94
|
const p = this.pending.get(id);
|
|
@@ -96,6 +100,7 @@ class RenderWorkerPool {
|
|
|
96
100
|
return; // keep entry until 'done'
|
|
97
101
|
}
|
|
98
102
|
this.pending.delete(id);
|
|
103
|
+
this.workerPending.get(w)?.delete(id);
|
|
99
104
|
if (type === 'done') p.controller.close();
|
|
100
105
|
else p.controller.error(new Error(error ?? 'Stream error'));
|
|
101
106
|
return;
|
|
@@ -103,11 +108,19 @@ class RenderWorkerPool {
|
|
|
103
108
|
|
|
104
109
|
// renderString
|
|
105
110
|
this.pending.delete(id);
|
|
111
|
+
this.workerPending.get(w)?.delete(id);
|
|
106
112
|
if (error) p.reject(new Error(error));
|
|
107
113
|
else p.resolve(html);
|
|
108
114
|
});
|
|
109
115
|
w.on('error', (err: Error) => {
|
|
110
116
|
console.error('[hadars] Render worker error:', err);
|
|
117
|
+
this._handleWorkerDeath(w, err);
|
|
118
|
+
});
|
|
119
|
+
w.on('exit', (code: number) => {
|
|
120
|
+
if (code !== 0) {
|
|
121
|
+
console.error(`[hadars] Render worker exited with code ${code}`);
|
|
122
|
+
this._handleWorkerDeath(w, new Error(`Render worker exited with code ${code}`));
|
|
123
|
+
}
|
|
111
124
|
});
|
|
112
125
|
this.workers.push(w);
|
|
113
126
|
}
|
|
@@ -116,7 +129,28 @@ class RenderWorkerPool {
|
|
|
116
129
|
});
|
|
117
130
|
}
|
|
118
131
|
|
|
119
|
-
private
|
|
132
|
+
private _handleWorkerDeath(w: any, err: Error) {
|
|
133
|
+
// Remove the dead worker from the rotation so it is never selected again.
|
|
134
|
+
const idx = this.workers.indexOf(w);
|
|
135
|
+
if (idx !== -1) this.workers.splice(idx, 1);
|
|
136
|
+
|
|
137
|
+
// Reject every in-flight request that was sent to this worker.
|
|
138
|
+
const ids = this.workerPending.get(w);
|
|
139
|
+
if (ids) {
|
|
140
|
+
for (const id of ids) {
|
|
141
|
+
const p = this.pending.get(id);
|
|
142
|
+
if (p) {
|
|
143
|
+
this.pending.delete(id);
|
|
144
|
+
if (p.kind === 'renderString') p.reject(err);
|
|
145
|
+
else p.controller.error(err);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
this.workerPending.delete(w);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private nextWorker(): any | undefined {
|
|
153
|
+
if (this.workers.length === 0) return undefined;
|
|
120
154
|
const w = this.workers[this.rrIndex % this.workers.length];
|
|
121
155
|
this.rrIndex++;
|
|
122
156
|
return w;
|
|
@@ -125,9 +159,21 @@ class RenderWorkerPool {
|
|
|
125
159
|
/** Offload a full renderToString call. Returns the HTML string. */
|
|
126
160
|
renderString(appProps: Record<string, unknown>, clientProps: Record<string, unknown>): Promise<string> {
|
|
127
161
|
return new Promise((resolve, reject) => {
|
|
162
|
+
const w = this.nextWorker();
|
|
163
|
+
if (!w) {
|
|
164
|
+
reject(new Error('[hadars] No render workers available'));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
128
167
|
const id = this.nextId++;
|
|
129
168
|
this.pending.set(id, { kind: 'renderString', resolve, reject });
|
|
130
|
-
this.
|
|
169
|
+
this.workerPending.get(w)?.add(id);
|
|
170
|
+
try {
|
|
171
|
+
w.postMessage({ id, type: 'renderString', appProps, clientProps });
|
|
172
|
+
} catch (err) {
|
|
173
|
+
this.pending.delete(id);
|
|
174
|
+
this.workerPending.get(w)?.delete(id);
|
|
175
|
+
reject(err);
|
|
176
|
+
}
|
|
131
177
|
});
|
|
132
178
|
}
|
|
133
179
|
|
|
@@ -138,11 +184,24 @@ class RenderWorkerPool {
|
|
|
138
184
|
const stream = new ReadableStream<Uint8Array>({
|
|
139
185
|
start: (ctrl) => { controller = ctrl; },
|
|
140
186
|
});
|
|
187
|
+
const w = this.nextWorker();
|
|
188
|
+
if (!w) {
|
|
189
|
+
// Immediately error the stream if no workers are available.
|
|
190
|
+
queueMicrotask(() => controller.error(new Error('[hadars] No render workers available')));
|
|
191
|
+
return stream;
|
|
192
|
+
}
|
|
141
193
|
const id = this.nextId++;
|
|
142
194
|
// Store controller before postMessage so the handler is ready when
|
|
143
195
|
// the first chunk arrives.
|
|
144
196
|
this.pending.set(id, { kind: 'renderStream', controller });
|
|
145
|
-
this.
|
|
197
|
+
this.workerPending.get(w)?.add(id);
|
|
198
|
+
try {
|
|
199
|
+
w.postMessage({ id, type: 'renderStream', appProps, clientProps });
|
|
200
|
+
} catch (err) {
|
|
201
|
+
this.pending.delete(id);
|
|
202
|
+
this.workerPending.get(w)?.delete(id);
|
|
203
|
+
queueMicrotask(() => controller.error(err));
|
|
204
|
+
}
|
|
146
205
|
return stream;
|
|
147
206
|
}
|
|
148
207
|
|
|
@@ -341,13 +400,10 @@ export const dev = async (options: HadarsRuntimeOptions) => {
|
|
|
341
400
|
const packageDir = pathMod.dirname(fileURLToPath(import.meta.url));
|
|
342
401
|
const clientScriptPath = pathMod.resolve(packageDir, 'utils', 'clientScript.tsx');
|
|
343
402
|
|
|
344
|
-
const headPath = pathMod.resolve(packageDir, 'utils', 'Head');
|
|
345
|
-
|
|
346
403
|
let clientScript = '';
|
|
347
404
|
try {
|
|
348
405
|
clientScript = (await fs.readFile(clientScriptPath, 'utf-8'))
|
|
349
|
-
.replace('$_MOD_PATH$', entry + getSuffix(options.mode))
|
|
350
|
-
.replace('$_HEAD_PATH$', headPath);
|
|
406
|
+
.replace('$_MOD_PATH$', entry + getSuffix(options.mode));
|
|
351
407
|
}
|
|
352
408
|
catch (err) {
|
|
353
409
|
console.error("Failed to read client script from package dist, falling back to src", err);
|
|
@@ -421,6 +477,12 @@ export const dev = async (options: HadarsRuntimeOptions) => {
|
|
|
421
477
|
], { stdio: 'pipe' });
|
|
422
478
|
child.stdin?.end();
|
|
423
479
|
|
|
480
|
+
// Ensure the SSR watcher child is killed when this process exits.
|
|
481
|
+
const cleanupChild = () => { try { if (!child.killed) child.kill(); } catch {} };
|
|
482
|
+
process.once('exit', cleanupChild);
|
|
483
|
+
process.once('SIGINT', () => { cleanupChild(); process.exit(0); });
|
|
484
|
+
process.once('SIGTERM', () => { cleanupChild(); process.exit(0); });
|
|
485
|
+
|
|
424
486
|
// Convert Node.js Readable streams to Web ReadableStream so the rest of
|
|
425
487
|
// the logic works identically across all runtimes.
|
|
426
488
|
const stdoutWebStream = nodeReadableToWebStream(child.stdout!);
|
|
@@ -553,17 +615,14 @@ export const build = async (options: HadarsRuntimeOptions) => {
|
|
|
553
615
|
// prepare client script
|
|
554
616
|
const packageDir = pathMod.dirname(fileURLToPath(import.meta.url));
|
|
555
617
|
const clientScriptPath = pathMod.resolve(packageDir, 'utils', 'clientScript.js');
|
|
556
|
-
const headPath = pathMod.resolve(packageDir, 'utils', 'Head');
|
|
557
618
|
let clientScript = '';
|
|
558
619
|
try {
|
|
559
620
|
clientScript = (await fs.readFile(clientScriptPath, 'utf-8'))
|
|
560
|
-
.replace('$_MOD_PATH$', entry + getSuffix(options.mode))
|
|
561
|
-
.replace('$_HEAD_PATH$', headPath);
|
|
621
|
+
.replace('$_MOD_PATH$', entry + getSuffix(options.mode));
|
|
562
622
|
} catch (err) {
|
|
563
623
|
const srcClientPath = pathMod.resolve(packageDir, 'utils', 'clientScript.tsx');
|
|
564
624
|
clientScript = (await fs.readFile(srcClientPath, 'utf-8'))
|
|
565
|
-
.replace('$_MOD_PATH$', entry + `?v=${Date.now()}`)
|
|
566
|
-
.replace('$_HEAD_PATH$', pathMod.resolve(packageDir, 'utils', 'Head'));
|
|
625
|
+
.replace('$_MOD_PATH$', entry + `?v=${Date.now()}`);
|
|
567
626
|
}
|
|
568
627
|
|
|
569
628
|
const tmpFilePath = pathMod.join(os.tmpdir(), `hadars-client-${Date.now()}.tsx`);
|
|
@@ -645,7 +704,7 @@ export const run = async (options: HadarsRuntimeOptions) => {
|
|
|
645
704
|
if (!isNode && workers > 1) {
|
|
646
705
|
const packageDir = pathMod.dirname(fileURLToPath(import.meta.url));
|
|
647
706
|
const workerJs = pathMod.resolve(packageDir, 'ssr-render-worker.js');
|
|
648
|
-
const workerTs = pathMod.resolve(packageDir, '
|
|
707
|
+
const workerTs = pathMod.resolve(packageDir, 'ssr-render-worker.ts');
|
|
649
708
|
const workerFile = existsSync(workerJs) ? workerJs : workerTs;
|
|
650
709
|
const ssrBundlePath = pathMod.resolve(__dirname, HadarsFolder, SSR_FILENAME);
|
|
651
710
|
renderPool = new RenderWorkerPool(workerFile, workers, ssrBundlePath);
|
package/src/index.tsx
CHANGED
|
@@ -9,7 +9,7 @@ export type {
|
|
|
9
9
|
HadarsEntryModule,
|
|
10
10
|
HadarsApp,
|
|
11
11
|
} from "./types/ninety";
|
|
12
|
-
export { Head as HadarsHead, useServerData } from './utils/Head';
|
|
12
|
+
export { Head as HadarsHead, useServerData, initServerDataCache } from './utils/Head';
|
|
13
13
|
import { AppProviderSSR, AppProviderCSR } from "./utils/Head";
|
|
14
14
|
|
|
15
15
|
export const HadarsContext = typeof window === 'undefined' ? AppProviderSSR : AppProviderCSR;
|
package/src/ssr-render-worker.ts
CHANGED
|
@@ -77,7 +77,7 @@ function buildReactPage(R: any, appProps: Record<string, unknown>, clientProps:
|
|
|
77
77
|
id: 'hadars',
|
|
78
78
|
type: 'application/json',
|
|
79
79
|
dangerouslySetInnerHTML: {
|
|
80
|
-
__html: JSON.stringify({ hadars: { props: clientProps } }),
|
|
80
|
+
__html: JSON.stringify({ hadars: { props: clientProps } }).replace(/</g, '\\u003c'),
|
|
81
81
|
},
|
|
82
82
|
}),
|
|
83
83
|
);
|
package/src/types/ninety.ts
CHANGED
|
@@ -27,6 +27,7 @@ export type UnsuspendEntry =
|
|
|
27
27
|
| { status: 'pending'; promise: Promise<unknown> }
|
|
28
28
|
| { status: 'fulfilled'; value: unknown }
|
|
29
29
|
| { status: 'suspense-resolved' }
|
|
30
|
+
| { status: 'suspense-cached'; value: unknown }
|
|
30
31
|
| { status: 'rejected'; reason: unknown };
|
|
31
32
|
|
|
32
33
|
/** @internal Populated by the framework's render loop — use useServerData() instead. */
|
package/src/utils/Head.tsx
CHANGED
|
@@ -231,17 +231,27 @@ export function useServerData<T>(key: string | string[], fn: () => Promise<T> |
|
|
|
231
231
|
const existing = unsuspend.cache.get(cacheKey);
|
|
232
232
|
|
|
233
233
|
// Suspense promise has resolved — re-call fn() so the hook returns its value
|
|
234
|
-
// synchronously from its own internal cache.
|
|
235
|
-
//
|
|
236
|
-
//
|
|
234
|
+
// synchronously from its own internal cache. Cache the result as
|
|
235
|
+
// 'suspense-cached' so later renders (e.g. the final renderToString in
|
|
236
|
+
// buildSsrResponse, which runs after getFinalProps may have cleared the
|
|
237
|
+
// user's QueryClient) can return the value without calling fn() again.
|
|
238
|
+
// NOT stored as 'fulfilled' so it is never included in serverData sent to
|
|
239
|
+
// the client — the Suspense library owns its own hydration.
|
|
237
240
|
if (existing?.status === 'suspense-resolved') {
|
|
238
241
|
try {
|
|
239
|
-
|
|
242
|
+
const value = fn() as T;
|
|
243
|
+
unsuspend.cache.set(cacheKey, { status: 'suspense-cached', value });
|
|
244
|
+
return value;
|
|
240
245
|
} catch {
|
|
241
246
|
return undefined;
|
|
242
247
|
}
|
|
243
248
|
}
|
|
244
249
|
|
|
250
|
+
// Return the cached Suspense value on all subsequent renders.
|
|
251
|
+
if (existing?.status === 'suspense-cached') {
|
|
252
|
+
return existing.value as T;
|
|
253
|
+
}
|
|
254
|
+
|
|
245
255
|
if (!existing) {
|
|
246
256
|
// First encounter — call fn(), which may:
|
|
247
257
|
// (a) return a Promise<T> — normal async usage (serialised for the client)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { hydrateRoot, createRoot } from 'react-dom/client';
|
|
3
3
|
import type { HadarsEntryModule } from '../types/ninety';
|
|
4
|
-
import { initServerDataCache } from '
|
|
4
|
+
import { initServerDataCache } from 'hadars';
|
|
5
5
|
import * as _appMod from '$_MOD_PATH$';
|
|
6
6
|
|
|
7
7
|
const appMod = _appMod as HadarsEntryModule<{}>;
|
package/src/utils/response.tsx
CHANGED
|
@@ -181,7 +181,7 @@ export const getReactResponse = async (
|
|
|
181
181
|
context,
|
|
182
182
|
})} />
|
|
183
183
|
</div>
|
|
184
|
-
<script id="hadars" type="application/json" dangerouslySetInnerHTML={{ __html: JSON.stringify({ hadars: { props: clientProps } }) }}></script>
|
|
184
|
+
<script id="hadars" type="application/json" dangerouslySetInnerHTML={{ __html: JSON.stringify({ hadars: { props: clientProps } }).replace(/</g, '\\u003c') }}></script>
|
|
185
185
|
</>
|
|
186
186
|
)
|
|
187
187
|
|
package/src/utils/rspack.ts
CHANGED
|
@@ -260,7 +260,6 @@ const buildCompilerConfig = (
|
|
|
260
260
|
|
|
261
261
|
return {
|
|
262
262
|
entry,
|
|
263
|
-
resolve: resolveConfig,
|
|
264
263
|
output: {
|
|
265
264
|
...opts.output,
|
|
266
265
|
clean: false,
|
|
@@ -280,6 +279,13 @@ const buildCompilerConfig = (
|
|
|
280
279
|
...extraPlugins,
|
|
281
280
|
],
|
|
282
281
|
...localConfig,
|
|
282
|
+
// Merge base resolve (modules, tsConfig, extensions) with per-build resolve
|
|
283
|
+
// (alias, mainFields). The spread order matters: resolveConfig wins for keys
|
|
284
|
+
// it defines, localConfig.resolve wins for keys it defines exclusively.
|
|
285
|
+
resolve: {
|
|
286
|
+
...localConfig.resolve,
|
|
287
|
+
...resolveConfig,
|
|
288
|
+
},
|
|
283
289
|
// HMR is not implemented for module chunk format, so disable outputModule
|
|
284
290
|
// for client builds. SSR builds still need it for dynamic import() of exports.
|
|
285
291
|
experiments: {
|
package/cli-bun.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
import { runCli } from './cli-lib'
|
|
3
|
-
|
|
4
|
-
runCli(process.argv).catch((err) => {
|
|
5
|
-
console.error(err)
|
|
6
|
-
// When Bun runs a script, allow non-zero exit codes to propagate
|
|
7
|
-
try {
|
|
8
|
-
process.exit(1)
|
|
9
|
-
} catch (_) {
|
|
10
|
-
// Some Bun environments may not allow process.exit; just rethrow
|
|
11
|
-
throw err
|
|
12
|
-
}
|
|
13
|
-
})
|