@taujs/server 0.0.8 → 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 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
 
package/dist/data.d.ts CHANGED
@@ -7,11 +7,11 @@ type SSRStore<T> = {
7
7
  setData: (newData: T) => void;
8
8
  subscribe: (callback: () => void) => () => void;
9
9
  };
10
- declare const createSSRStore: <T>(initialDataPromise: Promise<T>) => SSRStore<T>;
10
+ declare const createSSRStore: <T>(initialDataOrPromise: T | Promise<T>) => SSRStore<T>;
11
11
  declare const SSRStoreProvider: React.FC<React.PropsWithChildren<{
12
12
  store: SSRStore<Record<string, unknown>>;
13
13
  }>>;
14
- declare const useSSRStore: <T>() => SSRStore<T>;
14
+ declare const useSSRStore: <T>() => T;
15
15
 
16
16
  type HydrateAppOptions = {
17
17
  appComponent: React.ReactElement;
@@ -21,17 +21,29 @@ type HydrateAppOptions = {
21
21
  };
22
22
  declare const hydrateApp: ({ appComponent, initialDataKey, rootElementId, debug }: HydrateAppOptions) => void;
23
23
 
24
- type StreamRenderOptions = {
25
- appComponent: React.ReactElement;
26
- initialDataPromise: Promise<Record<string, unknown>>;
27
- bootstrapModules: string;
28
- headContent: string;
24
+ type RendererOptions = {
25
+ appComponent: (props: {
26
+ location: string;
27
+ }) => React.ReactElement;
28
+ headContent: string | ((data: Record<string, unknown>) => string);
29
29
  };
30
30
  type RenderCallbacks = {
31
31
  onHead: (headContent: string) => void;
32
32
  onFinish: (initialDataResolved: unknown) => void;
33
33
  onError: (error: unknown) => void;
34
34
  };
35
- declare const createStreamRenderer: (serverResponse: ServerResponse, { onHead, onFinish, onError }: RenderCallbacks, { appComponent, initialDataPromise, bootstrapModules, headContent }: StreamRenderOptions) => void;
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;
36
48
 
37
- export { type SSRStore, SSRStoreProvider, createSSRStore, createStreamRenderer, hydrateApp, useSSRStore };
49
+ export { type SSRStore, SSRStoreProvider, createRenderStream, createRenderer, createSSRStore, hydrateApp, resolveHeadContent, useSSRStore };
package/dist/data.js CHANGED
@@ -1,26 +1,31 @@
1
1
  // src/SSRDataStore.tsx
2
2
  import { createContext, useContext, useSyncExternalStore } from "react";
3
3
  import { jsx } from "react/jsx-runtime";
4
- var createSSRStore = (initialDataPromise) => {
4
+ var createSSRStore = (initialDataOrPromise) => {
5
5
  let currentData;
6
- let status = "pending";
6
+ let status;
7
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;
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;
12
22
  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
- });
23
+ serverDataPromise = Promise.resolve();
24
+ }
19
25
  const setData = (newData) => {
20
26
  currentData = newData;
21
27
  status = "success";
22
28
  subscribers.forEach((callback) => callback());
23
- if (resolvePromise) resolvePromise();
24
29
  };
25
30
  const subscribe = (callback) => {
26
31
  subscribers.add(callback);
@@ -28,7 +33,7 @@ var createSSRStore = (initialDataPromise) => {
28
33
  };
29
34
  const getSnapshot = () => {
30
35
  if (status === "pending") {
31
- throw initialDataPromise;
36
+ throw serverDataPromise;
32
37
  } else if (status === "error") {
33
38
  throw new Error("An error occurred while fetching the data.");
34
39
  }
@@ -105,15 +110,45 @@ var hydrateApp = ({ appComponent, initialDataKey = "__INITIAL_DATA__", rootEleme
105
110
  // src/SSRRender.tsx
106
111
  import { Writable } from "node:stream";
107
112
  import "react";
108
- import { renderToPipeableStream } from "react-dom/server";
113
+ import { renderToPipeableStream, renderToString } from "react-dom/server";
109
114
  import { jsx as jsx3 } from "react/jsx-runtime";
110
- var createStreamRenderer = (serverResponse, { onHead, onFinish, onError }, { appComponent, initialDataPromise, bootstrapModules, headContent }) => {
111
- const store = createSSRStore(initialDataPromise);
112
- const appElement = /* @__PURE__ */ jsx3(SSRStoreProvider, { store, children: appComponent });
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 }) });
113
147
  const { pipe } = renderToPipeableStream(appElement, {
114
- bootstrapModules: [bootstrapModules],
148
+ bootstrapModules: bootstrapModules ? [bootstrapModules] : void 0,
115
149
  onShellReady() {
116
- onHead(headContent);
150
+ const dynamicHeadContent = resolveHeadContent(headContent, initialDataResolved);
151
+ onHead(dynamicHeadContent);
117
152
  pipe(
118
153
  new Writable({
119
154
  write(chunk, _encoding, callback) {
@@ -135,8 +170,10 @@ var createStreamRenderer = (serverResponse, { onHead, onFinish, onError }, { app
135
170
  };
136
171
  export {
137
172
  SSRStoreProvider,
173
+ createRenderStream,
174
+ createRenderer,
138
175
  createSSRStore,
139
- createStreamRenderer,
140
176
  hydrateApp,
177
+ resolveHeadContent,
141
178
  useSSRStore
142
179
  };
package/dist/index.d.ts CHANGED
@@ -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,9 +45,15 @@ type Manifest = {
40
45
  assets?: string[];
41
46
  };
42
47
  };
43
- type StreamRender = (serverResponse: ServerResponse, callbacks: RenderCallbacks, initialDataPromise: Promise<Record<string, unknown>>, bootstrapModules: string) => void;
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;
44
54
  type RenderModule = {
45
- streamRender: StreamRender;
55
+ renderSSR: RenderSSR;
56
+ renderStream: RenderStream;
46
57
  };
47
58
  type RouteAttributes<Params = {}> = {
48
59
  fetch: (params?: Params, options?: RequestInit & {
@@ -55,9 +66,11 @@ type RouteAttributes<Params = {}> = {
55
66
  serviceMethod?: string;
56
67
  url?: string;
57
68
  }>;
69
+ meta?: Record<string, unknown>;
70
+ render?: typeof RENDERTYPE.ssr | typeof RENDERTYPE.streaming;
58
71
  };
59
72
  type Route<Params = {}> = {
60
- attributes?: RouteAttributes<Params>;
73
+ attr?: RouteAttributes<Params>;
61
74
  path: string;
62
75
  };
63
76
  interface InitialRouteParams extends Record<string, unknown> {
@@ -67,4 +80,4 @@ interface InitialRouteParams extends Record<string, unknown> {
67
80
  type RouteParams = InitialRouteParams & Record<string, unknown>;
68
81
  type RoutePathsAndAttributes<Params = {}> = Omit<Route<Params>, 'element'>;
69
82
 
70
- export { type FetchConfig, type InitialRouteParams, type Manifest, type RenderCallbacks, type RenderModule, type Route, type RouteAttributes, type RouteParams, type RoutePathsAndAttributes, SSRServer, type SSRServerOptions, type ServiceRegistry, type StreamRender };
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 };
package/dist/index.js CHANGED
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "@taujs/server",
3
- "version": "0.0.8",
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",
@@ -52,7 +52,7 @@
52
52
  "@types/react": "^18.3.3",
53
53
  "@types/react-dom": "^18.3.0",
54
54
  "@vitest/coverage-v8": "^2.1.0",
55
- "fastify": "^4.28.0",
55
+ "fastify": "^4.28.1",
56
56
  "jsdom": "^25.0.0",
57
57
  "prettier": "^3.3.3",
58
58
  "react-dom": "^18.3.1",
@@ -62,7 +62,7 @@
62
62
  "vitest": "^2.0.5"
63
63
  },
64
64
  "peerDependencies": {
65
- "fastify": "^5.0.0",
65
+ "fastify": "^4.28.1",
66
66
  "react": "^18.3.1",
67
67
  "react-dom": "^18.3.1",
68
68
  "typescript": "^5.5.4",
@@ -70,9 +70,11 @@
70
70
  },
71
71
  "scripts": {
72
72
  "build": "tsup",
73
+ "build-local": "tsup && ./move.sh",
73
74
  "ci": "npm run build && npm run check-format && npm run lint",
74
75
  "lint": "tsc",
75
76
  "test": "vitest run",
77
+ "test:ui": "vitest --ui --coverage.enabled=true",
76
78
  "coverage": "vitest run --coverage",
77
79
  "format": "prettier --write .",
78
80
  "check-format": "prettier --check .",