@taujs/react 0.0.7 → 0.1.0
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 +3 -91
- package/dist/index.d.ts +70 -31
- package/dist/index.js +472 -109
- package/package.json +6 -8
package/README.md
CHANGED
|
@@ -6,96 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
`pnpm add @taujs/react`
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
# τjs
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
React Renderer: CSR, SSR, Streaming SSR
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
- Server-side rendering (SSR)
|
|
15
|
-
- Streaming SSR
|
|
16
|
-
|
|
17
|
-
Supported application structure and composition:
|
|
18
|
-
|
|
19
|
-
- Single-page Application (SPA)
|
|
20
|
-
- Multi-page Application (MPA)
|
|
21
|
-
- Build-time Micro-Frontends (MFE), with server orchestration and delivery
|
|
22
|
-
|
|
23
|
-
Assemble independent frontends at build time incorporating flexible per-route SPA-MPA hybrid with CSR, SSR, and Streaming SSR, rendering options.
|
|
24
|
-
|
|
25
|
-
Fastify Plugin for integration with taujs [ τjs ] template https://github.com/aoede3/taujs
|
|
26
|
-
|
|
27
|
-
- Production: Fastify, React
|
|
28
|
-
- Development: Fastify, React, tsx, Vite
|
|
29
|
-
|
|
30
|
-
- TypeScript-first
|
|
31
|
-
- ESM-only focus
|
|
32
|
-
|
|
33
|
-
## τjs - DX Developer Experience
|
|
34
|
-
|
|
35
|
-
Integrated Vite HMR run alongside tsx (TS eXecute) providing fast responsive dev reload times for universal backend / frontend changes
|
|
36
|
-
|
|
37
|
-
- Fastify https://fastify.dev/
|
|
38
|
-
- React https://reactjs.org/
|
|
39
|
-
- tsx https://tsx.is/
|
|
40
|
-
- Vite https://vitejs.dev/guide/ssr#building-for-production
|
|
41
|
-
|
|
42
|
-
- ESBuild https://esbuild.github.io/
|
|
43
|
-
- Rollup https://rollupjs.org/
|
|
44
|
-
- ESM https://nodejs.org/api/esm.html
|
|
45
|
-
|
|
46
|
-
## Development / CI
|
|
47
|
-
|
|
48
|
-
`npm install --legacy-peer-deps`
|
|
49
|
-
|
|
50
|
-
## Usage
|
|
51
|
-
|
|
52
|
-
### Fastify
|
|
53
|
-
|
|
54
|
-
https://github.com/aoede3/taujs/blob/main/src/server/index.ts
|
|
55
|
-
|
|
56
|
-
Not utilising taujs [ τjs ] template? Add in your own ts `alias` object for your own particular directory setup e.g. `alias: { object }`
|
|
57
|
-
|
|
58
|
-
### React 'entry-client.tsx'
|
|
59
|
-
|
|
60
|
-
https://github.com/aoede3/taujs/blob/main/src/client/entry-client.tsx
|
|
61
|
-
|
|
62
|
-
### React 'entry-server.tsx'
|
|
63
|
-
|
|
64
|
-
Extended pipe object with callbacks to @taujs/server enabling additional manipulation of HEAD content from client code
|
|
65
|
-
|
|
66
|
-
https://github.com/aoede3/taujs/blob/main/src/client/entry-server.tsx
|
|
67
|
-
|
|
68
|
-
### index.html
|
|
69
|
-
|
|
70
|
-
https://github.com/aoede3/taujs/blob/main/src/client/index.html
|
|
71
|
-
|
|
72
|
-
### client.d.ts
|
|
73
|
-
|
|
74
|
-
https://github.com/aoede3/taujs/blob/main/src/client/client.d.ts
|
|
75
|
-
|
|
76
|
-
### Routes
|
|
77
|
-
|
|
78
|
-
Integral to τjs is its internal routing:
|
|
79
|
-
|
|
80
|
-
1. Fastify serving index.html to client browser for client routing
|
|
81
|
-
2. Internal service calls to API to provide data for streaming/hydration
|
|
82
|
-
3. Fastify serving API calls via HTTP in the more traditional sense of client/server
|
|
83
|
-
|
|
84
|
-
In ensuring a particular 'route' receives data for hydration there are two options:
|
|
85
|
-
|
|
86
|
-
1. An HTTP call syntactically not unlike 'fetch' providing params to a 'fetch' call
|
|
87
|
-
2. Internal service call returning data as per your architecture
|
|
88
|
-
|
|
89
|
-
In supporting Option 2. there is a registry of services. More detail in 'Service Registry'.
|
|
90
|
-
|
|
91
|
-
Each routes 'path' is a simple URL regex as per below examples.
|
|
92
|
-
|
|
93
|
-
https://github.com/aoede3/taujs/blob/main/src/shared/routes/Routes.ts
|
|
94
|
-
|
|
95
|
-
### Service Registry
|
|
96
|
-
|
|
97
|
-
In supporting internal calls via τjs a registry of available services and methods can provide linkage to your own architectural setup and developmental patterns
|
|
98
|
-
|
|
99
|
-
https://github.com/aoede3/taujs/blob/main/src/server/services/ServiceRegistry.ts
|
|
100
|
-
|
|
101
|
-
https://github.com/aoede3/taujs/blob/main/src/server/services/ServiceExample.ts
|
|
13
|
+
https://taujs.dev/
|
package/dist/index.d.ts
CHANGED
|
@@ -1,49 +1,88 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
1
2
|
import React from 'react';
|
|
2
|
-
import {
|
|
3
|
+
import { Writable } from 'node:stream';
|
|
3
4
|
|
|
4
5
|
type SSRStore<T> = {
|
|
5
6
|
getSnapshot: () => T;
|
|
6
7
|
getServerSnapshot: () => T;
|
|
7
8
|
setData: (newData: T) => void;
|
|
8
9
|
subscribe: (callback: () => void) => () => void;
|
|
10
|
+
status: 'pending' | 'success' | 'error';
|
|
11
|
+
lastError?: Error;
|
|
9
12
|
};
|
|
10
|
-
declare
|
|
11
|
-
declare const SSRStoreProvider:
|
|
12
|
-
store: SSRStore<
|
|
13
|
-
}
|
|
13
|
+
declare function createSSRStore<T>(initialDataOrPromise: T | Promise<T> | (() => Promise<T>)): SSRStore<T>;
|
|
14
|
+
declare const SSRStoreProvider: <T>({ store, children }: React.PropsWithChildren<{
|
|
15
|
+
store: SSRStore<T>;
|
|
16
|
+
}>) => react_jsx_runtime.JSX.Element;
|
|
14
17
|
declare const useSSRStore: <T>() => T;
|
|
15
18
|
|
|
16
|
-
type
|
|
19
|
+
type UILogger = {
|
|
20
|
+
log: (...args: unknown[]) => void;
|
|
21
|
+
warn: (...args: unknown[]) => void;
|
|
22
|
+
error: (...args: unknown[]) => void;
|
|
23
|
+
};
|
|
24
|
+
type ServerLogs = {
|
|
25
|
+
info: (message: string, meta?: unknown) => void;
|
|
26
|
+
warn: (message: string, meta?: unknown) => void;
|
|
27
|
+
error: (message: string, meta?: unknown) => void;
|
|
28
|
+
debug?: (category: string, message: string, meta?: unknown) => void;
|
|
29
|
+
child?: (ctx: Record<string, unknown>) => ServerLogs;
|
|
30
|
+
isDebugEnabled?: (category: string) => boolean;
|
|
31
|
+
};
|
|
32
|
+
type LoggerLike = Partial<UILogger> | Partial<ServerLogs>;
|
|
33
|
+
|
|
34
|
+
type HydrateAppOptions<T> = {
|
|
17
35
|
appComponent: React.ReactElement;
|
|
18
|
-
initialDataKey?: keyof Window;
|
|
19
36
|
rootElementId?: string;
|
|
20
|
-
|
|
37
|
+
enableDebug?: boolean;
|
|
38
|
+
logger?: LoggerLike;
|
|
39
|
+
dataKey?: string;
|
|
40
|
+
onHydrationError?: (err: unknown) => void;
|
|
41
|
+
onStart?: () => void;
|
|
42
|
+
onSuccess?: () => void;
|
|
21
43
|
};
|
|
22
|
-
declare
|
|
44
|
+
declare function hydrateApp<T>({ appComponent, rootElementId, enableDebug, logger, dataKey, onHydrationError, onStart, onSuccess, }: HydrateAppOptions<T>): void;
|
|
23
45
|
|
|
24
|
-
type
|
|
46
|
+
type RenderCallbacks<T> = {
|
|
47
|
+
onHead?: (head: string) => boolean | void;
|
|
48
|
+
onShellReady?: () => void;
|
|
49
|
+
onAllReady?: (data: T) => void;
|
|
50
|
+
onFinish?: (data: T) => void;
|
|
51
|
+
onError?: (err: unknown) => void;
|
|
52
|
+
};
|
|
53
|
+
type StreamOptions = {
|
|
54
|
+
/** Timeout in ms for shell to be ready (default: 10000) */
|
|
55
|
+
shellTimeoutMs?: number;
|
|
56
|
+
/** Whether to use cork/uncork for batched writes (default: true) */
|
|
57
|
+
useCork?: boolean;
|
|
58
|
+
};
|
|
59
|
+
type SSRResult = {
|
|
60
|
+
headContent: string;
|
|
61
|
+
appHtml: string;
|
|
62
|
+
aborted: boolean;
|
|
63
|
+
};
|
|
64
|
+
type StreamCallOptions = StreamOptions & {
|
|
65
|
+
logger?: LoggerLike;
|
|
66
|
+
};
|
|
67
|
+
declare function createRenderer<T extends Record<string, unknown>>({ appComponent, headContent, streamOptions, logger, enableDebug, }: {
|
|
25
68
|
appComponent: (props: {
|
|
26
69
|
location: string;
|
|
27
70
|
}) => React.ReactElement;
|
|
28
|
-
headContent:
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
initialDataPromise: Record<string, unknown>;
|
|
45
|
-
location: string;
|
|
46
|
-
bootstrapModules?: string;
|
|
47
|
-
}) => void;
|
|
71
|
+
headContent: (ctx: {
|
|
72
|
+
data: T;
|
|
73
|
+
meta: Record<string, unknown>;
|
|
74
|
+
}) => string;
|
|
75
|
+
enableDebug?: boolean;
|
|
76
|
+
logger?: LoggerLike;
|
|
77
|
+
streamOptions?: StreamOptions;
|
|
78
|
+
}): {
|
|
79
|
+
renderSSR: (initialData: T, location: string, meta?: Record<string, unknown>, signal?: AbortSignal, opts?: {
|
|
80
|
+
logger?: LoggerLike;
|
|
81
|
+
}) => Promise<SSRResult>;
|
|
82
|
+
renderStream: (writable: Writable, callbacks: RenderCallbacks<T> | undefined, initialData: T | Promise<T> | (() => Promise<T>), location: string, bootstrapModules?: string, meta?: Record<string, unknown>, cspNonce?: string, signal?: AbortSignal, opts?: StreamCallOptions) => {
|
|
83
|
+
abort: () => void;
|
|
84
|
+
done: Promise<void>;
|
|
85
|
+
};
|
|
86
|
+
};
|
|
48
87
|
|
|
49
|
-
export { type SSRStore, SSRStoreProvider,
|
|
88
|
+
export { type HydrateAppOptions, type RenderCallbacks, type SSRStore, SSRStoreProvider, type ServerLogs, type StreamOptions, createRenderer, createSSRStore, hydrateApp, useSSRStore };
|
package/dist/index.js
CHANGED
|
@@ -1,31 +1,33 @@
|
|
|
1
1
|
// src/SSRDataStore.tsx
|
|
2
|
-
import { createContext, useContext, useSyncExternalStore } from "react";
|
|
2
|
+
import { createContext, useContext, useSyncExternalStore, useDeferredValue, useMemo } from "react";
|
|
3
3
|
import { jsx } from "react/jsx-runtime";
|
|
4
|
-
|
|
4
|
+
function createSSRStore(initialDataOrPromise) {
|
|
5
5
|
let currentData;
|
|
6
6
|
let status;
|
|
7
7
|
let lastError;
|
|
8
|
-
const subscribers = /* @__PURE__ */ new Set();
|
|
9
8
|
let serverDataPromise;
|
|
9
|
+
const subscribers = /* @__PURE__ */ new Set();
|
|
10
|
+
const notify = () => subscribers.forEach((cb) => cb());
|
|
10
11
|
const handleError = (error) => {
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
const normalised = error instanceof Error ? error : new Error(String(JSON.stringify(error)));
|
|
13
|
+
console.error("Failed to load initial data:", normalised);
|
|
14
|
+
lastError = normalised;
|
|
13
15
|
status = "error";
|
|
16
|
+
notify();
|
|
14
17
|
};
|
|
15
18
|
if (typeof initialDataOrPromise === "function") {
|
|
16
19
|
status = "pending";
|
|
17
|
-
|
|
18
|
-
serverDataPromise = promiseFromFunction.then((data) => {
|
|
20
|
+
serverDataPromise = initialDataOrPromise().then((data) => {
|
|
19
21
|
currentData = data;
|
|
20
22
|
status = "success";
|
|
21
|
-
|
|
23
|
+
notify();
|
|
22
24
|
}).catch(handleError);
|
|
23
25
|
} else if (initialDataOrPromise instanceof Promise) {
|
|
24
26
|
status = "pending";
|
|
25
27
|
serverDataPromise = initialDataOrPromise.then((data) => {
|
|
26
28
|
currentData = data;
|
|
27
29
|
status = "success";
|
|
28
|
-
|
|
30
|
+
notify();
|
|
29
31
|
}).catch(handleError);
|
|
30
32
|
} else {
|
|
31
33
|
currentData = initialDataOrPromise;
|
|
@@ -35,38 +37,41 @@ var createSSRStore = (initialDataOrPromise) => {
|
|
|
35
37
|
const setData = (newData) => {
|
|
36
38
|
currentData = newData;
|
|
37
39
|
status = "success";
|
|
38
|
-
|
|
40
|
+
notify();
|
|
39
41
|
};
|
|
40
42
|
const subscribe = (callback) => {
|
|
41
43
|
subscribers.add(callback);
|
|
42
44
|
return () => subscribers.delete(callback);
|
|
43
45
|
};
|
|
44
46
|
const getSnapshot = () => {
|
|
45
|
-
if (status === "pending")
|
|
46
|
-
|
|
47
|
-
} else if (status === "error") {
|
|
48
|
-
throw new Error(`SSR data fetch failed: ${lastError?.message || "Unknown error"}`);
|
|
49
|
-
}
|
|
47
|
+
if (status === "pending") throw serverDataPromise;
|
|
48
|
+
if (status === "error") throw new Error(`SSR data fetch failed: ${lastError?.message || "Unknown error"}`);
|
|
50
49
|
if (currentData === void 0) throw new Error("SSR data is undefined - store initialisation problem");
|
|
51
50
|
return currentData;
|
|
52
51
|
};
|
|
53
52
|
const getServerSnapshot = () => {
|
|
54
|
-
if (status === "pending")
|
|
55
|
-
|
|
56
|
-
} else if (status === "error") {
|
|
57
|
-
throw new Error(`Server-side data fetch failed: ${lastError?.message || "Unknown error"}`);
|
|
58
|
-
}
|
|
53
|
+
if (status === "pending") throw serverDataPromise;
|
|
54
|
+
if (status === "error") throw new Error(`Server-side data fetch failed: ${lastError?.message || "Unknown error"}`);
|
|
59
55
|
if (currentData === void 0) throw new Error("Server data not available - check SSR configuration");
|
|
60
56
|
return currentData;
|
|
61
57
|
};
|
|
62
|
-
return {
|
|
63
|
-
|
|
58
|
+
return {
|
|
59
|
+
getSnapshot,
|
|
60
|
+
getServerSnapshot,
|
|
61
|
+
setData,
|
|
62
|
+
subscribe,
|
|
63
|
+
status,
|
|
64
|
+
lastError
|
|
65
|
+
};
|
|
66
|
+
}
|
|
64
67
|
var SSRStoreContext = createContext(null);
|
|
65
68
|
var SSRStoreProvider = ({ store, children }) => /* @__PURE__ */ jsx(SSRStoreContext.Provider, { value: store, children });
|
|
66
69
|
var useSSRStore = () => {
|
|
67
70
|
const store = useContext(SSRStoreContext);
|
|
68
71
|
if (!store) throw new Error("useSSRStore must be used within a SSRStoreProvider");
|
|
69
|
-
|
|
72
|
+
const syncVal = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getServerSnapshot);
|
|
73
|
+
const deferred = useDeferredValue(syncVal);
|
|
74
|
+
return useMemo(() => deferred, [deferred]);
|
|
70
75
|
};
|
|
71
76
|
|
|
72
77
|
// src/SSRHydration.tsx
|
|
@@ -74,121 +79,479 @@ import React2 from "react";
|
|
|
74
79
|
import { createRoot, hydrateRoot } from "react-dom/client";
|
|
75
80
|
|
|
76
81
|
// src/utils/Logger.ts
|
|
77
|
-
var
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
var toJSONString = (v) => typeof v === "string" ? v : v instanceof Error ? v.stack ?? v.message : JSON.stringify(v);
|
|
83
|
+
var splitMsgAndMeta = (args) => {
|
|
84
|
+
const [first, ...rest] = args;
|
|
85
|
+
const msg = toJSONString(first);
|
|
86
|
+
if (rest.length === 0) return { msg, meta: void 0 };
|
|
87
|
+
const only = rest.length === 1 ? rest[0] : void 0;
|
|
88
|
+
const meta = only && typeof only === "object" && !(only instanceof Error) ? only : { args: rest.map(toJSONString) };
|
|
89
|
+
return { msg, meta };
|
|
90
|
+
};
|
|
91
|
+
function createUILogger(logger, opts = {}) {
|
|
92
|
+
const { debugCategory = "ssr", context, preferDebug = false, enableDebug = false } = opts;
|
|
93
|
+
if (!enableDebug) return { log: () => {
|
|
94
|
+
}, warn: () => {
|
|
95
|
+
}, error: () => {
|
|
96
|
+
} };
|
|
97
|
+
const looksServer = !!logger && ("info" in logger || "debug" in logger || "child" in logger || "isDebugEnabled" in logger);
|
|
98
|
+
if (looksServer) {
|
|
99
|
+
let s = logger;
|
|
100
|
+
if (s.child && context) {
|
|
101
|
+
try {
|
|
102
|
+
s = s.child.call(s, context);
|
|
103
|
+
} catch {
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const info = s.info ? s.info.bind(s) : (m, meta) => meta ? console.log(m, meta) : console.log(m);
|
|
107
|
+
const warn = s.warn ? s.warn.bind(s) : (m, meta) => meta ? console.warn(m, meta) : console.warn(m);
|
|
108
|
+
const error = s.error ? s.error.bind(s) : (m, meta) => meta ? console.error(m, meta) : console.error(m);
|
|
109
|
+
const debug = s.debug ? s.debug.bind(s) : void 0;
|
|
110
|
+
const isDebugEnabled = s.isDebugEnabled ? s.isDebugEnabled.bind(s) : void 0;
|
|
111
|
+
return {
|
|
112
|
+
log: (...args) => {
|
|
113
|
+
const { msg, meta } = splitMsgAndMeta(args);
|
|
114
|
+
if (debug) {
|
|
115
|
+
const enabled = (isDebugEnabled ? isDebugEnabled(debugCategory) : false) || preferDebug;
|
|
116
|
+
if (enabled) {
|
|
117
|
+
debug(debugCategory, msg, meta);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
info(msg, meta);
|
|
122
|
+
},
|
|
123
|
+
warn: (...args) => {
|
|
124
|
+
const { msg, meta } = splitMsgAndMeta(args);
|
|
125
|
+
warn(msg, meta);
|
|
126
|
+
},
|
|
127
|
+
error: (...args) => {
|
|
128
|
+
const { msg, meta } = splitMsgAndMeta(args);
|
|
129
|
+
error(msg, meta);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
86
132
|
}
|
|
87
|
-
}
|
|
133
|
+
const ui = logger || {};
|
|
134
|
+
return {
|
|
135
|
+
log: (...a) => (ui.log ?? console.log)(...a),
|
|
136
|
+
warn: (...a) => (ui.warn ?? console.warn)(...a),
|
|
137
|
+
error: (...a) => (ui.error ?? console.error)(...a)
|
|
138
|
+
};
|
|
139
|
+
}
|
|
88
140
|
|
|
89
141
|
// src/SSRHydration.tsx
|
|
90
142
|
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
91
|
-
|
|
92
|
-
|
|
143
|
+
function hydrateApp({
|
|
144
|
+
appComponent,
|
|
145
|
+
rootElementId = "root",
|
|
146
|
+
enableDebug = false,
|
|
147
|
+
logger,
|
|
148
|
+
dataKey = "__INITIAL_DATA__",
|
|
149
|
+
onHydrationError,
|
|
150
|
+
onStart,
|
|
151
|
+
onSuccess
|
|
152
|
+
}) {
|
|
153
|
+
const { log, warn, error } = createUILogger(logger, { debugCategory: "ssr", context: { scope: "react-hydration" }, enableDebug });
|
|
154
|
+
const mountCSR = (rootEl) => {
|
|
155
|
+
rootEl.innerHTML = "";
|
|
156
|
+
const root = createRoot(rootEl);
|
|
157
|
+
root.render(/* @__PURE__ */ jsx2(React2.StrictMode, { children: appComponent }));
|
|
158
|
+
};
|
|
159
|
+
const startHydration = (rootEl, initialData) => {
|
|
160
|
+
if (enableDebug) log("Hydration started");
|
|
161
|
+
onStart?.();
|
|
162
|
+
if (enableDebug) log("Initial data loaded:", initialData);
|
|
163
|
+
const store = createSSRStore(initialData);
|
|
164
|
+
if (enableDebug) log("Store created:", store);
|
|
165
|
+
try {
|
|
166
|
+
hydrateRoot(
|
|
167
|
+
rootEl,
|
|
168
|
+
/* @__PURE__ */ jsx2(React2.StrictMode, { children: /* @__PURE__ */ jsx2(SSRStoreProvider, { store, children: appComponent }) }),
|
|
169
|
+
{
|
|
170
|
+
onRecoverableError: (err, info) => {
|
|
171
|
+
warn("Recoverable hydration error:", err, info);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
);
|
|
175
|
+
if (enableDebug) log("Hydration completed");
|
|
176
|
+
onSuccess?.();
|
|
177
|
+
} catch (err) {
|
|
178
|
+
error("Hydration error:", err);
|
|
179
|
+
onHydrationError?.(err);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
93
182
|
const bootstrap = () => {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (!rootElement) {
|
|
183
|
+
const rootEl = document.getElementById(rootElementId);
|
|
184
|
+
if (!rootEl) {
|
|
97
185
|
error(`Root element with id "${rootElementId}" not found.`);
|
|
98
186
|
return;
|
|
99
187
|
}
|
|
100
|
-
const
|
|
101
|
-
if (
|
|
102
|
-
warn(`
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
} else {
|
|
106
|
-
log("Initial data loaded:", initialData);
|
|
107
|
-
const initialDataPromise = Promise.resolve(initialData);
|
|
108
|
-
const store = createSSRStore(initialDataPromise);
|
|
109
|
-
log("Store created:", store);
|
|
110
|
-
hydrateRoot(
|
|
111
|
-
rootElement,
|
|
112
|
-
/* @__PURE__ */ jsx2(React2.StrictMode, { children: /* @__PURE__ */ jsx2(SSRStoreProvider, { store, children: appComponent }) })
|
|
113
|
-
);
|
|
114
|
-
log("Hydration completed");
|
|
188
|
+
const data = window[dataKey];
|
|
189
|
+
if (data === void 0) {
|
|
190
|
+
if (enableDebug) warn(`No initial SSR data at window["${dataKey}"]. Mounting CSR.`);
|
|
191
|
+
mountCSR(rootEl);
|
|
192
|
+
return;
|
|
115
193
|
}
|
|
194
|
+
startHydration(rootEl, data);
|
|
116
195
|
};
|
|
117
196
|
if (document.readyState !== "loading") {
|
|
118
197
|
bootstrap();
|
|
119
198
|
} else {
|
|
120
|
-
document.addEventListener("DOMContentLoaded", bootstrap);
|
|
199
|
+
document.addEventListener("DOMContentLoaded", bootstrap, { once: true });
|
|
121
200
|
}
|
|
122
|
-
}
|
|
201
|
+
}
|
|
123
202
|
|
|
124
203
|
// src/SSRRender.tsx
|
|
125
|
-
import "http";
|
|
126
|
-
import { Writable } from "stream";
|
|
127
204
|
import "react";
|
|
128
205
|
import { renderToPipeableStream, renderToString } from "react-dom/server";
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
var
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
206
|
+
|
|
207
|
+
// src/utils/Streaming.ts
|
|
208
|
+
var DEFAULT_BENIGN_ERRORS = /ECONNRESET|EPIPE|socket hang up|aborted|premature/i;
|
|
209
|
+
function isBenignStreamErr(err) {
|
|
210
|
+
const msg = String(err?.message ?? "");
|
|
211
|
+
return DEFAULT_BENIGN_ERRORS.test(msg);
|
|
212
|
+
}
|
|
213
|
+
function createSettler() {
|
|
214
|
+
let settled = false;
|
|
215
|
+
let resolve;
|
|
216
|
+
let reject;
|
|
217
|
+
const done = new Promise((r, j) => {
|
|
218
|
+
resolve = () => {
|
|
219
|
+
if (!settled) {
|
|
220
|
+
settled = true;
|
|
221
|
+
r();
|
|
222
|
+
}
|
|
139
223
|
};
|
|
224
|
+
reject = (e) => {
|
|
225
|
+
if (!settled) {
|
|
226
|
+
settled = true;
|
|
227
|
+
j(e);
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
});
|
|
231
|
+
return { done, resolve, reject, isSettled: () => settled };
|
|
232
|
+
}
|
|
233
|
+
function startShellTimer(ms, onTimeout) {
|
|
234
|
+
const t = setTimeout(onTimeout, ms);
|
|
235
|
+
return () => clearTimeout(t);
|
|
236
|
+
}
|
|
237
|
+
function wireWritableGuards(writable, {
|
|
238
|
+
benignAbort,
|
|
239
|
+
fatalAbort,
|
|
240
|
+
onError,
|
|
241
|
+
onFinish,
|
|
242
|
+
benignErrorPattern = DEFAULT_BENIGN_ERRORS
|
|
243
|
+
}) {
|
|
244
|
+
const handlers = [];
|
|
245
|
+
const add = (ev, fn) => {
|
|
246
|
+
writable.once(ev, fn);
|
|
247
|
+
handlers.push(() => writable.removeListener(ev, fn));
|
|
140
248
|
};
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
249
|
+
add("error", (err) => {
|
|
250
|
+
const msg = String(err?.message ?? "");
|
|
251
|
+
if (benignErrorPattern.test(msg)) {
|
|
252
|
+
benignAbort("Client disconnected during stream");
|
|
253
|
+
} else {
|
|
254
|
+
onError?.(err);
|
|
255
|
+
fatalAbort(err);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
add("close", () => benignAbort("Writable closed early (likely client disconnect)"));
|
|
259
|
+
add("finish", () => {
|
|
260
|
+
if (onFinish) onFinish();
|
|
261
|
+
else benignAbort("Stream finished (normal completion)");
|
|
262
|
+
});
|
|
263
|
+
return {
|
|
264
|
+
cleanup: () => {
|
|
265
|
+
for (const off of handlers) {
|
|
266
|
+
try {
|
|
267
|
+
off();
|
|
268
|
+
} catch {
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
150
272
|
};
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
273
|
+
}
|
|
274
|
+
function createStreamController(writable, logger) {
|
|
275
|
+
const { log, warn, error } = logger;
|
|
276
|
+
let aborted = false;
|
|
277
|
+
const settle = createSettler();
|
|
278
|
+
let stopShellTimer;
|
|
279
|
+
let removeAbortListener;
|
|
280
|
+
let guardsCleanup;
|
|
281
|
+
let streamAbort;
|
|
282
|
+
const cleanup = (benign, err) => {
|
|
283
|
+
if (aborted) return;
|
|
284
|
+
aborted = true;
|
|
285
|
+
try {
|
|
286
|
+
stopShellTimer?.();
|
|
287
|
+
} catch {
|
|
288
|
+
}
|
|
289
|
+
try {
|
|
290
|
+
removeAbortListener?.();
|
|
291
|
+
} catch {
|
|
292
|
+
}
|
|
293
|
+
try {
|
|
294
|
+
guardsCleanup?.();
|
|
295
|
+
} catch {
|
|
296
|
+
}
|
|
297
|
+
try {
|
|
298
|
+
streamAbort?.();
|
|
299
|
+
} catch {
|
|
300
|
+
}
|
|
301
|
+
try {
|
|
302
|
+
if (!writable.writableEnded && !writable.destroyed) writable.destroy();
|
|
303
|
+
} catch {
|
|
304
|
+
}
|
|
305
|
+
if (benign) settle.resolve();
|
|
306
|
+
else if (err !== void 0) settle.reject(err);
|
|
307
|
+
else settle.resolve();
|
|
308
|
+
};
|
|
309
|
+
return {
|
|
310
|
+
setStreamAbort: (fn) => {
|
|
311
|
+
streamAbort = fn;
|
|
312
|
+
},
|
|
313
|
+
setStopShellTimer: (fn) => {
|
|
314
|
+
stopShellTimer = fn;
|
|
315
|
+
},
|
|
316
|
+
setRemoveAbortListener: (fn) => {
|
|
317
|
+
removeAbortListener = fn;
|
|
318
|
+
},
|
|
319
|
+
setGuardsCleanup: (fn) => {
|
|
320
|
+
guardsCleanup = fn;
|
|
321
|
+
},
|
|
322
|
+
complete(message) {
|
|
323
|
+
if (aborted) return;
|
|
324
|
+
if (message) (log ?? warn)(message);
|
|
325
|
+
cleanup(true);
|
|
326
|
+
},
|
|
327
|
+
benignAbort(why) {
|
|
328
|
+
if (aborted) return;
|
|
329
|
+
warn(why);
|
|
330
|
+
cleanup(true);
|
|
331
|
+
},
|
|
332
|
+
fatalAbort(err) {
|
|
333
|
+
if (aborted) return;
|
|
334
|
+
error("Stream aborted with error:", err);
|
|
335
|
+
cleanup(false, err);
|
|
336
|
+
},
|
|
337
|
+
get done() {
|
|
338
|
+
return settle.done;
|
|
339
|
+
},
|
|
340
|
+
get isAborted() {
|
|
341
|
+
return aborted;
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// src/SSRRender.tsx
|
|
347
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
348
|
+
function createRenderer({
|
|
154
349
|
appComponent,
|
|
155
350
|
headContent,
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
})
|
|
160
|
-
const
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
351
|
+
streamOptions = {},
|
|
352
|
+
logger,
|
|
353
|
+
enableDebug = false
|
|
354
|
+
}) {
|
|
355
|
+
const { shellTimeoutMs = 1e4, useCork = true } = streamOptions;
|
|
356
|
+
const renderSSR = async (initialData, location, meta = {}, signal, opts) => {
|
|
357
|
+
const { log, warn } = createUILogger(opts?.logger ?? logger, {
|
|
358
|
+
debugCategory: "ssr",
|
|
359
|
+
context: { scope: "react-ssr" },
|
|
360
|
+
enableDebug
|
|
361
|
+
});
|
|
362
|
+
if (signal?.aborted) {
|
|
363
|
+
warn("SSR skipped; already aborted", { location });
|
|
364
|
+
return { headContent: "", appHtml: "", aborted: true };
|
|
365
|
+
}
|
|
366
|
+
let aborted = false;
|
|
367
|
+
const onAbort = () => aborted = true;
|
|
368
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
369
|
+
try {
|
|
370
|
+
log("Starting SSR:", location);
|
|
371
|
+
const dynamicHead = headContent({ data: initialData, meta });
|
|
372
|
+
const store = createSSRStore(initialData);
|
|
373
|
+
const html = renderToString(/* @__PURE__ */ jsx3(SSRStoreProvider, { store, children: appComponent({ location }) }));
|
|
374
|
+
if (aborted) {
|
|
375
|
+
warn("SSR completed after client abort", { location });
|
|
376
|
+
return { headContent: "", appHtml: "", aborted: true };
|
|
377
|
+
}
|
|
378
|
+
log("Completed SSR:", location);
|
|
379
|
+
return { headContent: dynamicHead, appHtml: html, aborted: false };
|
|
380
|
+
} finally {
|
|
381
|
+
try {
|
|
382
|
+
signal?.removeEventListener("abort", onAbort);
|
|
383
|
+
} catch {
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
const renderStream = (writable, callbacks = {}, initialData, location, bootstrapModules, meta = {}, cspNonce, signal, opts) => {
|
|
388
|
+
const { onAllReady, onError, onHead, onShellReady, onFinish } = callbacks;
|
|
389
|
+
const { log, warn, error } = createUILogger(opts?.logger ?? logger, {
|
|
390
|
+
debugCategory: "ssr",
|
|
391
|
+
context: { scope: "react-streaming" },
|
|
392
|
+
enableDebug
|
|
393
|
+
});
|
|
394
|
+
const effectiveShellTimeout = opts?.shellTimeoutMs ?? shellTimeoutMs;
|
|
395
|
+
const effectiveUseCork = opts?.useCork ?? useCork;
|
|
396
|
+
const controller = createStreamController(writable, { log, warn, error });
|
|
397
|
+
if (signal) {
|
|
398
|
+
const handleAbortSignal = () => controller.benignAbort(`AbortSignal triggered; aborting stream for location: ${location}`);
|
|
399
|
+
if (signal.aborted) {
|
|
400
|
+
handleAbortSignal();
|
|
401
|
+
return { abort: () => {
|
|
402
|
+
}, done: Promise.resolve() };
|
|
403
|
+
}
|
|
404
|
+
signal.addEventListener("abort", handleAbortSignal, { once: true });
|
|
405
|
+
controller.setRemoveAbortListener(() => {
|
|
406
|
+
try {
|
|
407
|
+
signal.removeEventListener("abort", handleAbortSignal);
|
|
408
|
+
} catch {
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
const { cleanup: guardsCleanup } = wireWritableGuards(writable, {
|
|
413
|
+
benignAbort: (why) => controller.benignAbort(why),
|
|
414
|
+
fatalAbort: (err) => {
|
|
415
|
+
onError?.(err);
|
|
416
|
+
controller.fatalAbort(err);
|
|
417
|
+
},
|
|
418
|
+
onError,
|
|
419
|
+
onFinish: () => controller.complete("Stream finished (normal completion)")
|
|
420
|
+
});
|
|
421
|
+
controller.setGuardsCleanup(guardsCleanup);
|
|
422
|
+
const stopShellTimer = startShellTimer(effectiveShellTimeout, () => {
|
|
423
|
+
if (controller.isAborted) return;
|
|
424
|
+
const timeoutErr = new Error(`Shell not ready after ${effectiveShellTimeout}ms`);
|
|
425
|
+
onError?.(timeoutErr);
|
|
426
|
+
controller.fatalAbort(timeoutErr);
|
|
427
|
+
});
|
|
428
|
+
controller.setStopShellTimer(stopShellTimer);
|
|
429
|
+
log("Starting stream:", location);
|
|
430
|
+
try {
|
|
431
|
+
const store = createSSRStore(initialData);
|
|
432
|
+
const appElement = /* @__PURE__ */ jsx3(SSRStoreProvider, { store, children: appComponent({ location }) });
|
|
433
|
+
const stream = renderToPipeableStream(appElement, {
|
|
434
|
+
nonce: cspNonce,
|
|
435
|
+
bootstrapModules: bootstrapModules ? [bootstrapModules] : void 0,
|
|
436
|
+
onShellReady() {
|
|
437
|
+
if (controller.isAborted) return;
|
|
438
|
+
try {
|
|
439
|
+
stopShellTimer();
|
|
440
|
+
} catch {
|
|
175
441
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
442
|
+
log("Shell ready:", location);
|
|
443
|
+
try {
|
|
444
|
+
let headData;
|
|
445
|
+
try {
|
|
446
|
+
headData = store.getSnapshot();
|
|
447
|
+
} catch (thrown) {
|
|
448
|
+
if (thrown && typeof thrown.then === "function") {
|
|
449
|
+
thrown.catch(() => {
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
const head = headContent({ data: headData ?? {}, meta });
|
|
454
|
+
const canCork = effectiveUseCork && typeof writable?.cork === "function" && typeof writable?.uncork === "function";
|
|
455
|
+
if (canCork) {
|
|
456
|
+
try {
|
|
457
|
+
writable.cork();
|
|
458
|
+
} catch {
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
let wroteOk = true;
|
|
462
|
+
try {
|
|
463
|
+
const res = writable?.write ? writable.write(head) : true;
|
|
464
|
+
wroteOk = res !== false;
|
|
465
|
+
} finally {
|
|
466
|
+
if (canCork) {
|
|
467
|
+
try {
|
|
468
|
+
writable.uncork();
|
|
469
|
+
} catch {
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
let forceWait = false;
|
|
474
|
+
try {
|
|
475
|
+
const ret = onHead?.(head);
|
|
476
|
+
forceWait = ret === false;
|
|
477
|
+
} catch (cbErr) {
|
|
478
|
+
warn("onHead callback threw:", cbErr);
|
|
479
|
+
}
|
|
480
|
+
const startPipe = () => stream.pipe(writable);
|
|
481
|
+
if (forceWait || !wroteOk) writable?.once?.("drain", startPipe);
|
|
482
|
+
else startPipe();
|
|
483
|
+
try {
|
|
484
|
+
onShellReady?.();
|
|
485
|
+
} catch (cbErr) {
|
|
486
|
+
warn("onShellReady callback threw:", cbErr);
|
|
487
|
+
}
|
|
488
|
+
} catch (err) {
|
|
489
|
+
onError?.(err);
|
|
490
|
+
controller.fatalAbort(err);
|
|
491
|
+
}
|
|
492
|
+
},
|
|
493
|
+
onAllReady() {
|
|
494
|
+
if (controller.isAborted) return;
|
|
495
|
+
log("All content ready:", location);
|
|
496
|
+
const deliver = () => {
|
|
497
|
+
try {
|
|
498
|
+
const data = store.getSnapshot();
|
|
499
|
+
onAllReady?.(data);
|
|
500
|
+
onFinish?.(data);
|
|
501
|
+
} catch (thrown) {
|
|
502
|
+
if (thrown && typeof thrown.then === "function") {
|
|
503
|
+
thrown.then(deliver).catch((e) => {
|
|
504
|
+
error("Data promise rejected:", e);
|
|
505
|
+
onError?.(e);
|
|
506
|
+
controller.fatalAbort(e);
|
|
507
|
+
});
|
|
508
|
+
} else {
|
|
509
|
+
error("Unexpected throw from getSnapshot:", thrown);
|
|
510
|
+
onError?.(thrown);
|
|
511
|
+
controller.fatalAbort(thrown);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
deliver();
|
|
516
|
+
},
|
|
517
|
+
onShellError(err) {
|
|
518
|
+
if (controller.isAborted) return;
|
|
519
|
+
try {
|
|
520
|
+
stopShellTimer();
|
|
521
|
+
} catch {
|
|
522
|
+
}
|
|
523
|
+
onError?.(err);
|
|
524
|
+
controller.fatalAbort(err);
|
|
525
|
+
},
|
|
526
|
+
onError(err) {
|
|
527
|
+
if (controller.isAborted) return;
|
|
528
|
+
const msg = String(err?.message ?? "");
|
|
529
|
+
warn?.("React stream error:", msg);
|
|
530
|
+
if (isBenignStreamErr(err)) {
|
|
531
|
+
controller.benignAbort("Client disconnected before stream finished");
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
onError?.(err);
|
|
535
|
+
controller.fatalAbort(err);
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
controller.setStreamAbort(() => stream.abort());
|
|
539
|
+
} catch (err) {
|
|
540
|
+
onError?.(err);
|
|
541
|
+
controller.fatalAbort(err);
|
|
183
542
|
}
|
|
184
|
-
|
|
185
|
-
}
|
|
543
|
+
return {
|
|
544
|
+
abort: () => controller.benignAbort(`Manual abort for location: ${location}`),
|
|
545
|
+
done: controller.done
|
|
546
|
+
// resolves on success/benign cancel; rejects on fatal error
|
|
547
|
+
};
|
|
548
|
+
};
|
|
549
|
+
return { renderSSR, renderStream };
|
|
550
|
+
}
|
|
186
551
|
export {
|
|
187
552
|
SSRStoreProvider,
|
|
188
|
-
createRenderStream,
|
|
189
553
|
createRenderer,
|
|
190
554
|
createSSRStore,
|
|
191
555
|
hydrateApp,
|
|
192
|
-
resolveHeadContent,
|
|
193
556
|
useSSRStore
|
|
194
557
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taujs/react",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "taujs | τjs",
|
|
5
5
|
"author": "Aoede <taujs@aoede.uk.net> (https://www.aoede.uk.net)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,7 +38,6 @@
|
|
|
38
38
|
"files": [
|
|
39
39
|
"dist"
|
|
40
40
|
],
|
|
41
|
-
"dependencies": {},
|
|
42
41
|
"devDependencies": {
|
|
43
42
|
"@arethetypeswrong/cli": "^0.15.4",
|
|
44
43
|
"@babel/preset-typescript": "^7.24.7",
|
|
@@ -48,24 +47,23 @@
|
|
|
48
47
|
"@types/node": "^24.0.7",
|
|
49
48
|
"@types/react": "^19.0.2",
|
|
50
49
|
"@types/react-dom": "^19.0.2",
|
|
51
|
-
"@vitest/coverage-v8": "^2.
|
|
52
|
-
"@vitest/ui": "^2.
|
|
50
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
51
|
+
"@vitest/ui": "^3.2.4",
|
|
53
52
|
"jsdom": "^25.0.0",
|
|
54
53
|
"prettier": "^3.3.3",
|
|
55
54
|
"react": "^19.0.0",
|
|
56
55
|
"react-dom": "^19.0.0",
|
|
57
56
|
"tsup": "^8.2.4",
|
|
58
57
|
"typescript": "^5.5.4",
|
|
59
|
-
"vite": "^
|
|
60
|
-
"vitest": "^2.
|
|
58
|
+
"vite": "^7.1.9",
|
|
59
|
+
"vitest": "^3.2.4"
|
|
61
60
|
},
|
|
62
61
|
"peerDependencies": {
|
|
63
|
-
"@taujs/server": "^0.3.0",
|
|
64
62
|
"@vitejs/plugin-react": "^4.6.0",
|
|
65
63
|
"react": "^19.0.0",
|
|
66
64
|
"react-dom": "^19.0.0",
|
|
67
65
|
"typescript": "^5.5.4",
|
|
68
|
-
"vite": "^
|
|
66
|
+
"vite": "^7.1.9"
|
|
69
67
|
},
|
|
70
68
|
"peerDependenciesMeta": {
|
|
71
69
|
"react": {
|