@plitzi/sdk-server 0.30.19

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.
Files changed (93) hide show
  1. package/CHANGELOG.md +192 -0
  2. package/README.md +870 -0
  3. package/dist/.gitkeep +0 -0
  4. package/dist/adapters/jsonAdapters.js +46 -0
  5. package/dist/core/createServer.js +47 -0
  6. package/dist/core/mimeTypes.js +41 -0
  7. package/dist/core/requestHandler.js +108 -0
  8. package/dist/core/requestParser.js +34 -0
  9. package/dist/core/staticFiles.js +42 -0
  10. package/dist/core/transports.js +51 -0
  11. package/dist/helpers/buildResponseHelpers.js +43 -0
  12. package/dist/helpers/buildServerInfo.js +55 -0
  13. package/dist/helpers/cache/TtlCache.js +55 -0
  14. package/dist/helpers/cache/cacheManager.js +25 -0
  15. package/dist/helpers/cache/defaults.js +9 -0
  16. package/dist/helpers/cache/keys.js +6 -0
  17. package/dist/helpers/cache/serverCaches.js +15 -0
  18. package/dist/helpers/compress.js +17 -0
  19. package/dist/helpers/escapeJson.js +4 -0
  20. package/dist/helpers/metrics.js +36 -0
  21. package/dist/helpers/normalizePlugins.js +16 -0
  22. package/dist/helpers/runMiddlewares.js +21 -0
  23. package/dist/index.d.ts +2 -0
  24. package/dist/index.js +3 -0
  25. package/dist/middlewares/auth.js +10 -0
  26. package/dist/middlewares/basicAuth.js +67 -0
  27. package/dist/middlewares/spaceDeployment.js +16 -0
  28. package/dist/modules/mcp/handler.js +34 -0
  29. package/dist/modules/mcp/server.js +120 -0
  30. package/dist/modules/rsc/handler.js +69 -0
  31. package/dist/modules/ssr/Component.js +20 -0
  32. package/dist/modules/ssr/buildBody.js +20 -0
  33. package/dist/modules/ssr/handler.js +36 -0
  34. package/dist/modules/ssr/loadPluginComponents.js +45 -0
  35. package/dist/modules/ssr/prepareRender.js +64 -0
  36. package/dist/modules/ssr/registerExternalPlugins.js +54 -0
  37. package/dist/modules/ssr/streamBody.js +54 -0
  38. package/dist/modules/ssr/template.js +10 -0
  39. package/dist/modules/ssr/views/template.ejs +71 -0
  40. package/dist/plugins/compile.js +29 -0
  41. package/dist/plugins/copy.js +13 -0
  42. package/dist/plugins/detect.js +9 -0
  43. package/dist/plugins/manager.js +251 -0
  44. package/dist/public/.gitkeep +0 -0
  45. package/dist/server.d.ts +1 -0
  46. package/dist/server.js +82 -0
  47. package/dist/src/adapters/jsonAdapters.d.ts +7 -0
  48. package/dist/src/core/createServer.d.ts +2 -0
  49. package/dist/src/core/mimeTypes.d.ts +4 -0
  50. package/dist/src/core/requestHandler.d.ts +5 -0
  51. package/dist/src/core/requestParser.d.ts +3 -0
  52. package/dist/src/core/staticFiles.d.ts +2 -0
  53. package/dist/src/core/transports.d.ts +18 -0
  54. package/dist/src/helpers/buildResponseHelpers.d.ts +10 -0
  55. package/dist/src/helpers/buildServerInfo.d.ts +2 -0
  56. package/dist/src/helpers/cache/TtlCache.d.ts +15 -0
  57. package/dist/src/helpers/cache/cacheManager.d.ts +3 -0
  58. package/dist/src/helpers/cache/defaults.d.ts +1 -0
  59. package/dist/src/helpers/cache/index.d.ts +6 -0
  60. package/dist/src/helpers/cache/keys.d.ts +7 -0
  61. package/dist/src/helpers/cache/serverCaches.d.ts +9 -0
  62. package/dist/src/helpers/compress.d.ts +3 -0
  63. package/dist/src/helpers/escapeJson.d.ts +1 -0
  64. package/dist/src/helpers/metrics.d.ts +11 -0
  65. package/dist/src/helpers/normalizePlugins.d.ts +4 -0
  66. package/dist/src/helpers/runMiddlewares.d.ts +2 -0
  67. package/dist/src/index.d.ts +4 -0
  68. package/dist/src/middlewares/auth.d.ts +2 -0
  69. package/dist/src/middlewares/basicAuth.d.ts +6 -0
  70. package/dist/src/middlewares/spaceDeployment.d.ts +2 -0
  71. package/dist/src/modules/mcp/handler.d.ts +3 -0
  72. package/dist/src/modules/mcp/server.d.ts +3 -0
  73. package/dist/src/modules/rsc/handler.d.ts +17 -0
  74. package/dist/src/modules/ssr/Component.d.ts +11 -0
  75. package/dist/src/modules/ssr/buildBody.d.ts +5 -0
  76. package/dist/src/modules/ssr/handler.d.ts +4 -0
  77. package/dist/src/modules/ssr/loadPluginComponents.d.ts +13 -0
  78. package/dist/src/modules/ssr/prepareRender.d.ts +13 -0
  79. package/dist/src/modules/ssr/registerExternalPlugins.d.ts +7 -0
  80. package/dist/src/modules/ssr/streamBody.d.ts +5 -0
  81. package/dist/src/modules/ssr/template.d.ts +6 -0
  82. package/dist/src/plugins/compile.d.ts +3 -0
  83. package/dist/src/plugins/copy.d.ts +1 -0
  84. package/dist/src/plugins/detect.d.ts +3 -0
  85. package/dist/src/plugins/manager.d.ts +40 -0
  86. package/dist/src/standalone/alias-loader.d.mts +1 -0
  87. package/dist/src/standalone/plugins/ClientInfo.d.ts +2 -0
  88. package/dist/src/standalone/plugins/ServerInfo.d.ts +2 -0
  89. package/dist/src/standalone/plugins/SharedInfo.d.ts +2 -0
  90. package/dist/src/standalone/plugins/styles.d.ts +5 -0
  91. package/dist/src/standalone/register-alias.d.mts +1 -0
  92. package/dist/src/standalone/server.d.ts +1 -0
  93. package/package.json +60 -0
@@ -0,0 +1,21 @@
1
+ //#region src/helpers/runMiddlewares.ts
2
+ var runMiddlewares = async (middlewares, req, res) => {
3
+ let index = 0;
4
+ const state = { stopped: false };
5
+ const next = async () => {
6
+ if (state.stopped) return;
7
+ const mw = index < middlewares.length ? middlewares[index++] : void 0;
8
+ if (!mw) return;
9
+ const nextState = { called: false };
10
+ await mw(req, res, async () => {
11
+ nextState.called = true;
12
+ await next();
13
+ });
14
+ if (!nextState.called) state.stopped = true;
15
+ };
16
+ await next();
17
+ if (req.spaceDeployment) req.ctx.spaceDeployment = req.spaceDeployment;
18
+ return state.stopped;
19
+ };
20
+ //#endregion
21
+ export { runMiddlewares };
@@ -0,0 +1,2 @@
1
+ export * from './src/index'
2
+ export {}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ import { createSSRServer } from "./core/createServer.js";
2
+ import { createJsonAdapters } from "./adapters/jsonAdapters.js";
3
+ export { createJsonAdapters, createSSRServer };
@@ -0,0 +1,10 @@
1
+ //#region src/middlewares/auth.ts
2
+ var authMiddleware = (adapters) => {
3
+ return async (req, _res, next) => {
4
+ const user = await adapters.getUser?.(req);
5
+ req.ctx.user = user;
6
+ await next();
7
+ };
8
+ };
9
+ //#endregion
10
+ export { authMiddleware };
@@ -0,0 +1,67 @@
1
+ import { DEFAULT_TTL_MS } from "../helpers/cache/defaults.js";
2
+ import { TtlCache } from "../helpers/cache/TtlCache.js";
3
+ import crypto from "node:crypto";
4
+ //#region src/middlewares/basicAuth.ts
5
+ var sendChallenge = (res, domain, realm) => {
6
+ res.setHeader("WWW-Authenticate", `Basic realm="${realm} - ${domain}", charset="UTF-8"`);
7
+ res.setHeader("Cache-Control", "no-store");
8
+ res.setStatus(401);
9
+ res.end();
10
+ };
11
+ var basicAuthMiddleware = (options = {}) => {
12
+ const { realm = "Restricted Area", cacheTtlMs = DEFAULT_TTL_MS.auth } = options;
13
+ const authCache = new TtlCache(cacheTtlMs, 1e4);
14
+ return async (req, res, next) => {
15
+ const credential = req.ctx.spaceDeployment?.credential;
16
+ if (!credential || credential.provider !== "ssr") {
17
+ await next();
18
+ return;
19
+ }
20
+ const { data: credentials } = credential;
21
+ if (credentials.type !== "basic") {
22
+ await next();
23
+ return;
24
+ }
25
+ const hostname = req.hostname;
26
+ const authHeader = req.headers["authorization"];
27
+ if (!authHeader || !authHeader.startsWith("Basic ")) {
28
+ sendChallenge(res, hostname, realm);
29
+ return;
30
+ }
31
+ let token;
32
+ let decoded;
33
+ try {
34
+ token = authHeader.slice(6);
35
+ decoded = Buffer.from(token, "base64").toString("utf8");
36
+ } catch {
37
+ sendChallenge(res, hostname, realm);
38
+ return;
39
+ }
40
+ const cacheKey = `${hostname}:${crypto.createHash("sha256").update(token).digest("hex")}`;
41
+ if (authCache.get(cacheKey)) {
42
+ await next();
43
+ return;
44
+ }
45
+ const colonIdx = decoded.indexOf(":");
46
+ if (colonIdx === -1) {
47
+ sendChallenge(res, hostname, realm);
48
+ return;
49
+ }
50
+ const user = decoded.slice(0, colonIdx);
51
+ const pass = decoded.slice(colonIdx + 1);
52
+ const expectedUser = Buffer.from(credentials.user);
53
+ const expectedPass = Buffer.from(credentials.pass);
54
+ const actualUser = Buffer.from(user);
55
+ const actualPass = Buffer.from(pass);
56
+ const userMatch = actualUser.length === expectedUser.length && crypto.timingSafeEqual(actualUser, expectedUser);
57
+ const passMatch = actualPass.length === expectedPass.length && crypto.timingSafeEqual(actualPass, expectedPass);
58
+ if (!userMatch || !passMatch) {
59
+ sendChallenge(res, hostname, realm);
60
+ return;
61
+ }
62
+ authCache.set(cacheKey, true);
63
+ await next();
64
+ };
65
+ };
66
+ //#endregion
67
+ export { basicAuthMiddleware };
@@ -0,0 +1,16 @@
1
+ //#region src/middlewares/spaceDeployment.ts
2
+ var spaceDeploymentMiddleware = (adapters) => {
3
+ return async (req, res, next) => {
4
+ const deployment = await adapters.getSpaceDeployment(req);
5
+ req.ctx.spaceDeployment = deployment;
6
+ const { spaceId, error } = deployment;
7
+ if (spaceId == null || error) {
8
+ res.setStatus(error?.code ?? 404);
9
+ res.send(error?.message ?? "Space not found");
10
+ return;
11
+ }
12
+ await next();
13
+ };
14
+ };
15
+ //#endregion
16
+ export { spaceDeploymentMiddleware };
@@ -0,0 +1,34 @@
1
+ import { createMcpServer } from "./server.js";
2
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
3
+ //#region src/modules/mcp/handler.ts
4
+ var readBody = (req) => new Promise((resolve, reject) => {
5
+ let raw = "";
6
+ req.on("data", (chunk) => {
7
+ raw += chunk.toString();
8
+ });
9
+ req.on("end", () => {
10
+ try {
11
+ resolve(raw ? JSON.parse(raw) : void 0);
12
+ } catch {
13
+ reject(/* @__PURE__ */ new Error("MCP: invalid JSON body"));
14
+ }
15
+ });
16
+ req.on("error", reject);
17
+ });
18
+ var handleMcp = async (req, res, config) => {
19
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
20
+ await createMcpServer(config.adapters).connect(transport);
21
+ if (req.method === "POST") {
22
+ let body;
23
+ try {
24
+ body = await readBody(req);
25
+ } catch {
26
+ res.writeHead(400, { "Content-Type": "application/json" });
27
+ res.end(JSON.stringify({ error: "Invalid JSON body" }));
28
+ return;
29
+ }
30
+ await transport.handleRequest(req, res, body);
31
+ } else await transport.handleRequest(req, res);
32
+ };
33
+ //#endregion
34
+ export { handleMcp };
@@ -0,0 +1,120 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ //#region src/modules/mcp/server.ts
4
+ var ok = (data) => ({ content: [{
5
+ type: "text",
6
+ text: JSON.stringify(data, null, 2)
7
+ }] });
8
+ var err = (message) => ({
9
+ content: [{
10
+ type: "text",
11
+ text: message
12
+ }],
13
+ isError: true
14
+ });
15
+ var createMcpServer = (adapters) => {
16
+ const server = new McpServer({
17
+ name: "plitzi-schema-agent",
18
+ version: "1.0.0"
19
+ });
20
+ server.tool("list_spaces", "List all spaces available in the system", {}, async () => {
21
+ return ok(await adapters.listSpaces());
22
+ });
23
+ server.tool("get_schema", "Get the full element tree for a space and environment", {
24
+ spaceId: z.number().describe("Space ID"),
25
+ environment: z.string().describe("Environment name (e.g. main, production)")
26
+ }, async ({ spaceId, environment }) => {
27
+ const schema = await adapters.getSchema(spaceId, environment);
28
+ if (!schema) return err(`Schema not found for space ${spaceId} / ${environment}`);
29
+ return ok(schema);
30
+ });
31
+ server.tool("list_elements", "List all element IDs, types and labels for a space and environment", {
32
+ spaceId: z.number(),
33
+ environment: z.string()
34
+ }, async ({ spaceId, environment }) => {
35
+ const schema = await adapters.getSchema(spaceId, environment);
36
+ if (!schema) return err(`Schema not found for space ${spaceId} / ${environment}`);
37
+ return ok(Object.values(schema.elements).map(({ id, type, label, parentId, runtime }) => ({
38
+ id,
39
+ type,
40
+ label,
41
+ parentId,
42
+ runtime
43
+ })));
44
+ });
45
+ server.tool("get_element", "Get the full details of a single element by ID", {
46
+ spaceId: z.number(),
47
+ environment: z.string(),
48
+ elementId: z.string().describe("Element ID")
49
+ }, async ({ spaceId, environment, elementId }) => {
50
+ const schema = await adapters.getSchema(spaceId, environment);
51
+ if (!schema) return err(`Schema not found for space ${spaceId} / ${environment}`);
52
+ const element = schema.elements[elementId];
53
+ if (!element) return err(`Element ${elementId} not found`);
54
+ return ok(element);
55
+ });
56
+ server.tool("create_element", "Add a new element to the schema. Returns the created element with its generated ID.", {
57
+ spaceId: z.number(),
58
+ environment: z.string(),
59
+ type: z.string().describe("Component type (e.g. Container, Text, Button, Image)"),
60
+ label: z.string().describe("Human-readable name for the element"),
61
+ props: z.record(z.string(), z.unknown()).optional().describe("Component props/attributes"),
62
+ runtime: z.enum([
63
+ "server",
64
+ "client",
65
+ "shared"
66
+ ]).optional().describe("Rendering runtime"),
67
+ parentId: z.string().optional().describe("Parent element ID; omit to place at root"),
68
+ position: z.number().optional().describe("Zero-based insertion index within the parent")
69
+ }, async ({ spaceId, environment, type, label, props, runtime, parentId, position }) => {
70
+ return ok(await adapters.createElement(spaceId, environment, {
71
+ type,
72
+ label,
73
+ props,
74
+ runtime
75
+ }, parentId, position));
76
+ });
77
+ server.tool("update_element", "Update an existing element — label, props, styles, or runtime", {
78
+ spaceId: z.number(),
79
+ environment: z.string(),
80
+ elementId: z.string(),
81
+ label: z.string().optional(),
82
+ props: z.record(z.string(), z.unknown()).optional(),
83
+ styles: z.record(z.string(), z.unknown()).optional(),
84
+ runtime: z.enum([
85
+ "server",
86
+ "client",
87
+ "shared"
88
+ ]).optional()
89
+ }, async ({ spaceId, environment, elementId, label, props, styles, runtime }) => {
90
+ return ok(await adapters.updateElement(spaceId, environment, elementId, {
91
+ label,
92
+ props,
93
+ styles,
94
+ runtime
95
+ }));
96
+ });
97
+ server.tool("delete_element", "Remove an element and all its descendants from the schema", {
98
+ spaceId: z.number(),
99
+ environment: z.string(),
100
+ elementId: z.string()
101
+ }, async ({ spaceId, environment, elementId }) => {
102
+ await adapters.deleteElement(spaceId, environment, elementId);
103
+ return ok({ deleted: elementId });
104
+ });
105
+ server.tool("publish_schema", "Publish the current draft schema as a new immutable revision", {
106
+ spaceId: z.number(),
107
+ environment: z.string()
108
+ }, async ({ spaceId, environment }) => {
109
+ return ok(await adapters.publishSchema(spaceId, environment));
110
+ });
111
+ if (adapters.listPlugins) {
112
+ const listPlugins = adapters.listPlugins;
113
+ server.tool("list_plugins", "List all plugins registered in the system", {}, async () => {
114
+ return ok(await listPlugins());
115
+ });
116
+ }
117
+ return server;
118
+ };
119
+ //#endregion
120
+ export { createMcpServer };
@@ -0,0 +1,69 @@
1
+ import { DEFAULT_TTL_MS } from "../../helpers/cache/defaults.js";
2
+ import { buildRscCacheKey } from "../../helpers/cache/keys.js";
3
+ //#region src/modules/rsc/handler.ts
4
+ /**
5
+ * Handles GET /_rsc requests.
6
+ *
7
+ * Calls adapters.getRscData to get server-side data for elements marked
8
+ * runtime:'server' in the schema, then returns a JSON payload. The SDK
9
+ * client uses this payload to update server-driven portions of the page
10
+ * without a full navigation.
11
+ *
12
+ * Responses are cached server-side (TtlCache) and via Cache-Control headers:
13
+ * - main environment: no-store (development, always fresh)
14
+ * - Authenticated requests: Cache-Control: private, max-age=<ttl>
15
+ * - Unauthenticated requests: Cache-Control: public, max-age=<ttl>
16
+ */
17
+ var handleRsc = async (req, res, config, _pluginManager, cache) => {
18
+ if (!config.adapters.getRscData) {
19
+ res.setStatus(501);
20
+ res.send(JSON.stringify({ error: "getRscData adapter not configured" }));
21
+ return;
22
+ }
23
+ const { environment = "main", spaceId, revision = 0 } = req.ctx.spaceDeployment ?? {};
24
+ if (typeof spaceId !== "number") {
25
+ res.setStatus(400);
26
+ res.send(JSON.stringify({ error: "Invalid space deployment" }));
27
+ return;
28
+ }
29
+ const idsRaw = req.query.ids;
30
+ const ids = idsRaw ? idsRaw.split(",").filter(Boolean).slice(0, 50).map((id) => id.slice(0, 128)) : void 0;
31
+ const idsParam = ids?.join(",");
32
+ const ttlMs = config.rsc?.cacheTtlMs ?? DEFAULT_TTL_MS.rsc;
33
+ const isAuthenticated = !!req.ctx.user;
34
+ const cacheControl = environment === "main" ? "no-store" : isAuthenticated ? `private, max-age=${Math.floor(ttlMs / 1e3)}` : `public, max-age=${Math.floor(ttlMs / 1e3)}`;
35
+ const cacheKey = environment !== "main" ? buildRscCacheKey(spaceId, environment, revision, req.ctx.user?.id, idsParam) : void 0;
36
+ const cached = cacheKey ? cache?.get(cacheKey) : void 0;
37
+ if (cached) {
38
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
39
+ res.setHeader("Cache-Control", cacheControl);
40
+ res.setHeader("X-Cache", "HIT");
41
+ res.send(cached);
42
+ return;
43
+ }
44
+ let rscData;
45
+ try {
46
+ rscData = await config.adapters.getRscData(req, spaceId, environment, revision, req.ctx.user, ids);
47
+ } catch (err) {
48
+ console.error("[RSC] getRscData error:", err);
49
+ res.setStatus(500);
50
+ res.send(JSON.stringify({ error: "RSC data fetch failed" }));
51
+ return;
52
+ }
53
+ const payload = {
54
+ version: 1,
55
+ transport: "json",
56
+ spaceId,
57
+ environment,
58
+ revision,
59
+ ...rscData
60
+ };
61
+ const payloadStr = JSON.stringify(payload);
62
+ if (cacheKey) cache?.set(cacheKey, payloadStr);
63
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
64
+ res.setHeader("Cache-Control", cacheControl);
65
+ res.setHeader("X-Cache", "MISS");
66
+ res.send(payloadStr);
67
+ };
68
+ //#endregion
69
+ export { handleRsc };
@@ -0,0 +1,20 @@
1
+ import PlitziSdk from "@plitzi/plitzi-sdk";
2
+ import { jsx } from "react/jsx-runtime";
3
+ //#region src/modules/ssr/Component.tsx
4
+ var Component = ({ server, renderMode = "raw", previewMode = true, offlineData, environment = "main", plugins }) => {
5
+ return /* @__PURE__ */ jsx(PlitziSdk, {
6
+ environment,
7
+ server,
8
+ previewMode,
9
+ renderMode,
10
+ offlineMode: !!offlineData && Object.keys(offlineData).length > 0,
11
+ offlineData,
12
+ children: plugins && Object.keys(plugins).map((key) => /* @__PURE__ */ jsx(PlitziSdk.Plugin, {
13
+ renderType: key,
14
+ component: plugins[key].component,
15
+ ...plugins[key].props
16
+ }, key))
17
+ });
18
+ };
19
+ //#endregion
20
+ export { Component as default };
@@ -0,0 +1,20 @@
1
+ import Component from "./Component.js";
2
+ import { prepareRender } from "./prepareRender.js";
3
+ import { renderToString } from "react-dom/server";
4
+ import { jsx } from "react/jsx-runtime";
5
+ //#region src/modules/ssr/buildBody.tsx
6
+ var buildBody = async (req, config, spaceId, environment, revision, renderFn, pluginManager, offlineDataCache, metrics) => {
7
+ const prep = await prepareRender(req, config, spaceId, environment, revision, pluginManager, offlineDataCache, metrics);
8
+ const reactStart = metrics ? performance.now() : 0;
9
+ const html = renderToString(/* @__PURE__ */ jsx(Component, { ...prep.componentProps })).trim();
10
+ metrics?.record("react", Math.round(performance.now() - reactStart));
11
+ const templateStart = metrics ? performance.now() : 0;
12
+ const body = renderFn({
13
+ ...prep.templateParams,
14
+ html
15
+ });
16
+ metrics?.record("template", Math.round(performance.now() - templateStart));
17
+ return body;
18
+ };
19
+ //#endregion
20
+ export { buildBody };
@@ -0,0 +1,36 @@
1
+ import { buildHtmlCacheKey } from "../../helpers/cache/keys.js";
2
+ import { buildBody } from "./buildBody.js";
3
+ import { streamBody } from "./streamBody.js";
4
+ import { RequestMetrics } from "../../helpers/metrics.js";
5
+ //#region src/modules/ssr/handler.tsx
6
+ var renderSSR = async (req, res, config, renderFn, pluginManager, caches) => {
7
+ const { environment = "main", spaceId = 1, revision = 0 } = req.ctx.spaceDeployment || {};
8
+ const devMode = config.devMode ?? false;
9
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
10
+ const htmlCache = environment !== "main" ? caches.html : void 0;
11
+ const cacheKey = htmlCache ? buildHtmlCacheKey(req.ctx.user?.token, spaceId, environment, revision, req) : void 0;
12
+ if (htmlCache && cacheKey) {
13
+ const cached = htmlCache.get(cacheKey);
14
+ if (cached) {
15
+ res.setHeader("X-Cache", "HIT");
16
+ if (devMode) res.setHeader("Server-Timing", "html;desc=\"cache-hit\";dur=0");
17
+ res.send(cached);
18
+ return;
19
+ }
20
+ }
21
+ const metrics = devMode ? new RequestMetrics() : void 0;
22
+ if (cacheKey) res.setHeader("X-Cache", "MISS");
23
+ if (config.streaming) {
24
+ await streamBody(req, res, config, spaceId, environment, revision, renderFn, pluginManager, caches.offlineData, htmlCache, cacheKey, metrics);
25
+ return;
26
+ }
27
+ const body = await buildBody(req, config, spaceId, environment, revision, renderFn, pluginManager, caches.offlineData, metrics);
28
+ if (htmlCache && cacheKey) htmlCache.set(cacheKey, body);
29
+ if (metrics) {
30
+ res.setHeader("Server-Timing", metrics.toServerTimingHeader());
31
+ metrics.log(`${req.method} ${req.path}`);
32
+ }
33
+ res.send(body);
34
+ };
35
+ //#endregion
36
+ export { renderSSR };
@@ -0,0 +1,45 @@
1
+ //#region src/modules/ssr/loadPluginComponents.ts
2
+ /** Module-level cache: absolute filePath → loaded React component. */
3
+ var componentCache = /* @__PURE__ */ new Map();
4
+ /**
5
+ * Plugins that failed to import (e.g. browser-only code like `document`).
6
+ * These are permanently skipped on the server and fall through to client-side rendering.
7
+ */
8
+ var failedImports = /* @__PURE__ */ new Set();
9
+ /**
10
+ * Dynamically imports plugin components from their compiled filesystem paths and returns
11
+ * a Record<keyName, FC> suitable for passing to <PlitziSdk.Plugin>.
12
+ *
13
+ * File-source plugins are cached by filePath so subsequent requests skip the import().
14
+ * Plugins that fail to import are added to a permanent skip-list — they are not retried
15
+ * and will be rendered client-side instead.
16
+ * Component-source plugins (already in memory via getComponents()) are merged in directly.
17
+ */
18
+ var loadPluginComponents = async (entries, inlineComponents) => {
19
+ const result = {};
20
+ if (inlineComponents) for (const [key, component] of Object.entries(inlineComponents)) result[key] = {
21
+ component,
22
+ props: {}
23
+ };
24
+ await Promise.all(entries.filter((e) => e.filePath).map(async (e) => {
25
+ const filePath = e.filePath;
26
+ if (failedImports.has(filePath)) return;
27
+ let component = componentCache.get(filePath);
28
+ if (!component) try {
29
+ const mod = await import(filePath);
30
+ component = mod.default ?? mod;
31
+ componentCache.set(filePath, component);
32
+ } catch (err) {
33
+ console.warn(`[SSR] Plugin "${e.keyName}" cannot be imported server-side, falling back to client rendering:`, err.message);
34
+ failedImports.add(filePath);
35
+ return;
36
+ }
37
+ result[e.keyName] = {
38
+ component,
39
+ props: e.props
40
+ };
41
+ }));
42
+ return result;
43
+ };
44
+ //#endregion
45
+ export { loadPluginComponents };
@@ -0,0 +1,64 @@
1
+ import { buildOfflineDataCacheKey } from "../../helpers/cache/keys.js";
2
+ import { loadPluginComponents } from "./loadPluginComponents.js";
3
+ import { registerExternalPlugins } from "./registerExternalPlugins.js";
4
+ import { buildServerInfo } from "../../helpers/buildServerInfo.js";
5
+ import { escapeJson } from "../../helpers/escapeJson.js";
6
+ //#region src/modules/ssr/prepareRender.tsx
7
+ var prepareRender = async (req, config, spaceId, environment, revision, pluginManager, offlineDataCache, metrics) => {
8
+ const m = (name, fn) => metrics ? metrics.measure(name, fn) : Promise.resolve(fn());
9
+ const offlineCacheKey = environment !== "main" ? buildOfflineDataCacheKey(spaceId, environment, revision) : void 0;
10
+ const cachedOfflineStr = offlineCacheKey ? offlineDataCache?.get(offlineCacheKey) : void 0;
11
+ const [offlineData, server] = await Promise.all([cachedOfflineStr ? JSON.parse(cachedOfflineStr) : m("schema", () => config.adapters.getOfflineData(spaceId, environment, revision)), m("rsc", () => buildServerInfo(req, config))]);
12
+ if (!cachedOfflineStr && offlineCacheKey && offlineData !== void 0) offlineDataCache?.set(offlineCacheKey, JSON.stringify(offlineData));
13
+ const offlineDataStr = escapeJson(JSON.stringify({
14
+ offlineData,
15
+ offlineMode: true,
16
+ environment,
17
+ renderMode: "raw",
18
+ server
19
+ }));
20
+ const pluginNames = req.ctx.spaceDeployment?.pluginNames ?? [];
21
+ const pluginSources = req.ctx.spaceDeployment?.pluginSources;
22
+ const pluginBaseNames = new Set(pluginNames.map((n) => n.replace(/@[^@]*$/, "")));
23
+ const dynamicNames = [];
24
+ if (pluginSources) for (const [pluginName, pluginSource] of Object.entries(pluginSources)) {
25
+ const key = pluginManager.ensure(pluginName, pluginSource);
26
+ if (!pluginBaseNames.has(pluginName)) dynamicNames.push(key);
27
+ }
28
+ const externalNamesFiltered = (config.autoLoadSchemaPlugins !== false ? await m("extPlugins", () => registerExternalPlugins(pluginManager, offlineData)) : []).filter((k) => !pluginBaseNames.has(k.replace(/@[^@]*$/, "")));
29
+ const allPluginNames = [
30
+ ...pluginNames,
31
+ ...dynamicNames,
32
+ ...externalNamesFiltered
33
+ ];
34
+ const entries = allPluginNames.length > 0 ? await pluginManager.getEntries(allPluginNames) : [];
35
+ const pluginComponents = await m("plugins", () => loadPluginComponents(entries, pluginManager.getComponents()));
36
+ const templatePlugins = entries.length > 0 ? entries : req.ctx.spaceDeployment?.templateProps?.plugins;
37
+ const v = config.assetVersion ? `?v=${config.assetVersion}` : "";
38
+ const vendorJs = (config.devMode ? "/sdk-assets/plitzi-sdk-dev-vendor.js" : "/sdk-assets/plitzi-sdk-vendor.js") + v;
39
+ return {
40
+ componentProps: {
41
+ plugins: Object.keys(pluginComponents).length > 0 ? pluginComponents : void 0,
42
+ offlineData,
43
+ server,
44
+ environment: req.ctx.spaceDeployment?.environment ?? environment
45
+ },
46
+ entries,
47
+ templateParams: {
48
+ title: "Plitzi App",
49
+ jsPath: `/sdk-assets/plitzi-sdk.js${v}`,
50
+ cssPath: `/sdk-assets/plitzi-sdk.css${v}`,
51
+ react: vendorJs,
52
+ reactJsx: vendorJs,
53
+ reactDom: vendorJs,
54
+ reactDomClient: vendorJs,
55
+ ...req.ctx.spaceDeployment?.templateProps,
56
+ plugins: templatePlugins,
57
+ debugMode: config.devMode,
58
+ ssrOnly: config.ssrOnly === true,
59
+ offlineData: offlineDataStr
60
+ }
61
+ };
62
+ };
63
+ //#endregion
64
+ export { prepareRender };
@@ -0,0 +1,54 @@
1
+ //#region src/modules/ssr/registerExternalPlugins.ts
2
+ var fetchManifest = async (resource) => {
3
+ try {
4
+ const url = `${resource}/plugin-manifest.json`;
5
+ const res = await fetch(url);
6
+ if (!res.ok) {
7
+ console.warn(`[SSR] Failed to fetch plugin manifest from ${url}: HTTP ${res.status}`);
8
+ return null;
9
+ }
10
+ return await res.json();
11
+ } catch (err) {
12
+ console.warn(`[SSR] Error fetching plugin manifest from ${resource}:`, err);
13
+ return null;
14
+ }
15
+ };
16
+ var resolveAssetUrl = (resource, asset) => {
17
+ if (asset.url) return asset.url;
18
+ if (asset.src) return `${resource}/${asset.src}`;
19
+ return null;
20
+ };
21
+ var findAsset = (manifest, type, resource) => {
22
+ const assets = Object.values(manifest.assets);
23
+ const main = assets.find((a) => a.type === type && a.isMain) ?? assets.find((a) => a.type === type);
24
+ return main ? resolveAssetUrl(resource, main) ?? void 0 : void 0;
25
+ };
26
+ var isAbsoluteUrl = (url) => url.startsWith("http://") || url.startsWith("https://");
27
+ var registerPlugin = async (pluginManager, plugin) => {
28
+ if (!plugin.resource || !isAbsoluteUrl(plugin.resource)) return null;
29
+ const manifest = await fetchManifest(plugin.resource);
30
+ if (!manifest) return null;
31
+ const jsUrl = findAsset(manifest, "script", plugin.resource);
32
+ if (!jsUrl) {
33
+ console.warn(`[SSR] Plugin "${plugin.type}" has no JS asset in manifest, skipping`);
34
+ return null;
35
+ }
36
+ const source = {
37
+ js: jsUrl,
38
+ css: findAsset(manifest, "style", plugin.resource),
39
+ action: "download",
40
+ version: manifest.version
41
+ };
42
+ return pluginManager.ensure(plugin.type, source);
43
+ };
44
+ /**
45
+ * Reads plugins listed in offlineData.plugins, fetches their manifests, downloads and caches
46
+ * JS/CSS via the PluginManager, and returns the effective plugin keys to pass to getEntries().
47
+ */
48
+ var registerExternalPlugins = async (pluginManager, offlineData) => {
49
+ const plugins = offlineData?.plugins;
50
+ if (!plugins || plugins.length === 0) return [];
51
+ return (await Promise.all(plugins.map((p) => registerPlugin(pluginManager, p)))).filter((k) => k !== null);
52
+ };
53
+ //#endregion
54
+ export { registerExternalPlugins };
@@ -0,0 +1,54 @@
1
+ import Component from "./Component.js";
2
+ import { prepareRender } from "./prepareRender.js";
3
+ import { renderToPipeableStream } from "react-dom/server";
4
+ import { jsx } from "react/jsx-runtime";
5
+ import { Writable } from "node:stream";
6
+ //#region src/modules/ssr/streamBody.tsx
7
+ var SSR_SENTINEL = "<!--SSR_CONTENT-->";
8
+ var streamBody = async (req, res, config, spaceId, environment, revision, renderFn, pluginManager, offlineDataCache, htmlCache, cacheKey, metrics) => {
9
+ const prep = await prepareRender(req, config, spaceId, environment, revision, pluginManager, offlineDataCache, metrics);
10
+ const templateStart = metrics ? performance.now() : 0;
11
+ const fullTemplate = renderFn({
12
+ ...prep.templateParams,
13
+ html: SSR_SENTINEL
14
+ });
15
+ metrics?.record("template", Math.round(performance.now() - templateStart));
16
+ const sentinelIdx = fullTemplate.indexOf(SSR_SENTINEL);
17
+ const head = sentinelIdx >= 0 ? fullTemplate.slice(0, sentinelIdx) : fullTemplate;
18
+ const tail = sentinelIdx >= 0 ? fullTemplate.slice(sentinelIdx + 18) : "";
19
+ await new Promise((resolve, reject) => {
20
+ const chunks = cacheKey && htmlCache ? [] : null;
21
+ const writable = new Writable({
22
+ write(chunk, _enc, cb) {
23
+ chunks?.push(chunk);
24
+ res.write(chunk);
25
+ cb();
26
+ },
27
+ final(cb) {
28
+ const tailBuf = Buffer.from(tail, "utf-8");
29
+ res.write(tailBuf);
30
+ res.end();
31
+ if (chunks && cacheKey && htmlCache) htmlCache.set(cacheKey, head + Buffer.concat(chunks).toString("utf-8") + tail);
32
+ resolve();
33
+ cb();
34
+ }
35
+ });
36
+ const reactStart = metrics ? performance.now() : 0;
37
+ const { pipe } = renderToPipeableStream(/* @__PURE__ */ jsx(Component, { ...prep.componentProps }), {
38
+ onShellReady() {
39
+ metrics?.record("react", Math.round(performance.now() - reactStart));
40
+ if (metrics) {
41
+ res.setHeader("Server-Timing", metrics.toServerTimingHeader());
42
+ metrics.log(`${req.method} ${req.path}`);
43
+ }
44
+ res.write(Buffer.from(head, "utf-8"));
45
+ pipe(writable);
46
+ },
47
+ onError(err) {
48
+ reject(err instanceof Error ? err : new Error(String(err)));
49
+ }
50
+ });
51
+ });
52
+ };
53
+ //#endregion
54
+ export { streamBody };
@@ -0,0 +1,10 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import fs from "node:fs";
4
+ import ejs from "ejs";
5
+ //#region src/modules/ssr/template.ts
6
+ var __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ var DEFAULT_TEMPLATE_PATH = path.resolve(__dirname, "views/template.ejs");
8
+ var compileTemplate = () => ejs.compile(fs.readFileSync(DEFAULT_TEMPLATE_PATH, "utf-8"), { filename: DEFAULT_TEMPLATE_PATH });
9
+ //#endregion
10
+ export { compileTemplate };