@taujs/server 0.0.1 → 0.0.4
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 +233 -15
- package/dist/SSRDataStore.d.ts +1 -1
- package/dist/SSRRender.d.ts +18 -0
- package/dist/SSRRender.js +31 -0
- package/dist/SSRServer.d.ts +1 -0
- package/dist/SSRServer.js +7 -4
- package/package.json +10 -1
package/README.md
CHANGED
|
@@ -1,26 +1,244 @@
|
|
|
1
1
|
# @taujs/server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`npm install @taujs/server`
|
|
4
|
+
|
|
5
|
+
`yarn add @taujs/server`
|
|
6
|
+
|
|
7
|
+
`pnpm add @taujs/server`
|
|
8
|
+
|
|
9
|
+
## Streaming React SSR & Hydration
|
|
10
|
+
|
|
11
|
+
Fastify Plugin for integration with taujs [ τjs ] template https://github.com/aoede3/taujs
|
|
12
|
+
|
|
13
|
+
- Production: Fastify, React
|
|
14
|
+
- Development: Fastify, React, tsx, Vite
|
|
4
15
|
|
|
5
16
|
TypeScript / ESM-only focus
|
|
6
|
-
τjs - DX: Developer eXperience first focus. Integrated ViteDevServer HMR + Vite Runtime API run alongside tsx (TS eXecute) providing fast responsive dev reload times for both backend / frontend
|
|
7
17
|
|
|
8
|
-
|
|
18
|
+
## τjs - Developer eXperience
|
|
19
|
+
|
|
20
|
+
Integrated ViteDevServer HMR + Vite Runtime API run alongside tsx (TS eXecute) providing fast responsive dev reload times for both backend / frontend
|
|
9
21
|
|
|
10
|
-
Fastify https://fastify.dev/
|
|
11
|
-
|
|
12
|
-
|
|
22
|
+
- Fastify https://fastify.dev/
|
|
23
|
+
- React https://reactjs.org/
|
|
24
|
+
- tsx https://tsx.is/
|
|
25
|
+
- Vite https://vitejs.dev/guide/ssr#building-for-production
|
|
13
26
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
ESM https://nodejs.org/api/esm.html
|
|
27
|
+
- ViteDevServer HMR https://vitejs.dev/guide/ssr#setting-up-the-dev-server
|
|
28
|
+
- Vite Runtime API https://vitejs.dev/guide/api-vite-runtime
|
|
29
|
+
- ESBuild https://esbuild.github.io/
|
|
30
|
+
- Rollup https://rollupjs.org/
|
|
31
|
+
- ESM https://nodejs.org/api/esm.html
|
|
20
32
|
|
|
21
33
|
## Development / CI
|
|
22
34
|
|
|
23
|
-
|
|
24
|
-
|
|
35
|
+
`npm install --legacy-peer-deps`
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
### Fastify
|
|
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
|
+
```
|
|
53
|
+
|
|
54
|
+
Not utilising taujs [ τjs ] template? Add in your own `alias` object for your own particular setup e.g. `alias: { object }`
|
|
55
|
+
|
|
56
|
+
### React 'entry-client.tsx'
|
|
57
|
+
|
|
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
|
+
```
|
|
86
|
+
|
|
87
|
+
### React 'entry-server.tsx'
|
|
88
|
+
|
|
89
|
+
Extended pipe object with callbacks to @taujs/server enabling additional manipulation of HEAD content from client code
|
|
90
|
+
|
|
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
|
+
```
|
|
133
|
+
|
|
134
|
+
### index.html
|
|
135
|
+
|
|
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
|
+
```
|
|
149
|
+
|
|
150
|
+
### client.d.ts
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
interface Window {
|
|
154
|
+
__INITIAL_DATA__: Record<string, unknown>;
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Routes
|
|
159
|
+
|
|
160
|
+
Integral to τjs is its internal routing:
|
|
161
|
+
|
|
162
|
+
1. Fastify serving index.html to client browser for client routing
|
|
163
|
+
2. Internal service calls to API to provide data for streaming/hydration
|
|
164
|
+
3. Fastify serving API calls via HTTP in the more traditional sense of client/server
|
|
165
|
+
|
|
166
|
+
In ensuring a particular 'route' receives data for hydration there are two options:
|
|
167
|
+
|
|
168
|
+
1. An HTTP call elsewhere syntactically not unlike 'fetch' providing params to a 'fetch' call
|
|
169
|
+
2. Internally calling a service which in turn will make 'call' to return data as per your architecture
|
|
170
|
+
|
|
171
|
+
In supporting Option 2. there is a registry of services. More detail in 'Service Registry'.
|
|
172
|
+
|
|
173
|
+
Each routes 'path' is a simple URL regex as per below examples.
|
|
174
|
+
|
|
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
|
+
```
|
|
219
|
+
|
|
220
|
+
### Service Registry
|
|
221
|
+
|
|
222
|
+
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
|
+
|
|
224
|
+
```
|
|
225
|
+
import { ServiceExample } from './ServiceExample';
|
|
226
|
+
|
|
227
|
+
import type { ServiceRegistry } from '@taujs/server';
|
|
228
|
+
|
|
229
|
+
export const serviceRegistry: ServiceRegistry = {
|
|
230
|
+
ServiceExample,
|
|
231
|
+
};
|
|
232
|
+
```
|
|
25
233
|
|
|
26
|
-
|
|
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
|
+
```
|
package/dist/SSRDataStore.d.ts
CHANGED
|
@@ -0,0 +1,18 @@
|
|
|
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 };
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
};
|
package/dist/SSRServer.d.ts
CHANGED
package/dist/SSRServer.js
CHANGED
|
@@ -244,7 +244,7 @@ var overrideCSSHMRConsoleError = () => {
|
|
|
244
244
|
// src/SSRServer.ts
|
|
245
245
|
var SSRServer = (0, import_fastify_plugin.default)(
|
|
246
246
|
async (app, opts) => {
|
|
247
|
-
const { clientRoot, clientHtmlTemplate, clientEntryClient, clientEntryServer, routes, serviceRegistry, isDebug } = opts;
|
|
247
|
+
const { alias, clientRoot, clientHtmlTemplate, clientEntryClient, clientEntryServer, routes, serviceRegistry, isDebug } = opts;
|
|
248
248
|
const templateHtmlPath = path.join(clientRoot, clientHtmlTemplate);
|
|
249
249
|
const templateHtml = !isDevelopment ? await fs.readFile(templateHtmlPath, "utf-8") : await fs.readFile(path.join(clientRoot, clientHtmlTemplate), "utf-8");
|
|
250
250
|
const ssrManifestPath = path.join(clientRoot, ".vite/ssr-manifest.json");
|
|
@@ -283,9 +283,12 @@ var SSRServer = (0, import_fastify_plugin.default)(
|
|
|
283
283
|
],
|
|
284
284
|
resolve: {
|
|
285
285
|
alias: {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
286
|
+
...{
|
|
287
|
+
"@client": path.resolve(clientRoot),
|
|
288
|
+
"@server": path.resolve(__dirname),
|
|
289
|
+
"@shared": path.resolve(__dirname, "../shared")
|
|
290
|
+
},
|
|
291
|
+
...alias
|
|
289
292
|
}
|
|
290
293
|
},
|
|
291
294
|
root: clientRoot,
|
package/package.json
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"type": "git",
|
|
21
21
|
"url": "git+https://github.com/aoede3/taujs-server.git"
|
|
22
22
|
},
|
|
23
|
-
"version": "0.0.
|
|
23
|
+
"version": "0.0.4",
|
|
24
24
|
"exports": {
|
|
25
25
|
".": {
|
|
26
26
|
"import": "./dist/SSRServer.js",
|
|
@@ -30,6 +30,10 @@
|
|
|
30
30
|
"import": "./dist/SSRDataStore.js",
|
|
31
31
|
"types": "./dist/SSRDataStore.d.ts"
|
|
32
32
|
},
|
|
33
|
+
"./render": {
|
|
34
|
+
"import": "./dist/SSRRender.js",
|
|
35
|
+
"types": "./dist/SSRRender.d.ts"
|
|
36
|
+
},
|
|
33
37
|
"./package.json": "./package.json"
|
|
34
38
|
},
|
|
35
39
|
"files": [
|
|
@@ -41,6 +45,9 @@
|
|
|
41
45
|
"*": {
|
|
42
46
|
"data-store": [
|
|
43
47
|
"./dist/SSRDataStore.d.ts"
|
|
48
|
+
],
|
|
49
|
+
"render": [
|
|
50
|
+
"./dist/SSRRender.d.ts"
|
|
44
51
|
]
|
|
45
52
|
}
|
|
46
53
|
},
|
|
@@ -55,8 +62,10 @@
|
|
|
55
62
|
"@changesets/cli": "^2.27.7",
|
|
56
63
|
"@types/node": "^20.14.9",
|
|
57
64
|
"@types/react": "^18.3.3",
|
|
65
|
+
"@types/react-dom": "^18.3.0",
|
|
58
66
|
"fastify": "^4.28.0",
|
|
59
67
|
"prettier": "^3.3.3",
|
|
68
|
+
"react-dom": "^18.3.1",
|
|
60
69
|
"tsup": "^8.2.4",
|
|
61
70
|
"vite": "^5.4.2",
|
|
62
71
|
"vitest": "^2.0.5"
|