@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.
- package/CHANGELOG.md +192 -0
- package/README.md +870 -0
- package/dist/.gitkeep +0 -0
- package/dist/adapters/jsonAdapters.js +46 -0
- package/dist/core/createServer.js +47 -0
- package/dist/core/mimeTypes.js +41 -0
- package/dist/core/requestHandler.js +108 -0
- package/dist/core/requestParser.js +34 -0
- package/dist/core/staticFiles.js +42 -0
- package/dist/core/transports.js +51 -0
- package/dist/helpers/buildResponseHelpers.js +43 -0
- package/dist/helpers/buildServerInfo.js +55 -0
- package/dist/helpers/cache/TtlCache.js +55 -0
- package/dist/helpers/cache/cacheManager.js +25 -0
- package/dist/helpers/cache/defaults.js +9 -0
- package/dist/helpers/cache/keys.js +6 -0
- package/dist/helpers/cache/serverCaches.js +15 -0
- package/dist/helpers/compress.js +17 -0
- package/dist/helpers/escapeJson.js +4 -0
- package/dist/helpers/metrics.js +36 -0
- package/dist/helpers/normalizePlugins.js +16 -0
- package/dist/helpers/runMiddlewares.js +21 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/middlewares/auth.js +10 -0
- package/dist/middlewares/basicAuth.js +67 -0
- package/dist/middlewares/spaceDeployment.js +16 -0
- package/dist/modules/mcp/handler.js +34 -0
- package/dist/modules/mcp/server.js +120 -0
- package/dist/modules/rsc/handler.js +69 -0
- package/dist/modules/ssr/Component.js +20 -0
- package/dist/modules/ssr/buildBody.js +20 -0
- package/dist/modules/ssr/handler.js +36 -0
- package/dist/modules/ssr/loadPluginComponents.js +45 -0
- package/dist/modules/ssr/prepareRender.js +64 -0
- package/dist/modules/ssr/registerExternalPlugins.js +54 -0
- package/dist/modules/ssr/streamBody.js +54 -0
- package/dist/modules/ssr/template.js +10 -0
- package/dist/modules/ssr/views/template.ejs +71 -0
- package/dist/plugins/compile.js +29 -0
- package/dist/plugins/copy.js +13 -0
- package/dist/plugins/detect.js +9 -0
- package/dist/plugins/manager.js +251 -0
- package/dist/public/.gitkeep +0 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +82 -0
- package/dist/src/adapters/jsonAdapters.d.ts +7 -0
- package/dist/src/core/createServer.d.ts +2 -0
- package/dist/src/core/mimeTypes.d.ts +4 -0
- package/dist/src/core/requestHandler.d.ts +5 -0
- package/dist/src/core/requestParser.d.ts +3 -0
- package/dist/src/core/staticFiles.d.ts +2 -0
- package/dist/src/core/transports.d.ts +18 -0
- package/dist/src/helpers/buildResponseHelpers.d.ts +10 -0
- package/dist/src/helpers/buildServerInfo.d.ts +2 -0
- package/dist/src/helpers/cache/TtlCache.d.ts +15 -0
- package/dist/src/helpers/cache/cacheManager.d.ts +3 -0
- package/dist/src/helpers/cache/defaults.d.ts +1 -0
- package/dist/src/helpers/cache/index.d.ts +6 -0
- package/dist/src/helpers/cache/keys.d.ts +7 -0
- package/dist/src/helpers/cache/serverCaches.d.ts +9 -0
- package/dist/src/helpers/compress.d.ts +3 -0
- package/dist/src/helpers/escapeJson.d.ts +1 -0
- package/dist/src/helpers/metrics.d.ts +11 -0
- package/dist/src/helpers/normalizePlugins.d.ts +4 -0
- package/dist/src/helpers/runMiddlewares.d.ts +2 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/middlewares/auth.d.ts +2 -0
- package/dist/src/middlewares/basicAuth.d.ts +6 -0
- package/dist/src/middlewares/spaceDeployment.d.ts +2 -0
- package/dist/src/modules/mcp/handler.d.ts +3 -0
- package/dist/src/modules/mcp/server.d.ts +3 -0
- package/dist/src/modules/rsc/handler.d.ts +17 -0
- package/dist/src/modules/ssr/Component.d.ts +11 -0
- package/dist/src/modules/ssr/buildBody.d.ts +5 -0
- package/dist/src/modules/ssr/handler.d.ts +4 -0
- package/dist/src/modules/ssr/loadPluginComponents.d.ts +13 -0
- package/dist/src/modules/ssr/prepareRender.d.ts +13 -0
- package/dist/src/modules/ssr/registerExternalPlugins.d.ts +7 -0
- package/dist/src/modules/ssr/streamBody.d.ts +5 -0
- package/dist/src/modules/ssr/template.d.ts +6 -0
- package/dist/src/plugins/compile.d.ts +3 -0
- package/dist/src/plugins/copy.d.ts +1 -0
- package/dist/src/plugins/detect.d.ts +3 -0
- package/dist/src/plugins/manager.d.ts +40 -0
- package/dist/src/standalone/alias-loader.d.mts +1 -0
- package/dist/src/standalone/plugins/ClientInfo.d.ts +2 -0
- package/dist/src/standalone/plugins/ServerInfo.d.ts +2 -0
- package/dist/src/standalone/plugins/SharedInfo.d.ts +2 -0
- package/dist/src/standalone/plugins/styles.d.ts +5 -0
- package/dist/src/standalone/register-alias.d.mts +1 -0
- package/dist/src/standalone/server.d.ts +1 -0
- 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 };
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -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 };
|