@xacos/server 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +17 -0
- package/dist/Logger.d.ts +11 -0
- package/dist/Logger.d.ts.map +1 -0
- package/dist/Logger.js +36 -0
- package/dist/Logger.js.map +1 -0
- package/dist/XServer.d.ts +20 -0
- package/dist/XServer.d.ts.map +1 -0
- package/dist/XServer.js +155 -0
- package/dist/XServer.js.map +1 -0
- package/dist/XServer.test.d.ts +2 -0
- package/dist/XServer.test.d.ts.map +1 -0
- package/dist/XServer.test.js +76 -0
- package/dist/XServer.test.js.map +1 -0
- package/dist/adapters/RequestAdapter.d.ts +6 -0
- package/dist/adapters/RequestAdapter.d.ts.map +1 -0
- package/dist/adapters/RequestAdapter.js +95 -0
- package/dist/adapters/RequestAdapter.js.map +1 -0
- package/dist/adapters/RequestAdapter.test.d.ts +2 -0
- package/dist/adapters/RequestAdapter.test.d.ts.map +1 -0
- package/dist/adapters/RequestAdapter.test.js +38 -0
- package/dist/adapters/RequestAdapter.test.js.map +1 -0
- package/dist/dev/mountVite.d.ts +3 -0
- package/dist/dev/mountVite.d.ts.map +1 -0
- package/dist/dev/mountVite.js +72 -0
- package/dist/dev/mountVite.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/routing/mountApiRoutes.d.ts +4 -0
- package/dist/routing/mountApiRoutes.d.ts.map +1 -0
- package/dist/routing/mountApiRoutes.js +136 -0
- package/dist/routing/mountApiRoutes.js.map +1 -0
- package/dist/static/serveStaticChunks.d.ts +7 -0
- package/dist/static/serveStaticChunks.d.ts.map +1 -0
- package/dist/static/serveStaticChunks.js +15 -0
- package/dist/static/serveStaticChunks.js.map +1 -0
- package/dist/views/buildHtmlShell.d.ts +14 -0
- package/dist/views/buildHtmlShell.d.ts.map +1 -0
- package/dist/views/buildHtmlShell.js +24 -0
- package/dist/views/buildHtmlShell.js.map +1 -0
- package/dist/views/buildHtmlShell.test.d.ts +2 -0
- package/dist/views/buildHtmlShell.test.d.ts.map +1 -0
- package/dist/views/buildHtmlShell.test.js +22 -0
- package/dist/views/buildHtmlShell.test.js.map +1 -0
- package/dist/views/mountViewRoutes.d.ts +7 -0
- package/dist/views/mountViewRoutes.d.ts.map +1 -0
- package/dist/views/mountViewRoutes.js +25 -0
- package/dist/views/mountViewRoutes.js.map +1 -0
- package/package.json +77 -0
- package/src/Logger.ts +45 -0
- package/src/XServer.test.ts +83 -0
- package/src/XServer.ts +172 -0
- package/src/adapters/RequestAdapter.test.ts +48 -0
- package/src/adapters/RequestAdapter.ts +108 -0
- package/src/dev/mountVite.ts +90 -0
- package/src/index.ts +9 -0
- package/src/routing/mountApiRoutes.ts +152 -0
- package/src/static/serveStaticChunks.ts +21 -0
- package/src/views/buildHtmlShell.test.ts +26 -0
- package/src/views/buildHtmlShell.ts +35 -0
- package/src/views/mountViewRoutes.ts +34 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { XServer } from "./XServer";
|
|
2
|
+
export { adaptRequest, adaptResponse } from "./adapters/RequestAdapter";
|
|
3
|
+
export { mountApiRoutes } from "./routing/mountApiRoutes";
|
|
4
|
+
export { mountViteDev } from "./dev/mountVite";
|
|
5
|
+
export { mountViewRoutes } from "./views/mountViewRoutes";
|
|
6
|
+
export { serveStaticChunks } from "./static/serveStaticChunks";
|
|
7
|
+
export { buildHtmlShell, type HtmlShellOptions } from "./views/buildHtmlShell";
|
|
8
|
+
export { Logger, log } from "./Logger";
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AACxE,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAC/D,OAAO,EAAE,cAAc,EAAE,KAAK,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/E,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { XServer } from "./XServer";
|
|
2
|
+
export { adaptRequest, adaptResponse } from "./adapters/RequestAdapter";
|
|
3
|
+
export { mountApiRoutes } from "./routing/mountApiRoutes";
|
|
4
|
+
export { mountViteDev } from "./dev/mountVite";
|
|
5
|
+
export { mountViewRoutes } from "./views/mountViewRoutes";
|
|
6
|
+
export { serveStaticChunks } from "./static/serveStaticChunks";
|
|
7
|
+
export { buildHtmlShell } from "./views/buildHtmlShell";
|
|
8
|
+
export { Logger, log } from "./Logger";
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AACxE,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAC/D,OAAO,EAAE,cAAc,EAAyB,MAAM,wBAAwB,CAAC;AAC/E,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mountApiRoutes.d.ts","sourceRoot":"","sources":["../../src/routing/mountApiRoutes.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAgB1C,wBAAsB,cAAc,CAClC,OAAO,EAAE,eAAe,EACxB,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,IAAI,GACR,OAAO,CAAC,IAAI,CAAC,CA+Hf"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { pathToFileURL } from "node:url";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { scanApiFiles, compose, invalidateScanCache } from "@xacos/core";
|
|
4
|
+
import { adaptRequest, adaptResponse } from "../adapters/RequestAdapter";
|
|
5
|
+
import { validate, ValidationError } from "@xacos/validation";
|
|
6
|
+
import { log } from "../Logger";
|
|
7
|
+
// WeakMap scoping route registries per Fastify instance
|
|
8
|
+
const serverRoutesRegistry = new WeakMap();
|
|
9
|
+
// Track if chokidar watcher is already initialized for this fastify instance
|
|
10
|
+
const activeWatchers = new WeakSet();
|
|
11
|
+
export async function mountApiRoutes(fastify, apiDir, app) {
|
|
12
|
+
let activeRoutesRegistry = serverRoutesRegistry.get(fastify);
|
|
13
|
+
if (!activeRoutesRegistry) {
|
|
14
|
+
activeRoutesRegistry = new Map();
|
|
15
|
+
serverRoutesRegistry.set(fastify, activeRoutesRegistry);
|
|
16
|
+
}
|
|
17
|
+
invalidateScanCache();
|
|
18
|
+
const files = scanApiFiles(apiDir);
|
|
19
|
+
for (const { filePath, urlPrefix } of files) {
|
|
20
|
+
try {
|
|
21
|
+
const mod = await import(pathToFileURL(filePath).href);
|
|
22
|
+
const router = mod?.router;
|
|
23
|
+
if (!router)
|
|
24
|
+
continue;
|
|
25
|
+
for (const route of router.routes) {
|
|
26
|
+
const routePath = route.path === "/" ? "" : route.path;
|
|
27
|
+
const fullPath = `${urlPrefix}${routePath}`.replace(/\/+/g, "/");
|
|
28
|
+
const pipeline = route.middlewares && route.middlewares.length ? compose(route.middlewares) : null;
|
|
29
|
+
const routeKey = `${route.method}:${fullPath}`;
|
|
30
|
+
if (activeRoutesRegistry.has(routeKey)) {
|
|
31
|
+
activeRoutesRegistry.set(routeKey, { route, pipeline });
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
activeRoutesRegistry.set(routeKey, { route, pipeline });
|
|
35
|
+
fastify.route({
|
|
36
|
+
method: route.method,
|
|
37
|
+
url: fullPath,
|
|
38
|
+
handler: async (request, reply) => {
|
|
39
|
+
// Ensure we retrieve from the instance-scoped registry at request time
|
|
40
|
+
const registry = serverRoutesRegistry.get(fastify);
|
|
41
|
+
const meta = registry?.get(routeKey);
|
|
42
|
+
if (!meta) {
|
|
43
|
+
void reply.code(404);
|
|
44
|
+
return { error: "Route not found" };
|
|
45
|
+
}
|
|
46
|
+
const currentRoute = meta.route;
|
|
47
|
+
const currentPipeline = meta.pipeline;
|
|
48
|
+
const req = adaptRequest(request, app);
|
|
49
|
+
const res = adaptResponse(reply);
|
|
50
|
+
try {
|
|
51
|
+
if (currentRoute.schema) {
|
|
52
|
+
if (currentRoute.schema.body)
|
|
53
|
+
validate(req.body, currentRoute.schema.body);
|
|
54
|
+
if (currentRoute.schema.query)
|
|
55
|
+
validate(req.query, currentRoute.schema.query);
|
|
56
|
+
if (currentRoute.schema.params)
|
|
57
|
+
validate(req.params, currentRoute.schema.params);
|
|
58
|
+
}
|
|
59
|
+
if (currentPipeline) {
|
|
60
|
+
await currentPipeline(req, res);
|
|
61
|
+
}
|
|
62
|
+
await currentRoute.handler(req, res);
|
|
63
|
+
if (res._payload !== undefined) {
|
|
64
|
+
return res._payload;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
if (e instanceof ValidationError) {
|
|
69
|
+
void reply.code(e.statusCode);
|
|
70
|
+
return { errors: e.errors };
|
|
71
|
+
}
|
|
72
|
+
throw e;
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
console.error("IMPORT ERROR:", err);
|
|
81
|
+
log.error(`Failed to mount API file ${filePath}: ${err.message}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (process.env["APP_ENV"] !== "production" && !activeWatchers.has(fastify)) {
|
|
85
|
+
activeWatchers.add(fastify);
|
|
86
|
+
try {
|
|
87
|
+
const chokidar = await import("chokidar");
|
|
88
|
+
const watcher = chokidar.watch([apiDir, resolve(apiDir, "../app")], {
|
|
89
|
+
ignoreInitial: true,
|
|
90
|
+
});
|
|
91
|
+
fastify.addHook("onClose", async () => {
|
|
92
|
+
await watcher.close();
|
|
93
|
+
activeWatchers.delete(fastify);
|
|
94
|
+
});
|
|
95
|
+
watcher.on("all", (event, path) => {
|
|
96
|
+
log.info(`[HMR] Detected change in ${path} (${event})`);
|
|
97
|
+
void (async () => {
|
|
98
|
+
try {
|
|
99
|
+
const registry = serverRoutesRegistry.get(fastify);
|
|
100
|
+
if (!registry)
|
|
101
|
+
return;
|
|
102
|
+
invalidateScanCache();
|
|
103
|
+
const updatedFiles = scanApiFiles(apiDir);
|
|
104
|
+
for (const { filePath, urlPrefix } of updatedFiles) {
|
|
105
|
+
const mod = await import(pathToFileURL(filePath).href + "?t=" + Date.now());
|
|
106
|
+
const router = mod?.router;
|
|
107
|
+
if (!router)
|
|
108
|
+
continue;
|
|
109
|
+
for (const route of router.routes) {
|
|
110
|
+
const routePath = route.path === "/" ? "" : route.path;
|
|
111
|
+
const fullPath = `${urlPrefix}${routePath}`.replace(/\/+/g, "/");
|
|
112
|
+
const pipeline = route.middlewares && route.middlewares.length ? compose(route.middlewares) : null;
|
|
113
|
+
const routeKey = `${route.method}:${fullPath}`;
|
|
114
|
+
if (registry.has(routeKey)) {
|
|
115
|
+
registry.set(routeKey, { route, pipeline });
|
|
116
|
+
log.info(`[HMR] Hot-swapped route ${route.method} ${fullPath}`);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
registry.set(routeKey, { route, pipeline });
|
|
120
|
+
log.info(`[HMR] Registered new route ${route.method} ${fullPath}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
log.error(`[HMR] Failed to hot-reload routes: ${err.message}`);
|
|
127
|
+
}
|
|
128
|
+
})();
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
log.error(`[HMR] Failed to initialize chokidar: ${err.message}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=mountApiRoutes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mountApiRoutes.js","sourceRoot":"","sources":["../../src/routing/mountApiRoutes.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { FastifyInstance } from 'fastify';
|
|
2
|
+
/**
|
|
3
|
+
* Serves production view bundles generated by esbuild.
|
|
4
|
+
* Chunks are served under the /__xacos/ prefix.
|
|
5
|
+
*/
|
|
6
|
+
export declare function serveStaticChunks(fastify: FastifyInstance, chunksDir: string): Promise<void>;
|
|
7
|
+
//# sourceMappingURL=serveStaticChunks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serveStaticChunks.d.ts","sourceRoot":"","sources":["../../src/static/serveStaticChunks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAG/C;;;GAGG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,eAAe,EACxB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CASf"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { resolve } from 'path';
|
|
2
|
+
/**
|
|
3
|
+
* Serves production view bundles generated by esbuild.
|
|
4
|
+
* Chunks are served under the /__xacos/ prefix.
|
|
5
|
+
*/
|
|
6
|
+
export async function serveStaticChunks(fastify, chunksDir) {
|
|
7
|
+
// @ts-ignore - Dynamic import of @fastify/static
|
|
8
|
+
const fastifyStatic = (await import('@fastify/static')).default;
|
|
9
|
+
await fastify.register(fastifyStatic, {
|
|
10
|
+
root: resolve(chunksDir),
|
|
11
|
+
prefix: '/__xacos/',
|
|
12
|
+
decorateReply: false, // Avoid collision if already registered
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=serveStaticChunks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serveStaticChunks.js","sourceRoot":"","sources":["../../src/static/serveStaticChunks.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAE/B;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,OAAwB,EACxB,SAAiB;IAEjB,iDAAiD;IACjD,MAAM,aAAa,GAAG,CAAC,MAAM,MAAM,CAAC,iBAAiB,CAAC,CAAC,CAAC,OAAO,CAAC;IAEhE,MAAM,OAAO,CAAC,QAAQ,CAAC,aAAa,EAAE;QACpC,IAAI,EAAE,OAAO,CAAC,SAAS,CAAC;QACxB,MAAM,EAAE,WAAW;QACnB,aAAa,EAAE,KAAK,EAAE,wCAAwC;KAC/D,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface HtmlShellOptions {
|
|
2
|
+
chunkUrl: string;
|
|
3
|
+
ssrHtml?: string;
|
|
4
|
+
meta?: {
|
|
5
|
+
title?: string | undefined;
|
|
6
|
+
description?: string | undefined;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Generate a complete HTML shell for a page.
|
|
11
|
+
* Wires the page-specific JS chunk into the <script> tag.
|
|
12
|
+
*/
|
|
13
|
+
export declare function buildHtmlShell(options: HtmlShellOptions): string;
|
|
14
|
+
//# sourceMappingURL=buildHtmlShell.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"buildHtmlShell.d.ts","sourceRoot":"","sources":["../../src/views/buildHtmlShell.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE;QACL,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;QAC3B,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;KAClC,CAAC;CACH;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,gBAAgB,GAAG,MAAM,CAoBhE"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate a complete HTML shell for a page.
|
|
3
|
+
* Wires the page-specific JS chunk into the <script> tag.
|
|
4
|
+
*/
|
|
5
|
+
export function buildHtmlShell(options) {
|
|
6
|
+
const { chunkUrl, ssrHtml = '', meta = {} } = options;
|
|
7
|
+
const headTags = [
|
|
8
|
+
`<title>${meta.title ?? 'XAOCS App'}</title>`,
|
|
9
|
+
meta.description ? `<meta name="description" content="${meta.description}" />` : '',
|
|
10
|
+
`<script type="module" src="${chunkUrl}"></script>`,
|
|
11
|
+
].filter(Boolean).join('\n ');
|
|
12
|
+
return `<!DOCTYPE html>
|
|
13
|
+
<html lang="en">
|
|
14
|
+
<head>
|
|
15
|
+
<meta charset="UTF-8" />
|
|
16
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
17
|
+
${headTags}
|
|
18
|
+
</head>
|
|
19
|
+
<body>
|
|
20
|
+
<div id="root">${ssrHtml}</div>
|
|
21
|
+
</body>
|
|
22
|
+
</html>`;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=buildHtmlShell.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"buildHtmlShell.js","sourceRoot":"","sources":["../../src/views/buildHtmlShell.ts"],"names":[],"mappings":"AASA;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,OAAyB;IACtD,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAG,EAAE,EAAE,IAAI,GAAG,EAAE,EAAE,GAAG,OAAO,CAAC;IAEtD,MAAM,QAAQ,GAAG;QACf,UAAU,IAAI,CAAC,KAAK,IAAI,WAAW,UAAU;QAC7C,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,qCAAqC,IAAI,CAAC,WAAW,MAAM,CAAC,CAAC,CAAC,EAAE;QACnF,8BAA8B,QAAQ,aAAa;KACpD,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAEjC,OAAO;;;;;IAKL,QAAQ;;;mBAGO,OAAO;;QAElB,CAAC;AACT,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"buildHtmlShell.test.d.ts","sourceRoot":"","sources":["../../src/views/buildHtmlShell.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, it, expect } from 'bun:test';
|
|
2
|
+
import { buildHtmlShell } from './buildHtmlShell';
|
|
3
|
+
describe('buildHtmlShell', () => {
|
|
4
|
+
it('injects chunk URL and meta tags', () => {
|
|
5
|
+
const html = buildHtmlShell({
|
|
6
|
+
chunkUrl: '/__xacos/home.js',
|
|
7
|
+
meta: { title: 'Home', description: 'Testing' }
|
|
8
|
+
});
|
|
9
|
+
expect(html).toContain('<title>Home</title>');
|
|
10
|
+
expect(html).toContain('description" content="Testing"');
|
|
11
|
+
expect(html).toContain('src="/__xacos/home.js"');
|
|
12
|
+
expect(html).toContain('<div id="root"></div>');
|
|
13
|
+
});
|
|
14
|
+
it('injects SSR HTML into root div', () => {
|
|
15
|
+
const html = buildHtmlShell({
|
|
16
|
+
chunkUrl: '/__xacos/home.js',
|
|
17
|
+
ssrHtml: '<h1>SSR Content</h1>'
|
|
18
|
+
});
|
|
19
|
+
expect(html).toContain('<div id="root"><h1>SSR Content</h1></div>');
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
//# sourceMappingURL=buildHtmlShell.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"buildHtmlShell.test.js","sourceRoot":"","sources":["../../src/views/buildHtmlShell.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAElD,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;IAC9B,EAAE,CAAC,iCAAiC,EAAE,GAAG,EAAE;QACzC,MAAM,IAAI,GAAG,cAAc,CAAC;YAC1B,QAAQ,EAAE,kBAAkB;YAC5B,IAAI,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,SAAS,EAAE;SAChD,CAAC,CAAC;QAEH,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,qBAAqB,CAAC,CAAC;QAC9C,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,gCAAgC,CAAC,CAAC;QACzD,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,wBAAwB,CAAC,CAAC;QACjD,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,uBAAuB,CAAC,CAAC;IAClD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gCAAgC,EAAE,GAAG,EAAE;QACxC,MAAM,IAAI,GAAG,cAAc,CAAC;YAC1B,QAAQ,EAAE,kBAAkB;YAC5B,OAAO,EAAE,sBAAsB;SAChC,CAAC,CAAC;QAEH,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,2CAA2C,CAAC,CAAC;IACtE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { FastifyInstance } from 'fastify';
|
|
2
|
+
/**
|
|
3
|
+
* Scans views and mounts GET routes for each page.
|
|
4
|
+
* Each route serves the production HTML shell with hydrated chunks.
|
|
5
|
+
*/
|
|
6
|
+
export declare function mountViewRoutes(fastify: FastifyInstance, viewsDir: string): Promise<void>;
|
|
7
|
+
//# sourceMappingURL=mountViewRoutes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mountViewRoutes.d.ts","sourceRoot":"","sources":["../../src/views/mountViewRoutes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAK/C;;;GAGG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,eAAe,EACxB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC,CAoBf"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { scanViews, extractPageMeta } from '@xacos/compiler';
|
|
2
|
+
import { buildHtmlShell } from './buildHtmlShell';
|
|
3
|
+
/**
|
|
4
|
+
* Scans views and mounts GET routes for each page.
|
|
5
|
+
* Each route serves the production HTML shell with hydrated chunks.
|
|
6
|
+
*/
|
|
7
|
+
export async function mountViewRoutes(fastify, viewsDir) {
|
|
8
|
+
const views = scanViews(viewsDir);
|
|
9
|
+
for (const view of views) {
|
|
10
|
+
// Import view to extract meta (Bun handles TS)
|
|
11
|
+
const mod = await import(view.filePath);
|
|
12
|
+
const meta = extractPageMeta(mod);
|
|
13
|
+
fastify.get(view.urlPattern, async (_request, reply) => {
|
|
14
|
+
const html = buildHtmlShell({
|
|
15
|
+
chunkUrl: `/__xacos/${view.chunkName}.js`,
|
|
16
|
+
meta: {
|
|
17
|
+
title: meta.title,
|
|
18
|
+
description: meta.description,
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
return reply.type('text/html').send(html);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=mountViewRoutes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mountViewRoutes.js","sourceRoot":"","sources":["../../src/views/mountViewRoutes.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAC7D,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAGlD;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,OAAwB,EACxB,QAAgB;IAEhB,MAAM,KAAK,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;IAElC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,+CAA+C;QAC/C,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACxC,MAAM,IAAI,GAAG,eAAe,CAAC,GAAG,CAAC,CAAC;QAElC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE;YACrD,MAAM,IAAI,GAAG,cAAc,CAAC;gBAC1B,QAAQ,EAAE,YAAY,IAAI,CAAC,SAAS,KAAK;gBACzC,IAAI,EAAE;oBACJ,KAAK,EAAE,IAAI,CAAC,KAAK;oBACjB,WAAW,EAAE,IAAI,CAAC,WAAW;iBAC9B;aACF,CAAC,CAAC;YAEH,OAAO,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC5C,CAAC,CAAC,CAAC;IACL,CAAC;AACH,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xacos/server",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@fastify/cookie": "^9.0.0",
|
|
15
|
+
"@fastify/cors": "9",
|
|
16
|
+
"@fastify/formbody": "7",
|
|
17
|
+
"@fastify/multipart": "^10.0.0",
|
|
18
|
+
"@fastify/rate-limit": "9",
|
|
19
|
+
"@xacos/compiler": "workspace:*",
|
|
20
|
+
"@xacos/config": "workspace:*",
|
|
21
|
+
"@xacos/core": "workspace:*",
|
|
22
|
+
"@xacos/orm": "workspace:*",
|
|
23
|
+
"@xacos/shared": "workspace:*",
|
|
24
|
+
"@xacos/validation": "workspace:*",
|
|
25
|
+
"chokidar": "^4.0.1",
|
|
26
|
+
"fastify": "^4.28.1",
|
|
27
|
+
"pino": "^10.3.1",
|
|
28
|
+
"pino-pretty": "^13.1.3",
|
|
29
|
+
"vite": "^5.x",
|
|
30
|
+
"zod": "^4.4.3"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^22.15.3",
|
|
34
|
+
"bun-types": "^1.3.12"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsc",
|
|
38
|
+
"dev": "tsc --watch",
|
|
39
|
+
"test": "bun test src",
|
|
40
|
+
"type-check": "tsc --noEmit"
|
|
41
|
+
},
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"author": "XAOCS Team",
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "https://github.com/zoherr/xaocs.git",
|
|
47
|
+
"directory": "packages/server"
|
|
48
|
+
},
|
|
49
|
+
"homepage": "https://xaocs.dev",
|
|
50
|
+
"bugs": {
|
|
51
|
+
"url": "https://github.com/zoherr/xaocs/issues"
|
|
52
|
+
},
|
|
53
|
+
"keywords": [
|
|
54
|
+
"xaocs",
|
|
55
|
+
"xacos",
|
|
56
|
+
"framework",
|
|
57
|
+
"typescript",
|
|
58
|
+
"fullstack",
|
|
59
|
+
"fastify",
|
|
60
|
+
"react"
|
|
61
|
+
],
|
|
62
|
+
"files": [
|
|
63
|
+
"dist",
|
|
64
|
+
"src",
|
|
65
|
+
"README.md",
|
|
66
|
+
"LICENSE"
|
|
67
|
+
],
|
|
68
|
+
"publishConfig": {
|
|
69
|
+
"access": "public",
|
|
70
|
+
"registry": "https://registry.npmjs.org",
|
|
71
|
+
"provenance": false
|
|
72
|
+
},
|
|
73
|
+
"engines": {
|
|
74
|
+
"bun": ">=1.0.0",
|
|
75
|
+
"node": ">=18.0.0"
|
|
76
|
+
}
|
|
77
|
+
}
|
package/src/Logger.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import pino from 'pino';
|
|
2
|
+
|
|
3
|
+
export class Logger {
|
|
4
|
+
private logger: pino.Logger;
|
|
5
|
+
|
|
6
|
+
constructor() {
|
|
7
|
+
const isDev = process.env["APP_ENV"] !== "production";
|
|
8
|
+
this.logger = pino({
|
|
9
|
+
level: process.env["APP_DEBUG"] === "true" || isDev ? "debug" : "info",
|
|
10
|
+
transport: isDev
|
|
11
|
+
? {
|
|
12
|
+
target: "pino-pretty",
|
|
13
|
+
options: {
|
|
14
|
+
translateTime: "HH:MM:ss Z",
|
|
15
|
+
ignore: "pid,hostname",
|
|
16
|
+
},
|
|
17
|
+
}
|
|
18
|
+
: undefined,
|
|
19
|
+
} as any);
|
|
20
|
+
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
info(message: string, ...args: any[]) {
|
|
24
|
+
this.logger.info(message, ...args);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
error(message: string, ...args: any[]) {
|
|
28
|
+
this.logger.error(message, ...args);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
warn(message: string, ...args: any[]) {
|
|
32
|
+
this.logger.warn(message, ...args);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
debug(message: string, ...args: any[]) {
|
|
36
|
+
this.logger.debug(message, ...args);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
fatal(message: string, ...args: any[]) {
|
|
40
|
+
this.logger.fatal(message, ...args);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const log = new Logger();
|
|
45
|
+
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { XServer } from "./XServer";
|
|
5
|
+
|
|
6
|
+
describe("XServer", () => {
|
|
7
|
+
it("registers global middleware", () => {
|
|
8
|
+
const server = new XServer();
|
|
9
|
+
|
|
10
|
+
server.useGlobal(async (_req, _res, next) => {
|
|
11
|
+
await next();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
expect(server.getGlobalMiddlewares().length).toBe(1);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("mountApi registers scanned routes", async () => {
|
|
18
|
+
const apiDir = join(fileURLToPath(new URL(".", import.meta.url)), "..", "test-fixtures", "api");
|
|
19
|
+
const app = { make: <T>(_key: string) => ({}) as T };
|
|
20
|
+
|
|
21
|
+
const server = new XServer();
|
|
22
|
+
const port = 18_900 + Math.floor(Math.random() * 500);
|
|
23
|
+
try {
|
|
24
|
+
await server.mountApi(apiDir, app);
|
|
25
|
+
await server.listen(port);
|
|
26
|
+
const res = await fetch(`http://127.0.0.1:${String(port)}/api/echo/`);
|
|
27
|
+
expect(res.ok).toBe(true);
|
|
28
|
+
const body = (await res.json()) as { ok?: boolean };
|
|
29
|
+
expect(body.ok).toBe(true);
|
|
30
|
+
} finally {
|
|
31
|
+
await server.close();
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("returns 422 JSON for route schema validation failures", async () => {
|
|
36
|
+
const apiDir = join(fileURLToPath(new URL(".", import.meta.url)), "..", "test-fixtures", "api");
|
|
37
|
+
const app = { make: <T>(_key: string) => ({}) as T };
|
|
38
|
+
|
|
39
|
+
const server = new XServer();
|
|
40
|
+
const port = 19_050 + Math.floor(Math.random() * 500);
|
|
41
|
+
try {
|
|
42
|
+
await server.mountApi(apiDir, app);
|
|
43
|
+
await server.listen(port);
|
|
44
|
+
|
|
45
|
+
const bad = await fetch(`http://127.0.0.1:${String(port)}/api/validated/`, {
|
|
46
|
+
method: "POST",
|
|
47
|
+
headers: { "content-type": "application/json" },
|
|
48
|
+
body: JSON.stringify({ name: "x" }),
|
|
49
|
+
});
|
|
50
|
+
expect(bad.status).toBe(422);
|
|
51
|
+
const payload = (await bad.json()) as { errors?: { field: string; message: string }[] };
|
|
52
|
+
expect(Array.isArray(payload.errors)).toBe(true);
|
|
53
|
+
|
|
54
|
+
const good = await fetch(`http://127.0.0.1:${String(port)}/api/validated/`, {
|
|
55
|
+
method: "POST",
|
|
56
|
+
headers: { "content-type": "application/json" },
|
|
57
|
+
body: JSON.stringify({ name: "Ada" }),
|
|
58
|
+
});
|
|
59
|
+
expect(good.ok).toBe(true);
|
|
60
|
+
const okBody = (await good.json()) as { name?: string };
|
|
61
|
+
expect(okBody.name).toBe("Ada");
|
|
62
|
+
} finally {
|
|
63
|
+
await server.close();
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("provides a health check endpoint", async () => {
|
|
68
|
+
const server = new XServer();
|
|
69
|
+
const port = 19_200 + Math.floor(Math.random() * 500);
|
|
70
|
+
try {
|
|
71
|
+
await server.listen(port);
|
|
72
|
+
const res = await fetch(`http://127.0.0.1:${String(port)}/health`);
|
|
73
|
+
expect(res.ok).toBe(true);
|
|
74
|
+
const body = (await res.json()) as { status?: string; version?: string; env?: string };
|
|
75
|
+
expect(body.status).toBe("ok");
|
|
76
|
+
expect(body.version).toBeDefined();
|
|
77
|
+
expect(body.env).toBeDefined();
|
|
78
|
+
} finally {
|
|
79
|
+
await server.close();
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|