@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
package/dist/.gitkeep
ADDED
|
File without changes
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
//#region src/adapters/jsonAdapters.ts
|
|
3
|
+
var isDeploymentObject = (v) => typeof v === "object" && ("spaceId" in v || "environment" in v || "error" in v);
|
|
4
|
+
var readJson = (filePath) => JSON.parse(readFileSync(filePath, "utf-8"));
|
|
5
|
+
var createJsonAdapters = (config) => {
|
|
6
|
+
const getOfflineData = (spaceId, environment, revision) => {
|
|
7
|
+
try {
|
|
8
|
+
const filePath = typeof config.offlineData === "function" ? config.offlineData(spaceId, environment, revision) : config.offlineData;
|
|
9
|
+
return Promise.resolve(readJson(filePath));
|
|
10
|
+
} catch (err) {
|
|
11
|
+
console.error("[JsonAdapters] Failed to read offlineData:", err.message);
|
|
12
|
+
return Promise.resolve(void 0);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
const getSpaceDeployment = (req) => {
|
|
16
|
+
const { deployment } = config;
|
|
17
|
+
if (!deployment) return Promise.resolve({
|
|
18
|
+
spaceId: 1,
|
|
19
|
+
environment: "main",
|
|
20
|
+
revision: 0
|
|
21
|
+
});
|
|
22
|
+
if (typeof deployment === "string") try {
|
|
23
|
+
return Promise.resolve(readJson(deployment));
|
|
24
|
+
} catch (err) {
|
|
25
|
+
console.error("[JsonAdapters] Failed to read deployment file:", err.message);
|
|
26
|
+
return Promise.resolve({ error: {
|
|
27
|
+
code: 500,
|
|
28
|
+
message: "Deployment config unreadable"
|
|
29
|
+
} });
|
|
30
|
+
}
|
|
31
|
+
if (isDeploymentObject(deployment)) return Promise.resolve(deployment);
|
|
32
|
+
const byHostname = deployment;
|
|
33
|
+
return Promise.resolve(byHostname[req.hostname] ?? byHostname["*"] ?? {
|
|
34
|
+
spaceId: 1,
|
|
35
|
+
environment: "main",
|
|
36
|
+
revision: 0
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
return {
|
|
40
|
+
getOfflineData,
|
|
41
|
+
getSpaceDeployment,
|
|
42
|
+
getUser: config.user ? (req) => Promise.resolve(typeof config.user === "function" ? config.user(req) : config.user) : void 0
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
//#endregion
|
|
46
|
+
export { createJsonAdapters };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { DEFAULT_TTL_MS } from "../helpers/cache/defaults.js";
|
|
2
|
+
import { buildCacheManager } from "../helpers/cache/cacheManager.js";
|
|
3
|
+
import { createServerCaches, destroyServerCaches } from "../helpers/cache/serverCaches.js";
|
|
4
|
+
import { makeHandler } from "./requestHandler.js";
|
|
5
|
+
import { buildTransport, protoLabel } from "./transports.js";
|
|
6
|
+
import normalizePlugins, { normalizePluginSource } from "../helpers/normalizePlugins.js";
|
|
7
|
+
import { compileTemplate } from "../modules/ssr/template.js";
|
|
8
|
+
import { PluginManager } from "../plugins/manager.js";
|
|
9
|
+
//#region src/core/createServer.ts
|
|
10
|
+
var createSSRServer = (config) => {
|
|
11
|
+
const { httpVersion: version = 2, cacheTtlMs: htmlTtlMs = DEFAULT_TTL_MS.html } = config;
|
|
12
|
+
if (version >= 3 && !config.tls) throw new Error(`[SSR] httpVersion: ${version} requires a tls config with key and cert`);
|
|
13
|
+
const caches = createServerCaches(htmlTtlMs, config.rsc?.cacheTtlMs ?? DEFAULT_TTL_MS.rsc);
|
|
14
|
+
const cache = caches.html ? buildCacheManager(caches.html) : null;
|
|
15
|
+
const renderFn = config.templateFn ?? compileTemplate();
|
|
16
|
+
const pluginManager = new PluginManager(normalizePlugins(config.plugins ?? {}), config.pluginsCacheDir, config.pluginsTtlMs, config.devMode);
|
|
17
|
+
const plugins = {
|
|
18
|
+
register: (name, source) => {
|
|
19
|
+
const normalized = normalizePluginSource(source);
|
|
20
|
+
pluginManager.register(`${name}@${normalized.version}`, normalized);
|
|
21
|
+
},
|
|
22
|
+
invalidate: (name, version) => pluginManager.invalidate(name, version)
|
|
23
|
+
};
|
|
24
|
+
let primary;
|
|
25
|
+
let h3;
|
|
26
|
+
return {
|
|
27
|
+
cache,
|
|
28
|
+
plugins,
|
|
29
|
+
listen(port, host = "0.0.0.0") {
|
|
30
|
+
const handler = makeHandler(config, port, renderFn, caches, pluginManager);
|
|
31
|
+
({primary, h3} = buildTransport(config, handler, port));
|
|
32
|
+
primary.listen(port, host, () => {
|
|
33
|
+
console.log(`[SSR] ${protoLabel(version, !!config.tls)} - listening on ${host}:${port}`);
|
|
34
|
+
});
|
|
35
|
+
},
|
|
36
|
+
async close() {
|
|
37
|
+
destroyServerCaches(caches);
|
|
38
|
+
pluginManager.destroy();
|
|
39
|
+
const closeOne = (srv) => new Promise((resolve, reject) => {
|
|
40
|
+
srv.close((err) => err ? reject(err) : resolve());
|
|
41
|
+
});
|
|
42
|
+
await Promise.all([closeOne(primary), ...h3 ? [closeOne(h3)] : []]);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
//#endregion
|
|
47
|
+
export { createSSRServer };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
//#region src/core/mimeTypes.ts
|
|
2
|
+
var MIME_TYPES = {
|
|
3
|
+
".html": "text/html; charset=utf-8",
|
|
4
|
+
".js": "application/javascript; charset=utf-8",
|
|
5
|
+
".mjs": "application/javascript; charset=utf-8",
|
|
6
|
+
".cjs": "application/javascript; charset=utf-8",
|
|
7
|
+
".css": "text/css; charset=utf-8",
|
|
8
|
+
".json": "application/json; charset=utf-8",
|
|
9
|
+
".png": "image/png",
|
|
10
|
+
".jpg": "image/jpeg",
|
|
11
|
+
".jpeg": "image/jpeg",
|
|
12
|
+
".gif": "image/gif",
|
|
13
|
+
".svg": "image/svg+xml",
|
|
14
|
+
".webp": "image/webp",
|
|
15
|
+
".ico": "image/x-icon",
|
|
16
|
+
".woff": "font/woff",
|
|
17
|
+
".woff2": "font/woff2",
|
|
18
|
+
".ttf": "font/ttf",
|
|
19
|
+
".eot": "application/vnd.ms-fontobject",
|
|
20
|
+
".txt": "text/plain; charset=utf-8",
|
|
21
|
+
".xml": "application/xml"
|
|
22
|
+
};
|
|
23
|
+
var IMMUTABLE_EXTS = new Set([
|
|
24
|
+
".js",
|
|
25
|
+
".mjs",
|
|
26
|
+
".cjs",
|
|
27
|
+
".css",
|
|
28
|
+
".woff",
|
|
29
|
+
".woff2",
|
|
30
|
+
".ttf",
|
|
31
|
+
".eot"
|
|
32
|
+
]);
|
|
33
|
+
var getMimeType = (filePath) => {
|
|
34
|
+
return MIME_TYPES[filePath.slice(filePath.lastIndexOf(".")).toLowerCase()] ?? "application/octet-stream";
|
|
35
|
+
};
|
|
36
|
+
var getCacheControl = (filePath) => {
|
|
37
|
+
const ext = filePath.slice(filePath.lastIndexOf(".")).toLowerCase();
|
|
38
|
+
return IMMUTABLE_EXTS.has(ext) ? "public, max-age=31536000, immutable" : "public, max-age=3600";
|
|
39
|
+
};
|
|
40
|
+
//#endregion
|
|
41
|
+
export { getCacheControl, getMimeType };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { parseRequest } from "./requestParser.js";
|
|
2
|
+
import { serveStatic } from "./staticFiles.js";
|
|
3
|
+
import { buildResponseHelpers } from "../helpers/buildResponseHelpers.js";
|
|
4
|
+
import { runMiddlewares } from "../helpers/runMiddlewares.js";
|
|
5
|
+
import { authMiddleware } from "../middlewares/auth.js";
|
|
6
|
+
import { basicAuthMiddleware } from "../middlewares/basicAuth.js";
|
|
7
|
+
import { spaceDeploymentMiddleware } from "../middlewares/spaceDeployment.js";
|
|
8
|
+
import { handleMcp } from "../modules/mcp/handler.js";
|
|
9
|
+
import { handleRsc } from "../modules/rsc/handler.js";
|
|
10
|
+
import { renderSSR } from "../modules/ssr/handler.js";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
//#region src/core/requestHandler.ts
|
|
14
|
+
var __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
var BUILTIN_PUBLIC_DIR = path.resolve(__dirname, "../public");
|
|
16
|
+
var handleRequest = async (raw, rawRes, config, port, renderFn, caches, pluginManager) => {
|
|
17
|
+
const req = parseRequest(raw);
|
|
18
|
+
const res = buildResponseHelpers(rawRes, req.headers["accept-encoding"]);
|
|
19
|
+
if (req.path === "\0") {
|
|
20
|
+
res.setStatus(400);
|
|
21
|
+
res.end();
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
25
|
+
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
26
|
+
res.setHeader("X-DNS-Prefetch-Control", "off");
|
|
27
|
+
const frameOptions = config.frameOptions === void 0 ? "DENY" : config.frameOptions;
|
|
28
|
+
if (frameOptions) if (Array.isArray(frameOptions)) res.setHeader("Content-Security-Policy", `frame-ancestors ${frameOptions.join(" ")}`);
|
|
29
|
+
else {
|
|
30
|
+
res.setHeader("X-Frame-Options", frameOptions);
|
|
31
|
+
res.setHeader("Content-Security-Policy", `frame-ancestors '${frameOptions === "DENY" ? "none" : "self"}'`);
|
|
32
|
+
}
|
|
33
|
+
if ((config.httpVersion ?? 2) >= 3) res.setHeader("Alt-Svc", `h3=":${port}"; ma=86400`);
|
|
34
|
+
if (serveStatic(req, res, BUILTIN_PUBLIC_DIR)) return;
|
|
35
|
+
if (config.publicDir && serveStatic(req, res, config.publicDir)) return;
|
|
36
|
+
if (req.path.startsWith("/.well-known/")) {
|
|
37
|
+
res.setStatus(404);
|
|
38
|
+
res.end();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (req.path.startsWith(pluginManager.urlPrefix + "/")) {
|
|
42
|
+
const relative = req.path.slice(pluginManager.urlPrefix.length);
|
|
43
|
+
const pluginName = relative.split("/")[1];
|
|
44
|
+
if (pluginName && pluginManager.hasPlugin(pluginName)) {
|
|
45
|
+
await pluginManager.prepare(pluginName);
|
|
46
|
+
if (serveStatic({
|
|
47
|
+
...req,
|
|
48
|
+
path: relative
|
|
49
|
+
}, res, pluginManager.outputDir)) return;
|
|
50
|
+
}
|
|
51
|
+
res.setStatus(404);
|
|
52
|
+
res.send("Not found");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const loginPath = config.loginPath === false ? null : config.loginPath ?? "/auth/login";
|
|
56
|
+
if (loginPath && req.method === "POST" && req.path === loginPath) {
|
|
57
|
+
if (await config.adapters.onLogin?.(req)) res.setStatus(200);
|
|
58
|
+
else res.setStatus(401);
|
|
59
|
+
res.end();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const logoutPath = config.logoutPath === false ? null : config.logoutPath ?? "/auth/logout";
|
|
63
|
+
if (logoutPath && req.method === "POST" && req.path === logoutPath) {
|
|
64
|
+
await config.adapters.onLogout?.(req);
|
|
65
|
+
res.setStatus(204);
|
|
66
|
+
res.end();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (config.static) for (const [prefix, rootDir] of Object.entries(config.static)) {
|
|
70
|
+
const normalizedPrefix = prefix.endsWith("/") ? prefix : `${prefix}/`;
|
|
71
|
+
if (req.path === prefix || req.path.startsWith(normalizedPrefix)) {
|
|
72
|
+
if (serveStatic({
|
|
73
|
+
...req,
|
|
74
|
+
path: req.path.slice(prefix.length) || "/"
|
|
75
|
+
}, res, rootDir)) return;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const mcpPath = config.mcp?.path ?? "/mcp";
|
|
79
|
+
if (config.mcp && (config.mcp.enabled ?? true) && req.path.startsWith(mcpPath)) {
|
|
80
|
+
await handleMcp(raw, rawRes, config.mcp);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (await runMiddlewares([
|
|
84
|
+
spaceDeploymentMiddleware(config.adapters),
|
|
85
|
+
basicAuthMiddleware({ cacheTtlMs: config.cacheTtlMs }),
|
|
86
|
+
authMiddleware(config.adapters),
|
|
87
|
+
...config.middlewares || []
|
|
88
|
+
], req, res) || res.status !== 200) return;
|
|
89
|
+
const rscPath = config.rsc?.path ?? "/_rsc";
|
|
90
|
+
if ((config.rsc?.enabled ?? !!config.adapters.getRscData) && req.method === "GET" && req.path === rscPath) {
|
|
91
|
+
await handleRsc(req, res, config, pluginManager, caches.rsc);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
await renderSSR(req, res, config, renderFn, pluginManager, caches);
|
|
95
|
+
};
|
|
96
|
+
var makeHandler = (config, port, renderFn, caches, pluginManager) => {
|
|
97
|
+
return (raw, rawRes) => {
|
|
98
|
+
handleRequest(raw, rawRes, config, port, renderFn, caches, pluginManager).catch((err) => {
|
|
99
|
+
console.error("[SSR] Unhandled error:", err);
|
|
100
|
+
try {
|
|
101
|
+
if (!rawRes.headersSent) rawRes.writeHead(500, { "Content-Type": "text/plain" });
|
|
102
|
+
rawRes.end("Internal Server Error");
|
|
103
|
+
} catch {}
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
//#endregion
|
|
108
|
+
export { makeHandler };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
//#region src/core/requestParser.ts
|
|
2
|
+
var HOSTNAME_RE = /^[a-zA-Z0-9.-]{1,253}$/u;
|
|
3
|
+
var parseRequest = (raw) => {
|
|
4
|
+
const headers = raw.headers;
|
|
5
|
+
const rawHostname = (headers[":authority"] ?? headers["host"] ?? "").split(":")[0] ?? "";
|
|
6
|
+
const hostname = HOSTNAME_RE.test(rawHostname) ? rawHostname : "";
|
|
7
|
+
const scheme = headers[":scheme"];
|
|
8
|
+
const forwarded = headers["x-forwarded-proto"];
|
|
9
|
+
const encrypted = !scheme && !forwarded && "encrypted" in raw.socket && raw.socket.encrypted === true;
|
|
10
|
+
const protocol = scheme === "https" || forwarded === "https" || encrypted ? "https" : "http";
|
|
11
|
+
const rawUrl = headers[":path"] ?? raw.url ?? "/";
|
|
12
|
+
const qIndex = rawUrl.indexOf("?");
|
|
13
|
+
const rawPath = qIndex === -1 ? rawUrl : rawUrl.slice(0, qIndex);
|
|
14
|
+
const path = rawPath.includes("\0") ? "\0" : rawPath;
|
|
15
|
+
const search = qIndex === -1 ? "" : rawUrl.slice(qIndex);
|
|
16
|
+
const query = {};
|
|
17
|
+
if (search) {
|
|
18
|
+
const sp = new URLSearchParams(search.slice(1));
|
|
19
|
+
for (const [k, v] of sp.entries()) query[k] = v;
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
method: (headers[":method"] ?? raw.method ?? "GET").toUpperCase(),
|
|
23
|
+
path,
|
|
24
|
+
search,
|
|
25
|
+
url: rawUrl,
|
|
26
|
+
hostname,
|
|
27
|
+
protocol,
|
|
28
|
+
headers,
|
|
29
|
+
query,
|
|
30
|
+
ctx: {}
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
//#endregion
|
|
34
|
+
export { parseRequest };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { getCacheControl, getMimeType } from "./mimeTypes.js";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
//#region src/core/staticFiles.ts
|
|
5
|
+
var buildEtag = (stat) => `"${stat.mtimeMs.toString(36)}-${stat.size.toString(36)}"`;
|
|
6
|
+
var serveFile = (req, res, filePath, stat) => {
|
|
7
|
+
const etag = buildEtag(stat);
|
|
8
|
+
const ifNoneMatch = req.headers["if-none-match"];
|
|
9
|
+
res.setHeader("ETag", etag);
|
|
10
|
+
res.setHeader("Cache-Control", getCacheControl(filePath));
|
|
11
|
+
res.setHeader("Last-Modified", stat.mtime.toUTCString());
|
|
12
|
+
if (ifNoneMatch === etag) {
|
|
13
|
+
res.setStatus(304);
|
|
14
|
+
res.end();
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
const content = fs.readFileSync(filePath);
|
|
18
|
+
res.setHeader("Content-Type", getMimeType(filePath));
|
|
19
|
+
res.setHeader("Content-Length", content.byteLength.toString());
|
|
20
|
+
res.send(content.toString("utf-8"));
|
|
21
|
+
return true;
|
|
22
|
+
};
|
|
23
|
+
var serveStatic = (req, res, rootDir) => {
|
|
24
|
+
const relative = req.path.replace(/^\/+/, "");
|
|
25
|
+
const filePath = path.resolve(rootDir, relative);
|
|
26
|
+
const resolvedRoot = path.resolve(rootDir);
|
|
27
|
+
if (filePath !== resolvedRoot && !filePath.startsWith(resolvedRoot + path.sep)) {
|
|
28
|
+
res.setStatus(403);
|
|
29
|
+
res.send("Forbidden");
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
if (!fs.existsSync(filePath)) return false;
|
|
33
|
+
const stat = fs.statSync(filePath);
|
|
34
|
+
if (stat.isDirectory()) {
|
|
35
|
+
const indexPath = path.join(filePath, "index.html");
|
|
36
|
+
if (!fs.existsSync(indexPath)) return false;
|
|
37
|
+
return serveFile(req, res, indexPath, fs.statSync(indexPath));
|
|
38
|
+
}
|
|
39
|
+
return serveFile(req, res, filePath, stat);
|
|
40
|
+
};
|
|
41
|
+
//#endregion
|
|
42
|
+
export { serveStatic };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import http2 from "node:http2";
|
|
3
|
+
import https from "node:https";
|
|
4
|
+
//#region src/core/transports.ts
|
|
5
|
+
var tlsOptions = (config) => {
|
|
6
|
+
const { tls } = config;
|
|
7
|
+
if (!tls) throw new Error("[SSR] TLS config required");
|
|
8
|
+
return {
|
|
9
|
+
key: tls.key,
|
|
10
|
+
cert: tls.cert,
|
|
11
|
+
minVersion: tls.minVersion ?? "TLSv1.3"
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
var protoLabel = (version, hasTls) => {
|
|
15
|
+
if (version >= 3) return "HTTP/2+3 (TLS)";
|
|
16
|
+
if (version >= 2) return hasTls ? "HTTP/2 (TLS)" : "HTTP/1.1 - TLS Missing";
|
|
17
|
+
return hasTls ? "HTTPS/1.1" : "HTTP/1.1";
|
|
18
|
+
};
|
|
19
|
+
var buildTransport = (config, handler, port) => {
|
|
20
|
+
const version = config.httpVersion ?? 2;
|
|
21
|
+
let primary;
|
|
22
|
+
let h3;
|
|
23
|
+
if (version >= 3) {
|
|
24
|
+
primary = http2.createSecureServer({
|
|
25
|
+
...tlsOptions(config),
|
|
26
|
+
allowHTTP1: true
|
|
27
|
+
}, handler);
|
|
28
|
+
(async () => {
|
|
29
|
+
try {
|
|
30
|
+
h3 = (await import("node:http3")).createServer(tlsOptions(config), handler);
|
|
31
|
+
h3.listen(port, "0.0.0.0", () => {
|
|
32
|
+
console.log(`[SSR] HTTP/3 (QUIC) listening on port ${port}`);
|
|
33
|
+
});
|
|
34
|
+
} catch {
|
|
35
|
+
console.warn("[SSR] HTTP/3 unavailable — start Node.js with --experimental-quic (requires Node ≥ 23). Falling back to HTTP/2.");
|
|
36
|
+
}
|
|
37
|
+
})();
|
|
38
|
+
} else if (version >= 2) if (config.tls) primary = http2.createSecureServer({
|
|
39
|
+
...tlsOptions(config),
|
|
40
|
+
allowHTTP1: true
|
|
41
|
+
}, handler);
|
|
42
|
+
else primary = http.createServer(handler);
|
|
43
|
+
else if (config.tls) primary = https.createServer(tlsOptions(config), handler);
|
|
44
|
+
else primary = http.createServer(handler);
|
|
45
|
+
return {
|
|
46
|
+
primary,
|
|
47
|
+
h3
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
//#endregion
|
|
51
|
+
export { buildTransport, protoLabel };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { compressBody, selectEncoding } from "./compress.js";
|
|
2
|
+
//#region src/helpers/buildResponseHelpers.ts
|
|
3
|
+
var buildResponseHelpers = (raw, acceptEncoding) => {
|
|
4
|
+
let statusCode = 200;
|
|
5
|
+
const encoding = selectEncoding(acceptEncoding);
|
|
6
|
+
const writeSend = (body) => {
|
|
7
|
+
const compressed = compressBody(body, encoding);
|
|
8
|
+
if (compressed !== body) {
|
|
9
|
+
raw.setHeader("Content-Encoding", encoding);
|
|
10
|
+
raw.setHeader("Vary", "Accept-Encoding");
|
|
11
|
+
}
|
|
12
|
+
raw.setHeader("Content-Length", Buffer.byteLength(compressed).toString());
|
|
13
|
+
if (!raw.headersSent) raw.writeHead(statusCode);
|
|
14
|
+
raw.end(compressed);
|
|
15
|
+
};
|
|
16
|
+
return {
|
|
17
|
+
get status() {
|
|
18
|
+
return statusCode;
|
|
19
|
+
},
|
|
20
|
+
get headers() {
|
|
21
|
+
return raw.getHeaders();
|
|
22
|
+
},
|
|
23
|
+
setHeader(name, value) {
|
|
24
|
+
raw.setHeader(name, value);
|
|
25
|
+
},
|
|
26
|
+
setStatus(code) {
|
|
27
|
+
statusCode = code;
|
|
28
|
+
},
|
|
29
|
+
send(body) {
|
|
30
|
+
writeSend(body);
|
|
31
|
+
},
|
|
32
|
+
write(chunk) {
|
|
33
|
+
if (!raw.headersSent) raw.writeHead(statusCode);
|
|
34
|
+
raw.write(chunk);
|
|
35
|
+
},
|
|
36
|
+
end() {
|
|
37
|
+
if (!raw.headersSent) raw.writeHead(statusCode);
|
|
38
|
+
raw.end();
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
//#endregion
|
|
43
|
+
export { buildResponseHelpers };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
//#region src/helpers/buildServerInfo.ts
|
|
2
|
+
var getEnvironment = (env = "production", server) => {
|
|
3
|
+
switch (env) {
|
|
4
|
+
case "production": return server;
|
|
5
|
+
case "staging": return {
|
|
6
|
+
apiServer: "https://api-stg.plitzi.com",
|
|
7
|
+
ssrServer: "https://ssr-stg.plitzi.com",
|
|
8
|
+
nodeServer: "https://server-stg.plitzi.com",
|
|
9
|
+
graphqlServer: "https://server-stg.plitzi.com/graphql",
|
|
10
|
+
websocketServer: "wss://server-stg.plitzi.com",
|
|
11
|
+
subscriptionServer: "wss://server-stg.plitzi.com/subscriptions",
|
|
12
|
+
...server
|
|
13
|
+
};
|
|
14
|
+
case "development": return {
|
|
15
|
+
apiServer: "https://api-dev.plitzi.com",
|
|
16
|
+
ssrServer: "https://ssr-dev.plitzi.com",
|
|
17
|
+
nodeServer: "https://server-dev.plitzi.com",
|
|
18
|
+
graphqlServer: "https://server-dev.plitzi.com/graphql",
|
|
19
|
+
websocketServer: "wss://server-dev.plitzi.com",
|
|
20
|
+
subscriptionServer: "wss://server-dev.plitzi.com/subscriptions",
|
|
21
|
+
...server
|
|
22
|
+
};
|
|
23
|
+
default: return {
|
|
24
|
+
apiServer: "https://api.plitzi.local",
|
|
25
|
+
ssrServer: "https://ssr.plitzi.local:4000",
|
|
26
|
+
nodeServer: "https://server.plitzi.local:8888",
|
|
27
|
+
graphqlServer: "https://server.plitzi.local:8888/graphql",
|
|
28
|
+
websocketServer: "wss://server.plitzi.local:8888",
|
|
29
|
+
subscriptionServer: "wss://server.plitzi.local:8888/subscriptions",
|
|
30
|
+
...server
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
var buildServerInfo = async (req, config) => {
|
|
35
|
+
const accessToken = req.query["access-token"];
|
|
36
|
+
const origin = `${req.protocol}://${req.hostname}`;
|
|
37
|
+
const user = req.ctx.user;
|
|
38
|
+
const { environment = "main", spaceId, revision = 0 } = req.ctx.spaceDeployment ?? {};
|
|
39
|
+
return getEnvironment(config.environment, {
|
|
40
|
+
basePath: "/",
|
|
41
|
+
requestUrl: req.url || "/",
|
|
42
|
+
origin,
|
|
43
|
+
location: {
|
|
44
|
+
hostname: req.hostname,
|
|
45
|
+
pathname: req.path || "/",
|
|
46
|
+
search: req.search
|
|
47
|
+
},
|
|
48
|
+
authenticated: !!user,
|
|
49
|
+
skipAuth: !!accessToken,
|
|
50
|
+
user: user ? { details: user } : void 0,
|
|
51
|
+
rscData: await config.adapters.getRscData?.(req, spaceId, environment, revision, user)
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
//#endregion
|
|
55
|
+
export { buildServerInfo };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { DEFAULT_TTL_MS } from "./defaults.js";
|
|
2
|
+
//#region src/helpers/cache/TtlCache.ts
|
|
3
|
+
var TtlCache = class {
|
|
4
|
+
store = /* @__PURE__ */ new Map();
|
|
5
|
+
sweepTimer;
|
|
6
|
+
constructor(ttlMs = DEFAULT_TTL_MS.html, maxSize = 500, sweepIntervalMs = ttlMs) {
|
|
7
|
+
this.ttlMs = ttlMs;
|
|
8
|
+
this.maxSize = maxSize;
|
|
9
|
+
this.sweepTimer = setInterval(() => this.sweep(), sweepIntervalMs);
|
|
10
|
+
this.sweepTimer.unref?.();
|
|
11
|
+
}
|
|
12
|
+
get(key) {
|
|
13
|
+
const entry = this.store.get(key);
|
|
14
|
+
if (!entry) return;
|
|
15
|
+
if (Date.now() > entry.expiresAt) {
|
|
16
|
+
this.store.delete(key);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
return entry.value;
|
|
20
|
+
}
|
|
21
|
+
set(key, value) {
|
|
22
|
+
if (this.store.size >= this.maxSize) this.store.delete(this.store.keys().next().value);
|
|
23
|
+
this.store.set(key, {
|
|
24
|
+
value,
|
|
25
|
+
expiresAt: Date.now() + this.ttlMs
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
delete(key) {
|
|
29
|
+
this.store.delete(key);
|
|
30
|
+
}
|
|
31
|
+
invalidateWhere(predicate) {
|
|
32
|
+
let count = 0;
|
|
33
|
+
for (const key of this.store.keys()) if (predicate(key)) {
|
|
34
|
+
this.store.delete(key);
|
|
35
|
+
count++;
|
|
36
|
+
}
|
|
37
|
+
return count;
|
|
38
|
+
}
|
|
39
|
+
clear() {
|
|
40
|
+
this.store.clear();
|
|
41
|
+
}
|
|
42
|
+
destroy() {
|
|
43
|
+
clearInterval(this.sweepTimer);
|
|
44
|
+
this.store.clear();
|
|
45
|
+
}
|
|
46
|
+
get size() {
|
|
47
|
+
return this.store.size;
|
|
48
|
+
}
|
|
49
|
+
sweep() {
|
|
50
|
+
const now = Date.now();
|
|
51
|
+
for (const [key, entry] of this.store) if (now > entry.expiresAt) this.store.delete(key);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
//#endregion
|
|
55
|
+
export { TtlCache };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region src/helpers/cache/cacheManager.ts
|
|
2
|
+
var buildCacheManager = (store) => ({
|
|
3
|
+
invalidate(filter) {
|
|
4
|
+
if (!filter || Object.keys(filter).length === 0) {
|
|
5
|
+
const count = store.size;
|
|
6
|
+
store.clear();
|
|
7
|
+
return count;
|
|
8
|
+
}
|
|
9
|
+
return store.invalidateWhere((key) => {
|
|
10
|
+
const [keySpaceId, keyEnvironment, , keyHostname] = key.split("\0");
|
|
11
|
+
if (filter.spaceId !== void 0 && keySpaceId !== String(filter.spaceId)) return false;
|
|
12
|
+
if (filter.environment !== void 0 && keyEnvironment !== filter.environment) return false;
|
|
13
|
+
if (filter.hostname !== void 0 && keyHostname !== filter.hostname) return false;
|
|
14
|
+
return true;
|
|
15
|
+
});
|
|
16
|
+
},
|
|
17
|
+
clear() {
|
|
18
|
+
store.clear();
|
|
19
|
+
},
|
|
20
|
+
get size() {
|
|
21
|
+
return store.size;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
//#endregion
|
|
25
|
+
export { buildCacheManager };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
//#region src/helpers/cache/keys.ts
|
|
2
|
+
var buildHtmlCacheKey = (accessToken = "anonymous", spaceId, environment, revision, req) => `${accessToken}\0${spaceId ?? 1}\0${environment}\0${revision}\0${req.hostname}\0${req.path}\0${req.search}`;
|
|
3
|
+
var buildOfflineDataCacheKey = (spaceId, environment, revision) => `${spaceId}|${environment}|${revision}`;
|
|
4
|
+
var buildRscCacheKey = (spaceId, environment, revision, userId, idsParam) => `${spaceId}|${environment}|${revision}|${userId ?? "anon"}|${idsParam ?? ""}`;
|
|
5
|
+
//#endregion
|
|
6
|
+
export { buildHtmlCacheKey, buildOfflineDataCacheKey, buildRscCacheKey };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { DEFAULT_TTL_MS } from "./defaults.js";
|
|
2
|
+
import { TtlCache } from "./TtlCache.js";
|
|
3
|
+
//#region src/helpers/cache/serverCaches.ts
|
|
4
|
+
var createServerCaches = (htmlTtlMs = DEFAULT_TTL_MS.html, rscTtlMs = DEFAULT_TTL_MS.rsc) => ({
|
|
5
|
+
html: htmlTtlMs > 0 ? new TtlCache(htmlTtlMs) : void 0,
|
|
6
|
+
rsc: rscTtlMs > 0 ? new TtlCache(rscTtlMs) : void 0,
|
|
7
|
+
offlineData: htmlTtlMs > 0 ? new TtlCache(htmlTtlMs) : void 0
|
|
8
|
+
});
|
|
9
|
+
var destroyServerCaches = ({ html, rsc, offlineData }) => {
|
|
10
|
+
html?.destroy();
|
|
11
|
+
rsc?.destroy();
|
|
12
|
+
offlineData?.destroy();
|
|
13
|
+
};
|
|
14
|
+
//#endregion
|
|
15
|
+
export { createServerCaches, destroyServerCaches };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { brotliCompressSync, constants, gzipSync } from "node:zlib";
|
|
2
|
+
//#region src/helpers/compress.ts
|
|
3
|
+
var MIN_SIZE = 1024;
|
|
4
|
+
var selectEncoding = (acceptEncoding) => {
|
|
5
|
+
if (!acceptEncoding) return "identity";
|
|
6
|
+
if (acceptEncoding.includes("br")) return "br";
|
|
7
|
+
if (acceptEncoding.includes("gzip")) return "gzip";
|
|
8
|
+
return "identity";
|
|
9
|
+
};
|
|
10
|
+
var compressBody = (body, encoding) => {
|
|
11
|
+
if (encoding === "identity" || body.length < MIN_SIZE) return body;
|
|
12
|
+
const buf = Buffer.from(body, "utf-8");
|
|
13
|
+
if (encoding === "br") return brotliCompressSync(buf, { params: { [constants.BROTLI_PARAM_QUALITY]: 4 } });
|
|
14
|
+
return gzipSync(buf, { level: 6 });
|
|
15
|
+
};
|
|
16
|
+
//#endregion
|
|
17
|
+
export { compressBody, selectEncoding };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
//#region src/helpers/metrics.ts
|
|
2
|
+
var RequestMetrics = class {
|
|
3
|
+
start;
|
|
4
|
+
phases = [];
|
|
5
|
+
constructor() {
|
|
6
|
+
this.start = performance.now();
|
|
7
|
+
}
|
|
8
|
+
async measure(name, fn) {
|
|
9
|
+
const t = performance.now();
|
|
10
|
+
const result = await Promise.resolve(fn());
|
|
11
|
+
this.phases.push({
|
|
12
|
+
name,
|
|
13
|
+
dur: Math.round(performance.now() - t)
|
|
14
|
+
});
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
/** Formats as Server-Timing header value. */
|
|
18
|
+
toServerTimingHeader() {
|
|
19
|
+
const total = Math.round(performance.now() - this.start);
|
|
20
|
+
return [...this.phases.map((p) => `${p.name};dur=${p.dur}`), `total;dur=${total}`].join(", ");
|
|
21
|
+
}
|
|
22
|
+
record(name, dur) {
|
|
23
|
+
this.phases.push({
|
|
24
|
+
name,
|
|
25
|
+
dur
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/** Logs a one-line summary to stdout (dev mode only). */
|
|
29
|
+
log(label) {
|
|
30
|
+
const total = Math.round(performance.now() - this.start);
|
|
31
|
+
const parts = this.phases.map((p) => `${p.name}=${p.dur}ms`).join(" ");
|
|
32
|
+
console.log(`[SSR] ${label} — ${parts} | total=${total}ms`);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
//#endregion
|
|
36
|
+
export { RequestMetrics };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/helpers/normalizePlugins.ts
|
|
2
|
+
var DEFAULT_PLUGIN_VERSION = "1.0.0";
|
|
3
|
+
var normalizePluginSource = (source) => ({
|
|
4
|
+
...source,
|
|
5
|
+
version: source.version ?? DEFAULT_PLUGIN_VERSION
|
|
6
|
+
});
|
|
7
|
+
var normalizePlugins = (plugins) => {
|
|
8
|
+
const out = {};
|
|
9
|
+
for (const [name, source] of Object.entries(plugins)) {
|
|
10
|
+
const normalized = normalizePluginSource(source);
|
|
11
|
+
out[`${name}@${normalized.version}`] = normalized;
|
|
12
|
+
}
|
|
13
|
+
return out;
|
|
14
|
+
};
|
|
15
|
+
//#endregion
|
|
16
|
+
export { normalizePlugins as default, normalizePluginSource };
|