@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 CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  `pnpm add @taujs/server`
8
8
 
9
- ## Streaming React SSR & Hydration
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
- import { ServiceExample } from './ServiceExample';
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
- streamRender: (serverResponse: ServerResponse, callbacks: RenderCallbacks, initialDataPromise: Promise<Record<string, unknown>>, bootstrapModules: string) => void;
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
- attributes?: RouteAttributes<Params>;
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/index.ts
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 (attributes, params, serviceRegistry) => {
215
- if (attributes && typeof attributes.fetch === "function") {
216
- return attributes.fetch(params, {
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 = await viteDevServer.transformIndexHtml(url, 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 { attributes } = route;
352
- const [beforeBody = "", afterBody] = template.split("<!--ssr-html-->");
353
- const [beforeHead, afterHead] = beforeBody.split("<!--ssr-head-->");
354
- const initialDataPromise = fetchInitialData(attributes, params, serviceRegistry);
355
- reply.raw.writeHead(200, { "Content-Type": "text/html" });
356
- reply.raw.write(beforeHead);
357
- streamRender(
358
- reply.raw,
359
- {
360
- onHead: (headContent) => {
361
- let fullHeadContent = headContent;
362
- if (ssrManifest) fullHeadContent += preloadLinks;
363
- if (manifest) fullHeadContent += cssLinks;
364
- reply.raw.write(`${fullHeadContent}${afterHead}`);
365
- },
366
- onFinish: async (initialDataResolved) => {
367
- reply.raw.write(`<script>window.__INITIAL_DATA__ = ${JSON.stringify(initialDataResolved).replace(/</g, "\\u003c")}</script>`);
368
- reply.raw.write(afterBody);
369
- reply.raw.end();
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
- onError: (error) => {
372
- console.error("Critical rendering onError:", error);
373
- reply.raw.end("Internal Server Error");
374
- }
375
- },
376
- initialDataPromise,
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("<!--ssr-head-->", "").replace("<!--ssr-html-->", "");
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
- "license": "MIT",
18
- "name": "@taujs/server",
19
- "repository": {
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/SSRServer.js",
27
- "types": "./dist/SSRServer.d.ts"
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
- "./render": {
34
- "import": "./dist/SSRRender.js",
35
- "types": "./dist/SSRRender.d.ts"
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": "^7.0.4",
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": "^4.28.0",
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
- "ci": "npm run build && npm run check-format && npm run check-exports && npm run lint",
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 .",
@@ -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 };
@@ -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
- };
@@ -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
- };