@taujs/react 0.1.0 → 0.1.2
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 +4 -0
- package/dist/index.d.ts +76 -9
- package/dist/index.js +111 -31
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,3 +11,7 @@
|
|
|
11
11
|
React Renderer: CSR, SSR, Streaming SSR
|
|
12
12
|
|
|
13
13
|
https://taujs.dev/
|
|
14
|
+
|
|
15
|
+
A lightweight, production-ready React SSR library with streaming capabilities, built for modern TypeScript applications. Designed as part of the taujs (τjs) ecosystem but fully standalone and runtime-agnostic.
|
|
16
|
+
|
|
17
|
+
https://taujs.dev/renderers/react/
|
package/dist/index.d.ts
CHANGED
|
@@ -22,10 +22,10 @@ type UILogger = {
|
|
|
22
22
|
error: (...args: unknown[]) => void;
|
|
23
23
|
};
|
|
24
24
|
type ServerLogs = {
|
|
25
|
-
info: (
|
|
26
|
-
warn: (
|
|
27
|
-
error: (
|
|
28
|
-
debug?: (category: string,
|
|
25
|
+
info: (meta?: unknown, message?: string) => void;
|
|
26
|
+
warn: (meta?: unknown, message?: string) => void;
|
|
27
|
+
error: (meta?: unknown, message?: string) => void;
|
|
28
|
+
debug?: (category: string, meta?: unknown, message?: string) => void;
|
|
29
29
|
child?: (ctx: Record<string, unknown>) => ServerLogs;
|
|
30
30
|
isDebugEnabled?: (category: string) => boolean;
|
|
31
31
|
};
|
|
@@ -56,6 +56,10 @@ type StreamOptions = {
|
|
|
56
56
|
/** Whether to use cork/uncork for batched writes (default: true) */
|
|
57
57
|
useCork?: boolean;
|
|
58
58
|
};
|
|
59
|
+
type HeadContext<T extends Record<string, unknown> = Record<string, unknown>> = {
|
|
60
|
+
data: T;
|
|
61
|
+
meta: Record<string, unknown>;
|
|
62
|
+
};
|
|
59
63
|
type SSRResult = {
|
|
60
64
|
headContent: string;
|
|
61
65
|
appHtml: string;
|
|
@@ -68,10 +72,7 @@ declare function createRenderer<T extends Record<string, unknown>>({ appComponen
|
|
|
68
72
|
appComponent: (props: {
|
|
69
73
|
location: string;
|
|
70
74
|
}) => React.ReactElement;
|
|
71
|
-
headContent: (ctx:
|
|
72
|
-
data: T;
|
|
73
|
-
meta: Record<string, unknown>;
|
|
74
|
-
}) => string;
|
|
75
|
+
headContent: (ctx: HeadContext<T>) => string;
|
|
75
76
|
enableDebug?: boolean;
|
|
76
77
|
logger?: LoggerLike;
|
|
77
78
|
streamOptions?: StreamOptions;
|
|
@@ -85,4 +86,70 @@ declare function createRenderer<T extends Record<string, unknown>>({ appComponen
|
|
|
85
86
|
};
|
|
86
87
|
};
|
|
87
88
|
|
|
88
|
-
|
|
89
|
+
/**
|
|
90
|
+
* τjs Client Data Bridge
|
|
91
|
+
*
|
|
92
|
+
* Provides framework-agnostic primitives for accessing route data:
|
|
93
|
+
* - SSR hydration (window.__INITIAL_DATA__)
|
|
94
|
+
* - Client-side fetch (/__taujs/data endpoint)
|
|
95
|
+
*
|
|
96
|
+
* This is a transport layer only. For data orchestration (caching, refetch, etc.),
|
|
97
|
+
* use TanStack Query or similar.
|
|
98
|
+
*/
|
|
99
|
+
type RouteData = Record<string, unknown>;
|
|
100
|
+
/**
|
|
101
|
+
* Error thrown when fetchRouteData receives a non-2xx response.
|
|
102
|
+
* Contains structured error information from the server.
|
|
103
|
+
*/
|
|
104
|
+
declare class RouteDataError extends Error {
|
|
105
|
+
readonly status: number;
|
|
106
|
+
readonly statusText: string;
|
|
107
|
+
readonly code?: string;
|
|
108
|
+
readonly body?: unknown;
|
|
109
|
+
constructor(message: string, opts: {
|
|
110
|
+
status: number;
|
|
111
|
+
statusText: string;
|
|
112
|
+
code?: string;
|
|
113
|
+
body?: unknown;
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Read SSR boot data from window.__INITIAL_DATA__ exactly once.
|
|
118
|
+
* Subsequent calls return null (forces client-side fetch).
|
|
119
|
+
*
|
|
120
|
+
* Returns null on server (typeof window === 'undefined').
|
|
121
|
+
*/
|
|
122
|
+
declare function readInitialDataOnce<T extends RouteData = RouteData>(): T | null;
|
|
123
|
+
/**
|
|
124
|
+
* Fetch route data from the τjs data endpoint.
|
|
125
|
+
*
|
|
126
|
+
* Calls: GET /__taujs/data?url=<pathname>
|
|
127
|
+
* Returns: { data: T }
|
|
128
|
+
*
|
|
129
|
+
* Throws RouteDataError on non-2xx responses with structured error info.
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* const data = await fetchRouteData('/app/dashboard');
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* try {
|
|
136
|
+
* const data = await fetchRouteData('/app/dashboard');
|
|
137
|
+
* } catch (err) {
|
|
138
|
+
* if (err instanceof RouteDataError && err.status === 404) {
|
|
139
|
+
* // Handle not found
|
|
140
|
+
* }
|
|
141
|
+
* }
|
|
142
|
+
*/
|
|
143
|
+
declare function fetchRouteData<T extends RouteData = RouteData>(pathname: string, init?: RequestInit): Promise<T>;
|
|
144
|
+
/**
|
|
145
|
+
* Get the current browser path (pathname + search).
|
|
146
|
+
* Does not include hash.
|
|
147
|
+
*
|
|
148
|
+
* Returns null on server (typeof window === 'undefined').
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* const path = getCurrentPath(); // "/app/dashboard?tab=overview"
|
|
152
|
+
*/
|
|
153
|
+
declare function getCurrentPath(): string | null;
|
|
154
|
+
|
|
155
|
+
export { type HeadContext, type HydrateAppOptions, type RenderCallbacks, type RouteData, RouteDataError, type SSRStore, SSRStoreProvider, type ServerLogs, type StreamOptions, createRenderer, createSSRStore, fetchRouteData, getCurrentPath, hydrateApp, readInitialDataOnce, useSSRStore };
|
package/dist/index.js
CHANGED
|
@@ -90,10 +90,16 @@ var splitMsgAndMeta = (args) => {
|
|
|
90
90
|
};
|
|
91
91
|
function createUILogger(logger, opts = {}) {
|
|
92
92
|
const { debugCategory = "ssr", context, preferDebug = false, enableDebug = false } = opts;
|
|
93
|
-
if (!enableDebug)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
93
|
+
if (!enableDebug) {
|
|
94
|
+
return {
|
|
95
|
+
log: () => {
|
|
96
|
+
},
|
|
97
|
+
warn: () => {
|
|
98
|
+
},
|
|
99
|
+
error: () => {
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
97
103
|
const looksServer = !!logger && ("info" in logger || "debug" in logger || "child" in logger || "isDebugEnabled" in logger);
|
|
98
104
|
if (looksServer) {
|
|
99
105
|
let s = logger;
|
|
@@ -103,11 +109,11 @@ function createUILogger(logger, opts = {}) {
|
|
|
103
109
|
} catch {
|
|
104
110
|
}
|
|
105
111
|
}
|
|
106
|
-
const info = s.info ? s.info
|
|
107
|
-
const warn = s.warn ? s.warn
|
|
108
|
-
const error = s.error ? s.error
|
|
109
|
-
const debug = s.debug ? s.debug
|
|
110
|
-
const isDebugEnabled = s.isDebugEnabled ? s.isDebugEnabled
|
|
112
|
+
const info = s.info ? (msg, meta) => s.info(meta, msg) : (msg, meta) => meta ? console.log(msg, meta) : console.log(msg);
|
|
113
|
+
const warn = s.warn ? (msg, meta) => s.warn(meta, msg) : (msg, meta) => meta ? console.warn(msg, meta) : console.warn(msg);
|
|
114
|
+
const error = s.error ? (msg, meta) => s.error(meta, msg) : (msg, meta) => meta ? console.error(msg, meta) : console.error(msg);
|
|
115
|
+
const debug = s.debug ? (category, msg, meta) => s.debug(category, meta, msg) : void 0;
|
|
116
|
+
const isDebugEnabled = s.isDebugEnabled ? (category) => s.isDebugEnabled(category) : void 0;
|
|
111
117
|
return {
|
|
112
118
|
log: (...args) => {
|
|
113
119
|
const { msg, meta } = splitMsgAndMeta(args);
|
|
@@ -345,6 +351,8 @@ function createStreamController(writable, logger) {
|
|
|
345
351
|
|
|
346
352
|
// src/SSRRender.tsx
|
|
347
353
|
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
354
|
+
var NOOP = () => {
|
|
355
|
+
};
|
|
348
356
|
function createRenderer({
|
|
349
357
|
appComponent,
|
|
350
358
|
headContent,
|
|
@@ -385,7 +393,13 @@ function createRenderer({
|
|
|
385
393
|
}
|
|
386
394
|
};
|
|
387
395
|
const renderStream = (writable, callbacks = {}, initialData, location, bootstrapModules, meta = {}, cspNonce, signal, opts) => {
|
|
388
|
-
const
|
|
396
|
+
const cb = {
|
|
397
|
+
onHead: callbacks.onHead ?? NOOP,
|
|
398
|
+
onShellReady: callbacks.onShellReady ?? NOOP,
|
|
399
|
+
onAllReady: callbacks.onAllReady ?? NOOP,
|
|
400
|
+
onFinish: callbacks.onFinish ?? NOOP,
|
|
401
|
+
onError: callbacks.onError ?? NOOP
|
|
402
|
+
};
|
|
389
403
|
const { log, warn, error } = createUILogger(opts?.logger ?? logger, {
|
|
390
404
|
debugCategory: "ssr",
|
|
391
405
|
context: { scope: "react-streaming" },
|
|
@@ -412,17 +426,17 @@ function createRenderer({
|
|
|
412
426
|
const { cleanup: guardsCleanup } = wireWritableGuards(writable, {
|
|
413
427
|
benignAbort: (why) => controller.benignAbort(why),
|
|
414
428
|
fatalAbort: (err) => {
|
|
415
|
-
onError
|
|
429
|
+
cb.onError(err);
|
|
416
430
|
controller.fatalAbort(err);
|
|
417
431
|
},
|
|
418
|
-
onError,
|
|
432
|
+
onError: cb.onError,
|
|
419
433
|
onFinish: () => controller.complete("Stream finished (normal completion)")
|
|
420
434
|
});
|
|
421
435
|
controller.setGuardsCleanup(guardsCleanup);
|
|
422
436
|
const stopShellTimer = startShellTimer(effectiveShellTimeout, () => {
|
|
423
437
|
if (controller.isAborted) return;
|
|
424
438
|
const timeoutErr = new Error(`Shell not ready after ${effectiveShellTimeout}ms`);
|
|
425
|
-
onError
|
|
439
|
+
cb.onError(timeoutErr);
|
|
426
440
|
controller.fatalAbort(timeoutErr);
|
|
427
441
|
});
|
|
428
442
|
controller.setStopShellTimer(stopShellTimer);
|
|
@@ -451,16 +465,15 @@ function createRenderer({
|
|
|
451
465
|
}
|
|
452
466
|
}
|
|
453
467
|
const head = headContent({ data: headData ?? {}, meta });
|
|
454
|
-
const canCork = effectiveUseCork && typeof writable
|
|
455
|
-
if (canCork)
|
|
468
|
+
const canCork = effectiveUseCork && typeof writable.cork === "function" && typeof writable.uncork === "function";
|
|
469
|
+
if (canCork)
|
|
456
470
|
try {
|
|
457
471
|
writable.cork();
|
|
458
472
|
} catch {
|
|
459
473
|
}
|
|
460
|
-
}
|
|
461
474
|
let wroteOk = true;
|
|
462
475
|
try {
|
|
463
|
-
const res = writable
|
|
476
|
+
const res = typeof writable.write === "function" ? writable.write(head) : true;
|
|
464
477
|
wroteOk = res !== false;
|
|
465
478
|
} finally {
|
|
466
479
|
if (canCork) {
|
|
@@ -472,21 +485,25 @@ function createRenderer({
|
|
|
472
485
|
}
|
|
473
486
|
let forceWait = false;
|
|
474
487
|
try {
|
|
475
|
-
|
|
476
|
-
forceWait = ret === false;
|
|
488
|
+
forceWait = cb.onHead(head) === false;
|
|
477
489
|
} catch (cbErr) {
|
|
478
490
|
warn("onHead callback threw:", cbErr);
|
|
479
491
|
}
|
|
480
492
|
const startPipe = () => stream.pipe(writable);
|
|
481
|
-
if (forceWait || !wroteOk)
|
|
482
|
-
|
|
493
|
+
if (forceWait || !wroteOk) {
|
|
494
|
+
if (typeof writable.once === "function") {
|
|
495
|
+
writable.once("drain", startPipe);
|
|
496
|
+
} else {
|
|
497
|
+
startPipe();
|
|
498
|
+
}
|
|
499
|
+
} else startPipe();
|
|
483
500
|
try {
|
|
484
|
-
onShellReady
|
|
501
|
+
cb.onShellReady();
|
|
485
502
|
} catch (cbErr) {
|
|
486
503
|
warn("onShellReady callback threw:", cbErr);
|
|
487
504
|
}
|
|
488
505
|
} catch (err) {
|
|
489
|
-
onError
|
|
506
|
+
cb.onError(err);
|
|
490
507
|
controller.fatalAbort(err);
|
|
491
508
|
}
|
|
492
509
|
},
|
|
@@ -496,18 +513,18 @@ function createRenderer({
|
|
|
496
513
|
const deliver = () => {
|
|
497
514
|
try {
|
|
498
515
|
const data = store.getSnapshot();
|
|
499
|
-
onAllReady
|
|
500
|
-
onFinish
|
|
516
|
+
cb.onAllReady(data);
|
|
517
|
+
cb.onFinish(data);
|
|
501
518
|
} catch (thrown) {
|
|
502
519
|
if (thrown && typeof thrown.then === "function") {
|
|
503
520
|
thrown.then(deliver).catch((e) => {
|
|
504
521
|
error("Data promise rejected:", e);
|
|
505
|
-
onError
|
|
522
|
+
cb.onError(e);
|
|
506
523
|
controller.fatalAbort(e);
|
|
507
524
|
});
|
|
508
525
|
} else {
|
|
509
526
|
error("Unexpected throw from getSnapshot:", thrown);
|
|
510
|
-
onError
|
|
527
|
+
cb.onError(thrown);
|
|
511
528
|
controller.fatalAbort(thrown);
|
|
512
529
|
}
|
|
513
530
|
}
|
|
@@ -520,24 +537,24 @@ function createRenderer({
|
|
|
520
537
|
stopShellTimer();
|
|
521
538
|
} catch {
|
|
522
539
|
}
|
|
523
|
-
onError
|
|
540
|
+
cb.onError(err);
|
|
524
541
|
controller.fatalAbort(err);
|
|
525
542
|
},
|
|
526
543
|
onError(err) {
|
|
527
544
|
if (controller.isAborted) return;
|
|
528
545
|
const msg = String(err?.message ?? "");
|
|
529
|
-
warn
|
|
546
|
+
warn("React stream error:", msg);
|
|
530
547
|
if (isBenignStreamErr(err)) {
|
|
531
548
|
controller.benignAbort("Client disconnected before stream finished");
|
|
532
549
|
return;
|
|
533
550
|
}
|
|
534
|
-
onError
|
|
551
|
+
cb.onError(err);
|
|
535
552
|
controller.fatalAbort(err);
|
|
536
553
|
}
|
|
537
554
|
});
|
|
538
555
|
controller.setStreamAbort(() => stream.abort());
|
|
539
556
|
} catch (err) {
|
|
540
|
-
onError
|
|
557
|
+
cb.onError(err);
|
|
541
558
|
controller.fatalAbort(err);
|
|
542
559
|
}
|
|
543
560
|
return {
|
|
@@ -548,10 +565,73 @@ function createRenderer({
|
|
|
548
565
|
};
|
|
549
566
|
return { renderSSR, renderStream };
|
|
550
567
|
}
|
|
568
|
+
|
|
569
|
+
// src/RouteData.ts
|
|
570
|
+
var RouteDataError = class _RouteDataError extends Error {
|
|
571
|
+
status;
|
|
572
|
+
statusText;
|
|
573
|
+
code;
|
|
574
|
+
body;
|
|
575
|
+
constructor(message, opts) {
|
|
576
|
+
super(message);
|
|
577
|
+
this.name = "RouteDataError";
|
|
578
|
+
this.status = opts.status;
|
|
579
|
+
this.statusText = opts.statusText;
|
|
580
|
+
this.code = opts.code;
|
|
581
|
+
this.body = opts.body;
|
|
582
|
+
Object.setPrototypeOf(this, _RouteDataError.prototype);
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
var INITIAL_DATA_KEY = "__INITIAL_DATA__";
|
|
586
|
+
function readInitialDataOnce() {
|
|
587
|
+
if (typeof window === "undefined") return null;
|
|
588
|
+
const w = window;
|
|
589
|
+
const data = w[INITIAL_DATA_KEY];
|
|
590
|
+
if (!data) return null;
|
|
591
|
+
delete w[INITIAL_DATA_KEY];
|
|
592
|
+
return data;
|
|
593
|
+
}
|
|
594
|
+
async function fetchRouteData(pathname, init) {
|
|
595
|
+
if (!pathname) {
|
|
596
|
+
throw new Error("fetchRouteData: pathname is required");
|
|
597
|
+
}
|
|
598
|
+
const url = `/__taujs/data?url=${encodeURIComponent(pathname)}`;
|
|
599
|
+
const res = await fetch(url, {
|
|
600
|
+
credentials: "include",
|
|
601
|
+
...init
|
|
602
|
+
});
|
|
603
|
+
if (!res.ok) {
|
|
604
|
+
let body2;
|
|
605
|
+
try {
|
|
606
|
+
body2 = await res.json();
|
|
607
|
+
} catch {
|
|
608
|
+
const text = await res.text().catch(() => "");
|
|
609
|
+
body2 = { error: text };
|
|
610
|
+
}
|
|
611
|
+
const json = body2;
|
|
612
|
+
throw new RouteDataError(json.error ?? `Request failed: ${res.status}`, {
|
|
613
|
+
status: res.status,
|
|
614
|
+
statusText: json.statusText ?? res.statusText,
|
|
615
|
+
code: json.code,
|
|
616
|
+
body: body2
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
const body = await res.json();
|
|
620
|
+
return body.data ?? {};
|
|
621
|
+
}
|
|
622
|
+
function getCurrentPath() {
|
|
623
|
+
if (typeof window === "undefined") return null;
|
|
624
|
+
const { pathname, search } = window.location;
|
|
625
|
+
return `${pathname}${search}`;
|
|
626
|
+
}
|
|
551
627
|
export {
|
|
628
|
+
RouteDataError,
|
|
552
629
|
SSRStoreProvider,
|
|
553
630
|
createRenderer,
|
|
554
631
|
createSSRStore,
|
|
632
|
+
fetchRouteData,
|
|
633
|
+
getCurrentPath,
|
|
555
634
|
hydrateApp,
|
|
635
|
+
readInitialDataOnce,
|
|
556
636
|
useSSRStore
|
|
557
637
|
};
|