@taujs/server 0.0.7 → 0.0.9
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 +10 -166
- package/dist/data.d.ts +49 -0
- package/dist/data.js +179 -0
- package/dist/{SSRServer.d.ts → index.d.ts} +17 -3
- package/dist/{SSRServer.js → index.js} +64 -39
- package/package.json +22 -35
- package/dist/SSRDataStore.d.ts +0 -15
- package/dist/SSRDataStore.js +0 -58
- package/dist/SSRRender.d.ts +0 -18
- package/dist/SSRRender.js +0 -31
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
`pnpm add @taujs/server`
|
|
8
8
|
|
|
9
|
-
## Streaming
|
|
9
|
+
## SPA; SSR; Streaming SSR; Hydration; Fastify + React 18
|
|
10
10
|
|
|
11
11
|
Fastify Plugin for integration with taujs [ τjs ] template https://github.com/aoede3/taujs
|
|
12
12
|
|
|
@@ -38,122 +38,27 @@ Integrated ViteDevServer HMR + Vite Runtime API run alongside tsx (TS eXecute) p
|
|
|
38
38
|
|
|
39
39
|
### Fastify
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
import { SSRServer } from '@taujs/server;
|
|
43
|
-
|
|
44
|
-
void (await fastify.register(SSRServer, {
|
|
45
|
-
clientEntryClient: 'entry-client',
|
|
46
|
-
clientEntryServer: 'entry-server',
|
|
47
|
-
clientHtmlTemplate: 'index.html',
|
|
48
|
-
clientRoot: path.resolve(__dirname, '../client'),
|
|
49
|
-
routes,
|
|
50
|
-
serviceRegistry,
|
|
51
|
-
}));
|
|
52
|
-
```
|
|
41
|
+
https://github.com/aoede3/taujs/blob/main/src/server/index.ts
|
|
53
42
|
|
|
54
43
|
Not utilising taujs [ τjs ] template? Add in your own `alias` object for your own particular setup e.g. `alias: { object }`
|
|
55
44
|
|
|
56
45
|
### React 'entry-client.tsx'
|
|
57
46
|
|
|
58
|
-
|
|
59
|
-
import React from 'react';
|
|
60
|
-
import { hydrateRoot } from 'react-dom/client';
|
|
61
|
-
import { createSSRStore, SSRStoreProvider } from '@taujs/server/data-store';
|
|
62
|
-
|
|
63
|
-
import AppBootstrap from './AppBootstrap';
|
|
64
|
-
|
|
65
|
-
const bootstrap = () => {
|
|
66
|
-
const initialDataPromise = Promise.resolve(window.__INITIAL_DATA__);
|
|
67
|
-
const store = createSSRStore(initialDataPromise);
|
|
68
|
-
|
|
69
|
-
hydrateRoot(
|
|
70
|
-
document.getElementById('root') as HTMLElement,
|
|
71
|
-
<SSRStoreProvider store={store}>
|
|
72
|
-
<AppBootstrap />
|
|
73
|
-
</SSRStoreProvider>,
|
|
74
|
-
);
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
if (document.readyState !== 'loading') {
|
|
78
|
-
bootstrap();
|
|
79
|
-
} else {
|
|
80
|
-
document.addEventListener('DOMContentLoaded', () => {
|
|
81
|
-
bootstrap();
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
```
|
|
47
|
+
https://github.com/aoede3/taujs/blob/main/src/client/entry-client.tsx
|
|
86
48
|
|
|
87
49
|
### React 'entry-server.tsx'
|
|
88
50
|
|
|
89
51
|
Extended pipe object with callbacks to @taujs/server enabling additional manipulation of HEAD content from client code
|
|
90
52
|
|
|
91
|
-
|
|
92
|
-
import { ServerResponse } from 'node:http';
|
|
93
|
-
|
|
94
|
-
import React from 'react';
|
|
95
|
-
import { createSSRStore, SSRStoreProvider } from '@taujs/server/data-store';
|
|
96
|
-
import { createStreamRenderer } from '@taujs/server/render';
|
|
97
|
-
|
|
98
|
-
import AppBootstrap from '@client/AppBootstrap';
|
|
99
|
-
|
|
100
|
-
import type { RenderCallbacks } from '@taujs/server';
|
|
101
|
-
|
|
102
|
-
export const streamRender = (
|
|
103
|
-
serverResponse: ServerResponse,
|
|
104
|
-
{ onHead, onFinish, onError }: RenderCallbacks,
|
|
105
|
-
initialDataPromise: Promise<Record<string, unknown>>,
|
|
106
|
-
bootstrapModules: string,
|
|
107
|
-
) => {
|
|
108
|
-
const store = createSSRStore(initialDataPromise);
|
|
109
|
-
|
|
110
|
-
const headContent = `
|
|
111
|
-
<meta name="description" content="taujs [ τjs ]">
|
|
112
|
-
<link rel="icon" type="image/svg+xml" href="/taujs.svg" />
|
|
113
|
-
<title>taujs [ τjs ]</title>
|
|
114
|
-
`;
|
|
115
|
-
|
|
116
|
-
createStreamRenderer(
|
|
117
|
-
serverResponse,
|
|
118
|
-
{ onHead, onFinish, onError },
|
|
119
|
-
{
|
|
120
|
-
appElement: (
|
|
121
|
-
<SSRStoreProvider store={store}>
|
|
122
|
-
<AppBootstrap />
|
|
123
|
-
</SSRStoreProvider>
|
|
124
|
-
),
|
|
125
|
-
bootstrapModules,
|
|
126
|
-
getStoreSnapshot: store.getSnapshot,
|
|
127
|
-
headContent,
|
|
128
|
-
},
|
|
129
|
-
);
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
```
|
|
53
|
+
https://github.com/aoede3/taujs/blob/main/src/client/entry-server.tsx
|
|
133
54
|
|
|
134
55
|
### index.html
|
|
135
56
|
|
|
136
|
-
|
|
137
|
-
<!DOCTYPE html>
|
|
138
|
-
<html lang="en">
|
|
139
|
-
<head>
|
|
140
|
-
<meta charset="UTF-8" />
|
|
141
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
142
|
-
<!--ssr-head-->
|
|
143
|
-
</head>
|
|
144
|
-
<body>
|
|
145
|
-
<main id="root"><!--ssr-html--></main>
|
|
146
|
-
</body>
|
|
147
|
-
</html>
|
|
148
|
-
```
|
|
57
|
+
https://github.com/aoede3/taujs/blob/main/src/client/index.html
|
|
149
58
|
|
|
150
59
|
### client.d.ts
|
|
151
60
|
|
|
152
|
-
|
|
153
|
-
interface Window {
|
|
154
|
-
__INITIAL_DATA__: Record<string, unknown>;
|
|
155
|
-
}
|
|
156
|
-
```
|
|
61
|
+
https://github.com/aoede3/taujs/blob/main/src/client/client.d.ts
|
|
157
62
|
|
|
158
63
|
### Routes
|
|
159
64
|
|
|
@@ -172,73 +77,12 @@ In supporting Option 2. there is a registry of services. More detail in 'Service
|
|
|
172
77
|
|
|
173
78
|
Each routes 'path' is a simple URL regex as per below examples.
|
|
174
79
|
|
|
175
|
-
|
|
176
|
-
import type { Route, RouteParams } from '@taujs/server';
|
|
177
|
-
|
|
178
|
-
export const routes: Route<RouteParams>[] = [
|
|
179
|
-
{
|
|
180
|
-
path: '/',
|
|
181
|
-
attributes: {
|
|
182
|
-
fetch: async () => {
|
|
183
|
-
return {
|
|
184
|
-
url: 'http://localhost:5173/api/initial',
|
|
185
|
-
options: {
|
|
186
|
-
method: 'GET',
|
|
187
|
-
},
|
|
188
|
-
};
|
|
189
|
-
},
|
|
190
|
-
},
|
|
191
|
-
},
|
|
192
|
-
{
|
|
193
|
-
path: '/:id',
|
|
194
|
-
attributes: {
|
|
195
|
-
fetch: async (params: RouteParams) => {
|
|
196
|
-
return {
|
|
197
|
-
url: `http://localhost:5173/api/initial/${params.id}`,
|
|
198
|
-
options: {
|
|
199
|
-
method: 'GET',
|
|
200
|
-
},
|
|
201
|
-
};
|
|
202
|
-
},
|
|
203
|
-
},
|
|
204
|
-
},
|
|
205
|
-
{
|
|
206
|
-
path: '/:id/:another',
|
|
207
|
-
attributes: {
|
|
208
|
-
fetch: async (params: RouteParams) => {
|
|
209
|
-
return {
|
|
210
|
-
options: { params },
|
|
211
|
-
serviceMethod: 'exampleMethod',
|
|
212
|
-
serviceName: 'ServiceExample',
|
|
213
|
-
};
|
|
214
|
-
},
|
|
215
|
-
},
|
|
216
|
-
},
|
|
217
|
-
];
|
|
218
|
-
```
|
|
80
|
+
https://github.com/aoede3/taujs/blob/main/src/shared/routes/Routes.ts
|
|
219
81
|
|
|
220
82
|
### Service Registry
|
|
221
83
|
|
|
222
84
|
In supporting internal calls via τjs a registry of available services and methods provides the linkage to your own architectural setup and developmental patterns
|
|
223
85
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
import type { ServiceRegistry } from '@taujs/server';
|
|
228
|
-
|
|
229
|
-
export const serviceRegistry: ServiceRegistry = {
|
|
230
|
-
ServiceExample,
|
|
231
|
-
};
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
```
|
|
235
|
-
export const ServiceExample = {
|
|
236
|
-
async exampleMethod(params: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
237
|
-
return new Promise((resolve) => {
|
|
238
|
-
setTimeout(() => {
|
|
239
|
-
resolve({ hello: `world internal service call response with id: ${params.id} and another: ${params.another}` });
|
|
240
|
-
}, 5500);
|
|
241
|
-
});
|
|
242
|
-
},
|
|
243
|
-
};
|
|
244
|
-
```
|
|
86
|
+
https://github.com/aoede3/taujs/blob/main/src/server/services/ServiceRegistry.ts
|
|
87
|
+
|
|
88
|
+
https://github.com/aoede3/taujs/blob/main/src/server/services/ServiceExample.ts
|
package/dist/data.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ServerResponse } from 'node:http';
|
|
3
|
+
|
|
4
|
+
type SSRStore<T> = {
|
|
5
|
+
getSnapshot: () => T;
|
|
6
|
+
getServerSnapshot: () => T;
|
|
7
|
+
setData: (newData: T) => void;
|
|
8
|
+
subscribe: (callback: () => void) => () => void;
|
|
9
|
+
};
|
|
10
|
+
declare const createSSRStore: <T>(initialDataOrPromise: T | Promise<T>) => SSRStore<T>;
|
|
11
|
+
declare const SSRStoreProvider: React.FC<React.PropsWithChildren<{
|
|
12
|
+
store: SSRStore<Record<string, unknown>>;
|
|
13
|
+
}>>;
|
|
14
|
+
declare const useSSRStore: <T>() => T;
|
|
15
|
+
|
|
16
|
+
type HydrateAppOptions = {
|
|
17
|
+
appComponent: React.ReactElement;
|
|
18
|
+
initialDataKey?: keyof Window;
|
|
19
|
+
rootElementId?: string;
|
|
20
|
+
debug?: boolean;
|
|
21
|
+
};
|
|
22
|
+
declare const hydrateApp: ({ appComponent, initialDataKey, rootElementId, debug }: HydrateAppOptions) => void;
|
|
23
|
+
|
|
24
|
+
type RendererOptions = {
|
|
25
|
+
appComponent: (props: {
|
|
26
|
+
location: string;
|
|
27
|
+
}) => React.ReactElement;
|
|
28
|
+
headContent: string | ((data: Record<string, unknown>) => string);
|
|
29
|
+
};
|
|
30
|
+
type RenderCallbacks = {
|
|
31
|
+
onHead: (headContent: string) => void;
|
|
32
|
+
onFinish: (initialDataResolved: unknown) => void;
|
|
33
|
+
onError: (error: unknown) => void;
|
|
34
|
+
};
|
|
35
|
+
declare const resolveHeadContent: (headContent: string | ((meta: Record<string, unknown>) => string), meta?: Record<string, unknown>) => string;
|
|
36
|
+
declare const createRenderer: ({ appComponent, headContent }: RendererOptions) => {
|
|
37
|
+
renderSSR: (initialDataResolved: Record<string, unknown>, location: string, meta?: Record<string, unknown>) => Promise<{
|
|
38
|
+
headContent: string;
|
|
39
|
+
appHtml: string;
|
|
40
|
+
}>;
|
|
41
|
+
renderStream: (serverResponse: ServerResponse, callbacks: RenderCallbacks, initialDataResolved: Record<string, unknown>, location: string, bootstrapModules?: string, meta?: Record<string, unknown>) => void;
|
|
42
|
+
};
|
|
43
|
+
declare const createRenderStream: (serverResponse: ServerResponse, { onHead, onFinish, onError }: RenderCallbacks, { appComponent, headContent, initialDataResolved, location, bootstrapModules, }: RendererOptions & {
|
|
44
|
+
initialDataResolved: Record<string, unknown>;
|
|
45
|
+
location: string;
|
|
46
|
+
bootstrapModules?: string;
|
|
47
|
+
}) => void;
|
|
48
|
+
|
|
49
|
+
export { type SSRStore, SSRStoreProvider, createRenderStream, createRenderer, createSSRStore, hydrateApp, resolveHeadContent, useSSRStore };
|
package/dist/data.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// src/SSRDataStore.tsx
|
|
2
|
+
import { createContext, useContext, useSyncExternalStore } from "react";
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
var createSSRStore = (initialDataOrPromise) => {
|
|
5
|
+
let currentData;
|
|
6
|
+
let status;
|
|
7
|
+
const subscribers = /* @__PURE__ */ new Set();
|
|
8
|
+
let serverDataPromise;
|
|
9
|
+
if (initialDataOrPromise instanceof Promise) {
|
|
10
|
+
status = "pending";
|
|
11
|
+
serverDataPromise = initialDataOrPromise.then((data) => {
|
|
12
|
+
currentData = data;
|
|
13
|
+
status = "success";
|
|
14
|
+
subscribers.forEach((callback) => callback());
|
|
15
|
+
}).catch((error) => {
|
|
16
|
+
console.error("Failed to load initial data:", error);
|
|
17
|
+
status = "error";
|
|
18
|
+
}).then(() => {
|
|
19
|
+
});
|
|
20
|
+
} else {
|
|
21
|
+
currentData = initialDataOrPromise;
|
|
22
|
+
status = "success";
|
|
23
|
+
serverDataPromise = Promise.resolve();
|
|
24
|
+
}
|
|
25
|
+
const setData = (newData) => {
|
|
26
|
+
currentData = newData;
|
|
27
|
+
status = "success";
|
|
28
|
+
subscribers.forEach((callback) => callback());
|
|
29
|
+
};
|
|
30
|
+
const subscribe = (callback) => {
|
|
31
|
+
subscribers.add(callback);
|
|
32
|
+
return () => subscribers.delete(callback);
|
|
33
|
+
};
|
|
34
|
+
const getSnapshot = () => {
|
|
35
|
+
if (status === "pending") {
|
|
36
|
+
throw serverDataPromise;
|
|
37
|
+
} else if (status === "error") {
|
|
38
|
+
throw new Error("An error occurred while fetching the data.");
|
|
39
|
+
}
|
|
40
|
+
return currentData;
|
|
41
|
+
};
|
|
42
|
+
const getServerSnapshot = () => {
|
|
43
|
+
if (status === "pending") {
|
|
44
|
+
throw serverDataPromise;
|
|
45
|
+
} else if (status === "error") {
|
|
46
|
+
throw new Error("Data is not available on the server.");
|
|
47
|
+
}
|
|
48
|
+
return currentData;
|
|
49
|
+
};
|
|
50
|
+
return { getSnapshot, getServerSnapshot, setData, subscribe };
|
|
51
|
+
};
|
|
52
|
+
var SSRStoreContext = createContext(null);
|
|
53
|
+
var SSRStoreProvider = ({ store, children }) => /* @__PURE__ */ jsx(SSRStoreContext.Provider, { value: store, children });
|
|
54
|
+
var useSSRStore = () => {
|
|
55
|
+
const store = useContext(SSRStoreContext);
|
|
56
|
+
if (!store) throw new Error("useSSRStore must be used within a SSRStoreProvider");
|
|
57
|
+
return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getServerSnapshot);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// src/SSRHydration.tsx
|
|
61
|
+
import React2 from "react";
|
|
62
|
+
import { hydrateRoot } from "react-dom/client";
|
|
63
|
+
|
|
64
|
+
// src/utils/Logger.ts
|
|
65
|
+
var createLogger = (debug) => ({
|
|
66
|
+
log: (...args) => {
|
|
67
|
+
if (debug) console.log(...args);
|
|
68
|
+
},
|
|
69
|
+
warn: (...args) => {
|
|
70
|
+
if (debug) console.warn(...args);
|
|
71
|
+
},
|
|
72
|
+
error: (...args) => {
|
|
73
|
+
if (debug) console.error(...args);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// src/SSRHydration.tsx
|
|
78
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
79
|
+
var hydrateApp = ({ appComponent, initialDataKey = "__INITIAL_DATA__", rootElementId = "root", debug = false }) => {
|
|
80
|
+
const { log, warn, error } = createLogger(debug);
|
|
81
|
+
const bootstrap = () => {
|
|
82
|
+
log("Hydration started");
|
|
83
|
+
const rootElement = document.getElementById(rootElementId);
|
|
84
|
+
if (!rootElement) {
|
|
85
|
+
error(`Root element with id "${rootElementId}" not found.`);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const initialData = window[initialDataKey];
|
|
89
|
+
if (!initialData) {
|
|
90
|
+
warn(`Initial data key "${initialDataKey}" is undefined on window.`);
|
|
91
|
+
} else {
|
|
92
|
+
log("Initial data loaded:", initialData);
|
|
93
|
+
}
|
|
94
|
+
const initialDataPromise = Promise.resolve(initialData);
|
|
95
|
+
const store = createSSRStore(initialDataPromise);
|
|
96
|
+
log("Store created:", store);
|
|
97
|
+
hydrateRoot(
|
|
98
|
+
rootElement,
|
|
99
|
+
/* @__PURE__ */ jsx2(React2.StrictMode, { children: /* @__PURE__ */ jsx2(SSRStoreProvider, { store, children: appComponent }) })
|
|
100
|
+
);
|
|
101
|
+
log("Hydration completed");
|
|
102
|
+
};
|
|
103
|
+
if (document.readyState !== "loading") {
|
|
104
|
+
bootstrap();
|
|
105
|
+
} else {
|
|
106
|
+
document.addEventListener("DOMContentLoaded", bootstrap);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// src/SSRRender.tsx
|
|
111
|
+
import { Writable } from "node:stream";
|
|
112
|
+
import "react";
|
|
113
|
+
import { renderToPipeableStream, renderToString } from "react-dom/server";
|
|
114
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
115
|
+
var resolveHeadContent = (headContent, meta = {}) => typeof headContent === "function" ? headContent(meta) : headContent;
|
|
116
|
+
var createRenderer = ({ appComponent, headContent }) => {
|
|
117
|
+
const renderSSR = async (initialDataResolved, location, meta = {}) => {
|
|
118
|
+
const dataForHeadContent = Object.keys(initialDataResolved).length > 0 ? initialDataResolved : meta;
|
|
119
|
+
const dynamicHeadContent = resolveHeadContent(headContent, dataForHeadContent);
|
|
120
|
+
const appHtml = renderToString(/* @__PURE__ */ jsx3(SSRStoreProvider, { store: createSSRStore(initialDataResolved), children: appComponent({ location }) }));
|
|
121
|
+
return {
|
|
122
|
+
headContent: dynamicHeadContent,
|
|
123
|
+
appHtml
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
const renderStream = (serverResponse, callbacks, initialDataResolved, location, bootstrapModules, meta = {}) => {
|
|
127
|
+
const dynamicHeadContent = resolveHeadContent(headContent, meta);
|
|
128
|
+
createRenderStream(serverResponse, callbacks, {
|
|
129
|
+
appComponent: (props) => appComponent({ ...props, location }),
|
|
130
|
+
headContent: dynamicHeadContent,
|
|
131
|
+
initialDataResolved,
|
|
132
|
+
location,
|
|
133
|
+
bootstrapModules
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
return { renderSSR, renderStream };
|
|
137
|
+
};
|
|
138
|
+
var createRenderStream = (serverResponse, { onHead, onFinish, onError }, {
|
|
139
|
+
appComponent,
|
|
140
|
+
headContent,
|
|
141
|
+
initialDataResolved,
|
|
142
|
+
location,
|
|
143
|
+
bootstrapModules
|
|
144
|
+
}) => {
|
|
145
|
+
const store = createSSRStore(initialDataResolved);
|
|
146
|
+
const appElement = /* @__PURE__ */ jsx3(SSRStoreProvider, { store, children: appComponent({ location }) });
|
|
147
|
+
const { pipe } = renderToPipeableStream(appElement, {
|
|
148
|
+
bootstrapModules: bootstrapModules ? [bootstrapModules] : void 0,
|
|
149
|
+
onShellReady() {
|
|
150
|
+
const dynamicHeadContent = resolveHeadContent(headContent, initialDataResolved);
|
|
151
|
+
onHead(dynamicHeadContent);
|
|
152
|
+
pipe(
|
|
153
|
+
new Writable({
|
|
154
|
+
write(chunk, _encoding, callback) {
|
|
155
|
+
serverResponse.write(chunk, callback);
|
|
156
|
+
},
|
|
157
|
+
final(callback) {
|
|
158
|
+
onFinish(store.getSnapshot());
|
|
159
|
+
callback();
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
);
|
|
163
|
+
},
|
|
164
|
+
onAllReady() {
|
|
165
|
+
},
|
|
166
|
+
onError(error) {
|
|
167
|
+
onError(error);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
};
|
|
171
|
+
export {
|
|
172
|
+
SSRStoreProvider,
|
|
173
|
+
createRenderStream,
|
|
174
|
+
createRenderer,
|
|
175
|
+
createSSRStore,
|
|
176
|
+
hydrateApp,
|
|
177
|
+
resolveHeadContent,
|
|
178
|
+
useSSRStore
|
|
179
|
+
};
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { FastifyPluginAsync } from 'fastify';
|
|
2
2
|
import { ServerResponse } from 'node:http';
|
|
3
3
|
|
|
4
|
+
declare const RENDERTYPE: {
|
|
5
|
+
ssr: string;
|
|
6
|
+
streaming: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
4
9
|
declare const SSRServer: FastifyPluginAsync<SSRServerOptions>;
|
|
5
10
|
type ServiceRegistry = {
|
|
6
11
|
[serviceName: string]: {
|
|
@@ -40,8 +45,15 @@ type Manifest = {
|
|
|
40
45
|
assets?: string[];
|
|
41
46
|
};
|
|
42
47
|
};
|
|
48
|
+
type RenderSSR = (initialDataResolved: Record<string, unknown>, location: string, meta?: Record<string, unknown>) => Promise<{
|
|
49
|
+
headContent: string;
|
|
50
|
+
appHtml: string;
|
|
51
|
+
initialDataScript: string;
|
|
52
|
+
}>;
|
|
53
|
+
type RenderStream = (serverResponse: ServerResponse, callbacks: RenderCallbacks, initialDataPromise: Promise<Record<string, unknown>>, location: string, bootstrapModules?: string, meta?: Record<string, unknown>) => void;
|
|
43
54
|
type RenderModule = {
|
|
44
|
-
|
|
55
|
+
renderSSR: RenderSSR;
|
|
56
|
+
renderStream: RenderStream;
|
|
45
57
|
};
|
|
46
58
|
type RouteAttributes<Params = {}> = {
|
|
47
59
|
fetch: (params?: Params, options?: RequestInit & {
|
|
@@ -54,9 +66,11 @@ type RouteAttributes<Params = {}> = {
|
|
|
54
66
|
serviceMethod?: string;
|
|
55
67
|
url?: string;
|
|
56
68
|
}>;
|
|
69
|
+
meta?: Record<string, unknown>;
|
|
70
|
+
render?: typeof RENDERTYPE.ssr | typeof RENDERTYPE.streaming;
|
|
57
71
|
};
|
|
58
72
|
type Route<Params = {}> = {
|
|
59
|
-
|
|
73
|
+
attr?: RouteAttributes<Params>;
|
|
60
74
|
path: string;
|
|
61
75
|
};
|
|
62
76
|
interface InitialRouteParams extends Record<string, unknown> {
|
|
@@ -66,4 +80,4 @@ interface InitialRouteParams extends Record<string, unknown> {
|
|
|
66
80
|
type RouteParams = InitialRouteParams & Record<string, unknown>;
|
|
67
81
|
type RoutePathsAndAttributes<Params = {}> = Omit<Route<Params>, 'element'>;
|
|
68
82
|
|
|
69
|
-
export { type FetchConfig, type InitialRouteParams, type Manifest, type RenderCallbacks, type RenderModule, type Route, type RouteAttributes, type RouteParams, type RoutePathsAndAttributes, SSRServer, type SSRServerOptions, type ServiceRegistry };
|
|
83
|
+
export { type FetchConfig, type InitialRouteParams, type Manifest, type RenderCallbacks, type RenderModule, type RenderSSR, type RenderStream, type Route, type RouteAttributes, type RouteParams, type RoutePathsAndAttributes, SSRServer, type SSRServerOptions, type ServiceRegistry };
|
|
@@ -121,7 +121,7 @@ import { readFile } from "node:fs/promises";
|
|
|
121
121
|
import path from "node:path";
|
|
122
122
|
import { createViteRuntime } from "vite";
|
|
123
123
|
|
|
124
|
-
// src/utils/
|
|
124
|
+
// src/utils/Utils.ts
|
|
125
125
|
import { fileURLToPath } from "node:url";
|
|
126
126
|
import { dirname, join } from "node:path";
|
|
127
127
|
import { match } from "path-to-regexp";
|
|
@@ -211,9 +211,9 @@ var fetchData = async ({ url, options }) => {
|
|
|
211
211
|
}
|
|
212
212
|
throw new Error("URL must be provided to fetch data");
|
|
213
213
|
};
|
|
214
|
-
var fetchInitialData = async (
|
|
215
|
-
if (
|
|
216
|
-
return
|
|
214
|
+
var fetchInitialData = async (attr, params, serviceRegistry) => {
|
|
215
|
+
if (attr && typeof attr.fetch === "function") {
|
|
216
|
+
return attr.fetch(params, {
|
|
217
217
|
headers: { "Content-Type": "application/json" },
|
|
218
218
|
params
|
|
219
219
|
}).then(async (data) => {
|
|
@@ -257,6 +257,16 @@ var overrideCSSHMRConsoleError = () => {
|
|
|
257
257
|
};
|
|
258
258
|
};
|
|
259
259
|
|
|
260
|
+
// src/constants.ts
|
|
261
|
+
var RENDERTYPE = {
|
|
262
|
+
ssr: "ssr",
|
|
263
|
+
streaming: "streaming"
|
|
264
|
+
};
|
|
265
|
+
var SSRTAG = {
|
|
266
|
+
ssrHead: "<!--ssr-head-->",
|
|
267
|
+
ssrHtml: "<!--ssr-html-->"
|
|
268
|
+
};
|
|
269
|
+
|
|
260
270
|
// src/SSRServer.ts
|
|
261
271
|
var SSRServer = (0, import_fastify_plugin.default)(
|
|
262
272
|
async (app, opts) => {
|
|
@@ -275,6 +285,12 @@ var SSRServer = (0, import_fastify_plugin.default)(
|
|
|
275
285
|
let template = templateHtml;
|
|
276
286
|
let viteDevServer;
|
|
277
287
|
let viteRuntime;
|
|
288
|
+
void await app.register(import("@fastify/static"), {
|
|
289
|
+
index: false,
|
|
290
|
+
prefix: "/",
|
|
291
|
+
root: clientRoot,
|
|
292
|
+
wildcard: false
|
|
293
|
+
});
|
|
278
294
|
if (isDevelopment) {
|
|
279
295
|
const { createServer } = await import("vite");
|
|
280
296
|
viteDevServer = await createServer({
|
|
@@ -326,14 +342,10 @@ var SSRServer = (0, import_fastify_plugin.default)(
|
|
|
326
342
|
});
|
|
327
343
|
} else {
|
|
328
344
|
renderModule = await import(path.join(clientRoot, `${clientEntryServer}.js`));
|
|
329
|
-
void await app.register(import("@fastify/static"), {
|
|
330
|
-
index: false,
|
|
331
|
-
root: path.resolve(clientRoot),
|
|
332
|
-
wildcard: false
|
|
333
|
-
});
|
|
334
345
|
}
|
|
335
346
|
void app.get("/*", async (req, reply) => {
|
|
336
347
|
try {
|
|
348
|
+
if (/\.\w+$/.test(req.raw.url ?? "")) return reply.callNotFound();
|
|
337
349
|
const url = req.url ? new URL(req.url, `http://${req.headers.host}`).pathname : "/";
|
|
338
350
|
const matchedRoute = matchRoute(url, routes);
|
|
339
351
|
if (!matchedRoute) {
|
|
@@ -341,41 +353,54 @@ var SSRServer = (0, import_fastify_plugin.default)(
|
|
|
341
353
|
return;
|
|
342
354
|
}
|
|
343
355
|
if (isDevelopment) {
|
|
344
|
-
template =
|
|
356
|
+
template = template.replace(/<script type="module" src="\/@vite\/client"><\/script>/g, "");
|
|
357
|
+
template = template.replace(/<style type="text\/css">[\s\S]*?<\/style>/g, "");
|
|
345
358
|
renderModule = await viteRuntime.executeEntrypoint(path.join(clientRoot, `${clientEntryServer}.tsx`));
|
|
346
359
|
styles = await collectStyle(viteDevServer, [`${clientRoot}/${clientEntryServer}.tsx`]);
|
|
347
360
|
template = template.replace("</head>", `<style type="text/css">${styles}</style></head>`);
|
|
361
|
+
template = await viteDevServer.transformIndexHtml(url, template);
|
|
348
362
|
}
|
|
349
|
-
const { streamRender } = renderModule;
|
|
350
363
|
const { route, params } = matchedRoute;
|
|
351
|
-
const {
|
|
352
|
-
const
|
|
353
|
-
const [
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
{
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
364
|
+
const { attr } = route;
|
|
365
|
+
const renderType = attr?.render || RENDERTYPE.streaming;
|
|
366
|
+
const [beforeBody = "", afterBody] = template.split(SSRTAG.ssrHtml);
|
|
367
|
+
const [beforeHead, afterHead] = beforeBody.split(SSRTAG.ssrHead);
|
|
368
|
+
const initialDataPromise = fetchInitialData(attr, params, serviceRegistry);
|
|
369
|
+
if (renderType === RENDERTYPE.ssr) {
|
|
370
|
+
const { renderSSR } = renderModule;
|
|
371
|
+
const initialDataResolved = await initialDataPromise;
|
|
372
|
+
const initialDataScript = `<script>window.__INITIAL_DATA__ = ${JSON.stringify(initialDataResolved).replace(/</g, "\\u003c")}</script>`;
|
|
373
|
+
const { headContent, appHtml } = await renderSSR(initialDataResolved, req.url, attr?.meta);
|
|
374
|
+
const fullHtml = template.replace(SSRTAG.ssrHead, headContent).replace(SSRTAG.ssrHtml, `${appHtml}${initialDataScript}<script type="module" src="${bootstrapModules}" async=""></script>`);
|
|
375
|
+
return reply.status(200).header("Content-Type", "text/html").send(fullHtml);
|
|
376
|
+
} else {
|
|
377
|
+
const { renderStream } = renderModule;
|
|
378
|
+
reply.raw.writeHead(200, { "Content-Type": "text/html" });
|
|
379
|
+
renderStream(
|
|
380
|
+
reply.raw,
|
|
381
|
+
{
|
|
382
|
+
onHead: (headContent) => {
|
|
383
|
+
let aggregateHeadContent = headContent;
|
|
384
|
+
if (ssrManifest) aggregateHeadContent += preloadLinks;
|
|
385
|
+
if (manifest) aggregateHeadContent += cssLinks;
|
|
386
|
+
reply.raw.write(`${beforeHead}${aggregateHeadContent}${afterHead}`);
|
|
387
|
+
},
|
|
388
|
+
onFinish: async (initialDataResolved) => {
|
|
389
|
+
reply.raw.write(`<script>window.__INITIAL_DATA__ = ${JSON.stringify(initialDataResolved).replace(/</g, "\\u003c")}</script>`);
|
|
390
|
+
reply.raw.write(afterBody);
|
|
391
|
+
reply.raw.end();
|
|
392
|
+
},
|
|
393
|
+
onError: (error) => {
|
|
394
|
+
console.error("Critical rendering onError:", error);
|
|
395
|
+
reply.raw.end("Internal Server Error");
|
|
396
|
+
}
|
|
370
397
|
},
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
bootstrapModules
|
|
378
|
-
);
|
|
398
|
+
initialDataPromise,
|
|
399
|
+
req.url,
|
|
400
|
+
bootstrapModules,
|
|
401
|
+
attr?.meta
|
|
402
|
+
);
|
|
403
|
+
}
|
|
379
404
|
} catch (error) {
|
|
380
405
|
console.error("Error setting up SSR stream:", error);
|
|
381
406
|
if (!reply.raw.headersSent) reply.raw.writeHead(500, { "Content-Type": "text/plain" });
|
|
@@ -386,7 +411,7 @@ var SSRServer = (0, import_fastify_plugin.default)(
|
|
|
386
411
|
if (/\.\w+$/.test(req.raw.url ?? "")) return reply.callNotFound();
|
|
387
412
|
try {
|
|
388
413
|
let template2 = templateHtml;
|
|
389
|
-
template2 = template2.replace(
|
|
414
|
+
template2 = template2.replace(SSRTAG.ssrHead, "").replace(SSRTAG.ssrHtml, "");
|
|
390
415
|
if (!isDevelopment) template2 = template2.replace("</head>", `${getCssLinks(manifest)}</head>`);
|
|
391
416
|
template2 = template2.replace("</body>", `<script type="module" src="${bootstrapModules}" async=""></script></body>`);
|
|
392
417
|
reply.status(200).type("text/html").send(template2);
|
package/package.json
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
{
|
|
2
|
+
"name": "@taujs/server",
|
|
3
|
+
"version": "0.0.9",
|
|
4
|
+
"description": "taujs | τjs",
|
|
2
5
|
"author": "Aoede <taujs@aoede.uk.net> (https://www.aoede.uk.net)",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://github.com/aoede3/taujs-server",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/aoede3/taujs-server.git"
|
|
11
|
+
},
|
|
3
12
|
"bugs": {
|
|
4
13
|
"url": "https://github.com/aoede3/taujs-server/issues"
|
|
5
14
|
},
|
|
6
|
-
"description": "taujs | τjs",
|
|
7
|
-
"homepage": "https://github.com/aoede3/taujs-server",
|
|
8
15
|
"keywords": [
|
|
9
16
|
"fastify",
|
|
10
17
|
"typescript",
|
|
@@ -14,46 +21,25 @@
|
|
|
14
21
|
"react",
|
|
15
22
|
"ssr"
|
|
16
23
|
],
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"type": "git",
|
|
21
|
-
"url": "git+https://github.com/aoede3/taujs-server.git"
|
|
22
|
-
},
|
|
23
|
-
"version": "0.0.7",
|
|
24
|
+
"type": "module",
|
|
25
|
+
"main": "./dist/index.js",
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
24
27
|
"exports": {
|
|
25
28
|
".": {
|
|
26
|
-
"import": "./dist/
|
|
27
|
-
"types": "./dist/
|
|
28
|
-
},
|
|
29
|
-
"./data-store": {
|
|
30
|
-
"import": "./dist/SSRDataStore.js",
|
|
31
|
-
"types": "./dist/SSRDataStore.d.ts"
|
|
29
|
+
"import": "./dist/index.js",
|
|
30
|
+
"types": "./dist/index.d.ts"
|
|
32
31
|
},
|
|
33
|
-
"./
|
|
34
|
-
"import": "./dist/
|
|
35
|
-
"types": "./dist/
|
|
32
|
+
"./data": {
|
|
33
|
+
"import": "./dist/data.js",
|
|
34
|
+
"types": "./dist/data.d.ts"
|
|
36
35
|
},
|
|
37
36
|
"./package.json": "./package.json"
|
|
38
37
|
},
|
|
39
38
|
"files": [
|
|
40
39
|
"dist"
|
|
41
40
|
],
|
|
42
|
-
"main": "./dist/SSRServer.js",
|
|
43
|
-
"types": "./dist/SSRServer.d.ts",
|
|
44
|
-
"typesVersions": {
|
|
45
|
-
"*": {
|
|
46
|
-
"data-store": [
|
|
47
|
-
"./dist/SSRDataStore.d.ts"
|
|
48
|
-
],
|
|
49
|
-
"render": [
|
|
50
|
-
"./dist/SSRRender.d.ts"
|
|
51
|
-
]
|
|
52
|
-
}
|
|
53
|
-
},
|
|
54
|
-
"type": "module",
|
|
55
41
|
"dependencies": {
|
|
56
|
-
"@fastify/static": "^
|
|
42
|
+
"@fastify/static": "^8.0.2",
|
|
57
43
|
"path-to-regexp": "^8.1.0"
|
|
58
44
|
},
|
|
59
45
|
"devDependencies": {
|
|
@@ -66,7 +52,7 @@
|
|
|
66
52
|
"@types/react": "^18.3.3",
|
|
67
53
|
"@types/react-dom": "^18.3.0",
|
|
68
54
|
"@vitest/coverage-v8": "^2.1.0",
|
|
69
|
-
"fastify": "^
|
|
55
|
+
"fastify": "^5.0.0",
|
|
70
56
|
"jsdom": "^25.0.0",
|
|
71
57
|
"prettier": "^3.3.3",
|
|
72
58
|
"react-dom": "^18.3.1",
|
|
@@ -76,7 +62,6 @@
|
|
|
76
62
|
"vitest": "^2.0.5"
|
|
77
63
|
},
|
|
78
64
|
"peerDependencies": {
|
|
79
|
-
"@vitejs/plugin-react": "^4.3.1",
|
|
80
65
|
"fastify": "^5.0.0",
|
|
81
66
|
"react": "^18.3.1",
|
|
82
67
|
"react-dom": "^18.3.1",
|
|
@@ -85,9 +70,11 @@
|
|
|
85
70
|
},
|
|
86
71
|
"scripts": {
|
|
87
72
|
"build": "tsup",
|
|
88
|
-
"
|
|
73
|
+
"build-local": "tsup && ./move.sh",
|
|
74
|
+
"ci": "npm run build && npm run check-format && npm run lint",
|
|
89
75
|
"lint": "tsc",
|
|
90
76
|
"test": "vitest run",
|
|
77
|
+
"test:ui": "vitest --ui --coverage.enabled=true",
|
|
91
78
|
"coverage": "vitest run --coverage",
|
|
92
79
|
"format": "prettier --write .",
|
|
93
80
|
"check-format": "prettier --check .",
|
package/dist/SSRDataStore.d.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
|
|
3
|
-
type SSRStore<T> = {
|
|
4
|
-
getSnapshot: () => T;
|
|
5
|
-
getServerSnapshot: () => T;
|
|
6
|
-
setData: (newData: T) => void;
|
|
7
|
-
subscribe: (callback: () => void) => () => void;
|
|
8
|
-
};
|
|
9
|
-
declare const createSSRStore: <T>(initialDataPromise: Promise<T>) => SSRStore<T>;
|
|
10
|
-
declare const SSRStoreProvider: React.FC<React.PropsWithChildren<{
|
|
11
|
-
store: SSRStore<Record<string, unknown>>;
|
|
12
|
-
}>>;
|
|
13
|
-
declare const useSSRStore: <T>() => SSRStore<T>;
|
|
14
|
-
|
|
15
|
-
export { type SSRStore, SSRStoreProvider, createSSRStore, useSSRStore };
|
package/dist/SSRDataStore.js
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
// src/SSRDataStore.tsx
|
|
2
|
-
import { createContext, useContext, useSyncExternalStore } from "react";
|
|
3
|
-
import { jsx } from "react/jsx-runtime";
|
|
4
|
-
var createSSRStore = (initialDataPromise) => {
|
|
5
|
-
let currentData;
|
|
6
|
-
let status = "pending";
|
|
7
|
-
const subscribers = /* @__PURE__ */ new Set();
|
|
8
|
-
let resolvePromise = null;
|
|
9
|
-
const serverDataPromise = new Promise((resolve) => resolvePromise = resolve);
|
|
10
|
-
initialDataPromise.then((data) => {
|
|
11
|
-
currentData = data;
|
|
12
|
-
status = "success";
|
|
13
|
-
subscribers.forEach((callback) => callback());
|
|
14
|
-
if (resolvePromise) resolvePromise();
|
|
15
|
-
}).catch((error) => {
|
|
16
|
-
console.error("Failed to load initial data:", error);
|
|
17
|
-
status = "error";
|
|
18
|
-
});
|
|
19
|
-
const setData = (newData) => {
|
|
20
|
-
currentData = newData;
|
|
21
|
-
status = "success";
|
|
22
|
-
subscribers.forEach((callback) => callback());
|
|
23
|
-
if (resolvePromise) resolvePromise();
|
|
24
|
-
};
|
|
25
|
-
const subscribe = (callback) => {
|
|
26
|
-
subscribers.add(callback);
|
|
27
|
-
return () => subscribers.delete(callback);
|
|
28
|
-
};
|
|
29
|
-
const getSnapshot = () => {
|
|
30
|
-
if (status === "pending") {
|
|
31
|
-
throw initialDataPromise;
|
|
32
|
-
} else if (status === "error") {
|
|
33
|
-
throw new Error("An error occurred while fetching the data.");
|
|
34
|
-
}
|
|
35
|
-
return currentData;
|
|
36
|
-
};
|
|
37
|
-
const getServerSnapshot = () => {
|
|
38
|
-
if (status === "pending") {
|
|
39
|
-
throw serverDataPromise;
|
|
40
|
-
} else if (status === "error") {
|
|
41
|
-
throw new Error("Data is not available on the server.");
|
|
42
|
-
}
|
|
43
|
-
return currentData;
|
|
44
|
-
};
|
|
45
|
-
return { getSnapshot, getServerSnapshot, setData, subscribe };
|
|
46
|
-
};
|
|
47
|
-
var SSRStoreContext = createContext(null);
|
|
48
|
-
var SSRStoreProvider = ({ store, children }) => /* @__PURE__ */ jsx(SSRStoreContext.Provider, { value: store, children });
|
|
49
|
-
var useSSRStore = () => {
|
|
50
|
-
const store = useContext(SSRStoreContext);
|
|
51
|
-
if (!store) throw new Error("useSSRStore must be used within a SSRStoreProvider");
|
|
52
|
-
return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getServerSnapshot);
|
|
53
|
-
};
|
|
54
|
-
export {
|
|
55
|
-
SSRStoreProvider,
|
|
56
|
-
createSSRStore,
|
|
57
|
-
useSSRStore
|
|
58
|
-
};
|
package/dist/SSRRender.d.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { ServerResponse } from 'node:http';
|
|
2
|
-
import React from 'react';
|
|
3
|
-
|
|
4
|
-
type RenderCallbacks = {
|
|
5
|
-
onHead: (headContent: string) => void;
|
|
6
|
-
onFinish: (initialDataResolved: unknown) => void;
|
|
7
|
-
onError: (error: unknown) => void;
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
type StreamRender = {
|
|
11
|
-
appElement: React.JSX.Element;
|
|
12
|
-
bootstrapModules: string;
|
|
13
|
-
headContent: string;
|
|
14
|
-
getStoreSnapshot: () => unknown;
|
|
15
|
-
};
|
|
16
|
-
declare const createStreamRenderer: (serverResponse: ServerResponse, { onHead, onFinish, onError }: RenderCallbacks, { appElement, bootstrapModules, headContent, getStoreSnapshot }: StreamRender) => void;
|
|
17
|
-
|
|
18
|
-
export { createStreamRenderer };
|
package/dist/SSRRender.js
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
// src/SSRRender.ts
|
|
2
|
-
import { Writable } from "node:stream";
|
|
3
|
-
import "react";
|
|
4
|
-
import { renderToPipeableStream } from "react-dom/server";
|
|
5
|
-
var createStreamRenderer = (serverResponse, { onHead, onFinish, onError }, { appElement, bootstrapModules, headContent, getStoreSnapshot }) => {
|
|
6
|
-
const { pipe } = renderToPipeableStream(appElement, {
|
|
7
|
-
bootstrapModules: [bootstrapModules],
|
|
8
|
-
onShellReady() {
|
|
9
|
-
onHead(headContent);
|
|
10
|
-
pipe(
|
|
11
|
-
new Writable({
|
|
12
|
-
write(chunk, _encoding, callback) {
|
|
13
|
-
serverResponse.write(chunk, callback);
|
|
14
|
-
},
|
|
15
|
-
final(callback) {
|
|
16
|
-
onFinish(getStoreSnapshot());
|
|
17
|
-
callback();
|
|
18
|
-
}
|
|
19
|
-
})
|
|
20
|
-
);
|
|
21
|
-
},
|
|
22
|
-
onAllReady() {
|
|
23
|
-
},
|
|
24
|
-
onError(error) {
|
|
25
|
-
onError(error);
|
|
26
|
-
}
|
|
27
|
-
});
|
|
28
|
-
};
|
|
29
|
-
export {
|
|
30
|
-
createStreamRenderer
|
|
31
|
-
};
|