@krantho/server-node 0.0.0-stage → 1.0.1
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/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/node-request.d.ts +4 -0
- package/dist/node-request.d.ts.map +1 -0
- package/dist/node-request.js +31 -0
- package/dist/node-request.js.map +1 -0
- package/dist/node-response.d.ts +18 -0
- package/dist/node-response.d.ts.map +1 -0
- package/dist/node-response.js +74 -0
- package/dist/node-response.js.map +1 -0
- package/dist/page-cache.d.ts +58 -0
- package/dist/page-cache.d.ts.map +1 -0
- package/dist/page-cache.js +63 -0
- package/dist/page-cache.js.map +1 -0
- package/dist/standalone.d.ts +17 -0
- package/dist/standalone.d.ts.map +1 -0
- package/dist/standalone.js +83 -0
- package/dist/standalone.js.map +1 -0
- package/dist/vite.d.ts +11 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +72 -0
- package/dist/vite.js.map +1 -0
- package/package.json +53 -3
- package/README.md +0 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Descoped
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { KranthoHandler, KranthoHandlerOptions } from '@krantho/server';
|
|
2
|
+
export type { ContentLoader, PageRequestOptions, PageRequestResult, StudioRequest, StudioServer, } from '@krantho/server';
|
|
3
|
+
export { createContentLoader, createStudioServer, runPageRequest, verifyPreviewRequest, } from '@krantho/server';
|
|
4
|
+
export { toWebRequest } from './node-request.js';
|
|
5
|
+
export { writeResponseHeaders, writeWebResponse } from './node-response.js';
|
|
6
|
+
export type { CachedPage, CachedPageHeaders, PageHeaders, PageRequestInfo, RenderedPage, } from './page-cache.js';
|
|
7
|
+
export { renderCachedPage } from './page-cache.js';
|
|
8
|
+
export type { KranthoHandlerOptions };
|
|
9
|
+
/**
|
|
10
|
+
* Create a raw handler for use with any Node.js HTTP server.
|
|
11
|
+
*
|
|
12
|
+
* Returns an object with `handle(request: Request): Promise<Response>`.
|
|
13
|
+
* The handler is initialized lazily on first request and reused (singleton).
|
|
14
|
+
*
|
|
15
|
+
* Use this when integrating with Express, Fastify, or a custom Node.js server.
|
|
16
|
+
* For Vite dev middleware, use `@krantho/server-node/vite` instead.
|
|
17
|
+
* For a standalone production server, use `@krantho/server-node/standalone`.
|
|
18
|
+
*/
|
|
19
|
+
export declare function createHandler(options: KranthoHandlerOptions): KranthoHandler;
|
|
20
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAA;AAG5E,YAAY,EACV,aAAa,EACb,kBAAkB,EAClB,iBAAiB,EACjB,aAAa,EACb,YAAY,GACb,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,cAAc,EACd,oBAAoB,GACrB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAC3E,YAAY,EACV,UAAU,EACV,iBAAiB,EACjB,WAAW,EACX,eAAe,EACf,YAAY,GACb,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAClD,YAAY,EAAE,qBAAqB,EAAE,CAAA;AAErC;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,qBAAqB,GAAG,cAAc,CAqB5E"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { createKranthoHandler } from '@krantho/server';
|
|
2
|
+
export { createContentLoader, createStudioServer, runPageRequest, verifyPreviewRequest, } from '@krantho/server';
|
|
3
|
+
export { toWebRequest } from './node-request.js';
|
|
4
|
+
export { writeResponseHeaders, writeWebResponse } from './node-response.js';
|
|
5
|
+
export { renderCachedPage } from './page-cache.js';
|
|
6
|
+
/**
|
|
7
|
+
* Create a raw handler for use with any Node.js HTTP server.
|
|
8
|
+
*
|
|
9
|
+
* Returns an object with `handle(request: Request): Promise<Response>`.
|
|
10
|
+
* The handler is initialized lazily on first request and reused (singleton).
|
|
11
|
+
*
|
|
12
|
+
* Use this when integrating with Express, Fastify, or a custom Node.js server.
|
|
13
|
+
* For Vite dev middleware, use `@krantho/server-node/vite` instead.
|
|
14
|
+
* For a standalone production server, use `@krantho/server-node/standalone`.
|
|
15
|
+
*/
|
|
16
|
+
export function createHandler(options) {
|
|
17
|
+
let handlerPromise;
|
|
18
|
+
function getHandler() {
|
|
19
|
+
if (!handlerPromise) {
|
|
20
|
+
handlerPromise = createKranthoHandler({ ...options, framework: 'node' });
|
|
21
|
+
}
|
|
22
|
+
return handlerPromise;
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
async handle(request) {
|
|
26
|
+
const handler = await getHandler();
|
|
27
|
+
return handler.handle(request);
|
|
28
|
+
},
|
|
29
|
+
close() {
|
|
30
|
+
if (handlerPromise) {
|
|
31
|
+
handlerPromise.then((h) => h.close()).catch(() => { });
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;AAStD,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,cAAc,EACd,oBAAoB,GACrB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAQ3E,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAGlD;;;;;;;;;GASG;AACH,MAAM,UAAU,aAAa,CAAC,OAA8B;IAC1D,IAAI,cAAmD,CAAA;IAEvD,SAAS,UAAU;QACjB,IAAI,CAAC,cAAc,EAAE,CAAC;YACpB,cAAc,GAAG,oBAAoB,CAAC,EAAE,GAAG,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC,CAAA;QAC1E,CAAC;QACD,OAAO,cAAc,CAAA;IACvB,CAAC;IAED,OAAO;QACL,KAAK,CAAC,MAAM,CAAC,OAAgB;YAC3B,MAAM,OAAO,GAAG,MAAM,UAAU,EAAE,CAAA;YAClC,OAAO,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;QAChC,CAAC;QACD,KAAK;YACH,IAAI,cAAc,EAAE,CAAC;gBACnB,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;YACvD,CAAC;QACH,CAAC;KACF,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node-request.d.ts","sourceRoot":"","sources":["../src/node-request.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAchD,yDAAyD;AACzD,wBAAsB,YAAY,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,OAAO,CAAC,CAsBzE"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const METHODS_WITH_BODY = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
|
|
2
|
+
/** Read the full request body from a Node.js IncomingMessage, byte for byte: an upload is not text */
|
|
3
|
+
function readBody(req) {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
const chunks = [];
|
|
6
|
+
req.on('data', (chunk) => chunks.push(chunk));
|
|
7
|
+
req.on('end', () => resolve(new Uint8Array(Buffer.concat(chunks))));
|
|
8
|
+
req.on('error', reject);
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
/** Convert a Node.js IncomingMessage to a Web Request */
|
|
12
|
+
export async function toWebRequest(req) {
|
|
13
|
+
const protocol = req.headers['x-forwarded-proto'] ?? 'http';
|
|
14
|
+
const host = req.headers.host ?? 'localhost';
|
|
15
|
+
const fullUrl = `${protocol}://${host}${req.url ?? '/'}`;
|
|
16
|
+
const headers = new Headers();
|
|
17
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
18
|
+
if (typeof value === 'string') {
|
|
19
|
+
headers.set(key, value);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const init = { method: req.method ?? 'GET', headers };
|
|
23
|
+
if (req.method && METHODS_WITH_BODY.has(req.method)) {
|
|
24
|
+
const body = await readBody(req);
|
|
25
|
+
if (body.byteLength > 0) {
|
|
26
|
+
init.body = body;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return new Request(fullUrl, init);
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=node-request.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node-request.js","sourceRoot":"","sources":["../src/node-request.ts"],"names":[],"mappings":"AAEA,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAA;AAErE,sGAAsG;AACtG,SAAS,QAAQ,CAAC,GAAoB;IACpC,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,MAAM,GAAa,EAAE,CAAA;QAC3B,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAA;QACrD,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAA;QACnE,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;IACzB,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,yDAAyD;AACzD,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,GAAoB;IACrD,MAAM,QAAQ,GAAG,GAAG,CAAC,OAAO,CAAC,mBAAmB,CAAC,IAAI,MAAM,CAAA;IAC3D,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,IAAI,WAAW,CAAA;IAC5C,MAAM,OAAO,GAAG,GAAG,QAAQ,MAAM,IAAI,GAAG,GAAG,CAAC,GAAG,IAAI,GAAG,EAAE,CAAA;IAExD,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAA;IAC7B,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;QACvD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YAC9B,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;QACzB,CAAC;IACH,CAAC;IAED,MAAM,IAAI,GAAgB,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,IAAI,KAAK,EAAE,OAAO,EAAE,CAAA;IAElE,IAAI,GAAG,CAAC,MAAM,IAAI,iBAAiB,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;QACpD,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,GAAG,CAAC,CAAA;QAChC,IAAI,IAAI,CAAC,UAAU,GAAG,CAAC,EAAE,CAAC;YACxB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;QAClB,CAAC;IACH,CAAC;IAED,OAAO,IAAI,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;AACnC,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ServerResponse } from 'node:http';
|
|
2
|
+
/**
|
|
3
|
+
* Copy a Web Response's headers onto a Node ServerResponse.
|
|
4
|
+
*
|
|
5
|
+
* Set-Cookie is emitted as one header line per cookie: iterating a Web `Headers`
|
|
6
|
+
* object folds same-name headers (including Set-Cookie) into a single comma-joined
|
|
7
|
+
* value, which RFC 6265 forbids. `getSetCookie()` recovers the distinct values, and
|
|
8
|
+
* `res.setHeader(name, string[])` writes one line per array element.
|
|
9
|
+
*/
|
|
10
|
+
export declare function writeResponseHeaders(res: ServerResponse, response: Response): void;
|
|
11
|
+
/**
|
|
12
|
+
* Write a Web Response to a Node response: headers (one Set-Cookie line per
|
|
13
|
+
* cookie), then the body chunk by chunk as it arrives, honouring backpressure.
|
|
14
|
+
* When the client goes away first, the body stream is cancelled; when the body
|
|
15
|
+
* fails after the headers were sent, the response is destroyed.
|
|
16
|
+
*/
|
|
17
|
+
export declare function writeWebResponse(res: ServerResponse, response: Response): Promise<void>;
|
|
18
|
+
//# sourceMappingURL=node-response.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node-response.d.ts","sourceRoot":"","sources":["../src/node-response.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAE/C;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,cAAc,EAAE,QAAQ,EAAE,QAAQ,GAAG,IAAI,CASlF;AAgBD;;;;;GAKG;AACH,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,cAAc,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CA4B7F"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copy a Web Response's headers onto a Node ServerResponse.
|
|
3
|
+
*
|
|
4
|
+
* Set-Cookie is emitted as one header line per cookie: iterating a Web `Headers`
|
|
5
|
+
* object folds same-name headers (including Set-Cookie) into a single comma-joined
|
|
6
|
+
* value, which RFC 6265 forbids. `getSetCookie()` recovers the distinct values, and
|
|
7
|
+
* `res.setHeader(name, string[])` writes one line per array element.
|
|
8
|
+
*/
|
|
9
|
+
export function writeResponseHeaders(res, response) {
|
|
10
|
+
for (const [key, value] of response.headers) {
|
|
11
|
+
if (key.toLowerCase() === 'set-cookie')
|
|
12
|
+
continue;
|
|
13
|
+
res.setHeader(key, value);
|
|
14
|
+
}
|
|
15
|
+
const cookies = response.headers.getSetCookie();
|
|
16
|
+
if (cookies.length > 0) {
|
|
17
|
+
res.setHeader('set-cookie', cookies);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** Write one chunk, waiting for `drain` when the socket's buffer is full */
|
|
21
|
+
function writeChunk(res, chunk) {
|
|
22
|
+
if (res.write(chunk))
|
|
23
|
+
return Promise.resolve();
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
const done = () => {
|
|
26
|
+
res.off('drain', done);
|
|
27
|
+
res.off('close', done);
|
|
28
|
+
resolve();
|
|
29
|
+
};
|
|
30
|
+
res.once('drain', done);
|
|
31
|
+
res.once('close', done);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Write a Web Response to a Node response: headers (one Set-Cookie line per
|
|
36
|
+
* cookie), then the body chunk by chunk as it arrives, honouring backpressure.
|
|
37
|
+
* When the client goes away first, the body stream is cancelled; when the body
|
|
38
|
+
* fails after the headers were sent, the response is destroyed.
|
|
39
|
+
*/
|
|
40
|
+
export async function writeWebResponse(res, response) {
|
|
41
|
+
res.statusCode = response.status;
|
|
42
|
+
writeResponseHeaders(res, response);
|
|
43
|
+
res.flushHeaders();
|
|
44
|
+
if (!response.body) {
|
|
45
|
+
res.end();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const reader = response.body.getReader();
|
|
49
|
+
const onClose = () => {
|
|
50
|
+
if (!res.writableFinished)
|
|
51
|
+
reader.cancel().catch(() => { });
|
|
52
|
+
};
|
|
53
|
+
res.once('close', onClose);
|
|
54
|
+
try {
|
|
55
|
+
for (;;) {
|
|
56
|
+
const { done, value } = await reader.read();
|
|
57
|
+
if (done)
|
|
58
|
+
break;
|
|
59
|
+
if (res.destroyed)
|
|
60
|
+
break;
|
|
61
|
+
await writeChunk(res, value);
|
|
62
|
+
}
|
|
63
|
+
if (!res.destroyed)
|
|
64
|
+
res.end();
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
res.destroy(err instanceof Error ? err : new Error(String(err)));
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
res.off('close', onClose);
|
|
71
|
+
reader.releaseLock();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=node-response.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node-response.js","sourceRoot":"","sources":["../src/node-response.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAAmB,EAAE,QAAkB;IAC1E,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,QAAQ,CAAC,OAAO,EAAE,CAAC;QAC5C,IAAI,GAAG,CAAC,WAAW,EAAE,KAAK,YAAY;YAAE,SAAQ;QAChD,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;IAC3B,CAAC;IACD,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC,YAAY,EAAE,CAAA;IAC/C,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,GAAG,CAAC,SAAS,CAAC,YAAY,EAAE,OAAO,CAAC,CAAA;IACtC,CAAC;AACH,CAAC;AAED,4EAA4E;AAC5E,SAAS,UAAU,CAAC,GAAmB,EAAE,KAAiB;IACxD,IAAI,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC;QAAE,OAAO,OAAO,CAAC,OAAO,EAAE,CAAA;IAC9C,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,MAAM,IAAI,GAAG,GAAS,EAAE;YACtB,GAAG,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;YACtB,GAAG,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;YACtB,OAAO,EAAE,CAAA;QACX,CAAC,CAAA;QACD,GAAG,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;QACvB,GAAG,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;IACzB,CAAC,CAAC,CAAA;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,GAAmB,EAAE,QAAkB;IAC5E,GAAG,CAAC,UAAU,GAAG,QAAQ,CAAC,MAAM,CAAA;IAChC,oBAAoB,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAA;IACnC,GAAG,CAAC,YAAY,EAAE,CAAA;IAClB,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;QACnB,GAAG,CAAC,GAAG,EAAE,CAAA;QACT,OAAM;IACR,CAAC;IAED,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,CAAA;IACxC,MAAM,OAAO,GAAG,GAAS,EAAE;QACzB,IAAI,CAAC,GAAG,CAAC,gBAAgB;YAAE,MAAM,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;IAC5D,CAAC,CAAA;IACD,GAAG,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;IAC1B,IAAI,CAAC;QACH,SAAS,CAAC;YACR,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAA;YAC3C,IAAI,IAAI;gBAAE,MAAK;YACf,IAAI,GAAG,CAAC,SAAS;gBAAE,MAAK;YACxB,MAAM,UAAU,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;QAC9B,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,SAAS;YAAE,GAAG,CAAC,GAAG,EAAE,CAAA;IAC/B,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACtB,GAAG,CAAC,OAAO,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;IAClE,CAAC;YAAS,CAAC;QACT,GAAG,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;QACzB,MAAM,CAAC,WAAW,EAAE,CAAA;IACtB,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { KranthoHandlerOptions } from '@krantho/server';
|
|
2
|
+
/** A page request as Express (or any Node server) sees it */
|
|
3
|
+
export interface PageRequestInfo {
|
|
4
|
+
/** The full request path with its query string (Express: `req.originalUrl`), or an absolute URL */
|
|
5
|
+
url: string;
|
|
6
|
+
method: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Response headers in Node's shape: what Express's `res.getHeaders()` returns,
|
|
10
|
+
* and what `res.setHeader(name, value)` takes (an `undefined` value is skipped).
|
|
11
|
+
*/
|
|
12
|
+
export type PageHeaders = Record<string, string | number | readonly string[] | undefined>;
|
|
13
|
+
/** The headers to send with a page, each ready for `res.setHeader(name, value)` */
|
|
14
|
+
export type CachedPageHeaders = Record<string, string | number | readonly string[]>;
|
|
15
|
+
/** What a server-side render produced */
|
|
16
|
+
export interface RenderedPage {
|
|
17
|
+
status: number;
|
|
18
|
+
html: string;
|
|
19
|
+
/**
|
|
20
|
+
* The page's own response headers — a `Set-Cookie` among them, or a
|
|
21
|
+
* `Cache-Control` saying `private`, `no-store` or `no-cache`, keeps the page
|
|
22
|
+
* off the CDN
|
|
23
|
+
*/
|
|
24
|
+
headers?: PageHeaders | undefined;
|
|
25
|
+
}
|
|
26
|
+
/** A rendered page and every header to send with it */
|
|
27
|
+
export interface CachedPage {
|
|
28
|
+
status: number;
|
|
29
|
+
html: string;
|
|
30
|
+
headers: CachedPageHeaders;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Render one page under the page cache rules.
|
|
34
|
+
*
|
|
35
|
+
* A `GET` or `HEAD` outside `/api` and `/studio` renders inside a page request
|
|
36
|
+
* scope, so the content loader records its tags. A verified signed preview
|
|
37
|
+
* link reads drafts and answers `private, no-store`, `noindex`, with no
|
|
38
|
+
* referrer. Under `cache.pages: 'cdn'` a published `200` or `404` whose
|
|
39
|
+
* headers set no cookie is cached at the CDN, tagged with every read that
|
|
40
|
+
* rendered it — unless its own `Cache-Control` says `private`, `no-store` or
|
|
41
|
+
* `no-cache`, which is then left as it is.
|
|
42
|
+
*
|
|
43
|
+
* The returned `headers` are the page's own merged with the cache headers,
|
|
44
|
+
* the cache headers winning on a name both set; a `Set-Cookie` is always
|
|
45
|
+
* passed through. Send every one:
|
|
46
|
+
*
|
|
47
|
+
* ```ts
|
|
48
|
+
* const page = await renderCachedPage(req, config, render)
|
|
49
|
+
* for (const [name, value] of Object.entries(page.headers)) res.setHeader(name, value)
|
|
50
|
+
* res.status(page.status).send(page.html)
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* Start every content read inside `render`: a read made before it, or one that
|
|
54
|
+
* outlives it, records no tags. Never read `loader.preview` outside `render` —
|
|
55
|
+
* that puts a draft into a page the CDN caches.
|
|
56
|
+
*/
|
|
57
|
+
export declare function renderCachedPage(req: PageRequestInfo, config: KranthoHandlerOptions['config'], render: () => Promise<RenderedPage>): Promise<CachedPage>;
|
|
58
|
+
//# sourceMappingURL=page-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page-cache.d.ts","sourceRoot":"","sources":["../src/page-cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAA;AAG5D,6DAA6D;AAC7D,MAAM,WAAW,eAAe;IAC9B,mGAAmG;IACnG,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;CACf;AAED;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC,CAAA;AAEzF,mFAAmF;AACnF,MAAM,MAAM,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC,CAAA;AAEnF,yCAAyC;AACzC,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ;;;;OAIG;IACH,OAAO,CAAC,EAAE,WAAW,GAAG,SAAS,CAAA;CAClC;AAED,uDAAuD;AACvD,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,iBAAiB,CAAA;CAC3B;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,eAAe,EACpB,MAAM,EAAE,qBAAqB,CAAC,QAAQ,CAAC,EACvC,MAAM,EAAE,MAAM,OAAO,CAAC,YAAY,CAAC,GAClC,OAAO,CAAC,UAAU,CAAC,CAUrB"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { servePageRequest } from '@krantho/server';
|
|
2
|
+
/**
|
|
3
|
+
* Render one page under the page cache rules.
|
|
4
|
+
*
|
|
5
|
+
* A `GET` or `HEAD` outside `/api` and `/studio` renders inside a page request
|
|
6
|
+
* scope, so the content loader records its tags. A verified signed preview
|
|
7
|
+
* link reads drafts and answers `private, no-store`, `noindex`, with no
|
|
8
|
+
* referrer. Under `cache.pages: 'cdn'` a published `200` or `404` whose
|
|
9
|
+
* headers set no cookie is cached at the CDN, tagged with every read that
|
|
10
|
+
* rendered it — unless its own `Cache-Control` says `private`, `no-store` or
|
|
11
|
+
* `no-cache`, which is then left as it is.
|
|
12
|
+
*
|
|
13
|
+
* The returned `headers` are the page's own merged with the cache headers,
|
|
14
|
+
* the cache headers winning on a name both set; a `Set-Cookie` is always
|
|
15
|
+
* passed through. Send every one:
|
|
16
|
+
*
|
|
17
|
+
* ```ts
|
|
18
|
+
* const page = await renderCachedPage(req, config, render)
|
|
19
|
+
* for (const [name, value] of Object.entries(page.headers)) res.setHeader(name, value)
|
|
20
|
+
* res.status(page.status).send(page.html)
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* Start every content read inside `render`: a read made before it, or one that
|
|
24
|
+
* outlives it, records no tags. Never read `loader.preview` outside `render` —
|
|
25
|
+
* that puts a draft into a page the CDN caches.
|
|
26
|
+
*/
|
|
27
|
+
export async function renderCachedPage(req, config, render) {
|
|
28
|
+
const { value, headers } = await servePageRequest(req, config, render, (page) => ({
|
|
29
|
+
status: page.status,
|
|
30
|
+
header: (name) => headerValue(page.headers ?? {}, name),
|
|
31
|
+
}));
|
|
32
|
+
return {
|
|
33
|
+
status: value.status,
|
|
34
|
+
html: value.html,
|
|
35
|
+
headers: mergeHeaders(value.headers ?? {}, headers),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** A header by name, in any case, several values joined with `, ` — null when absent */
|
|
39
|
+
function headerValue(headers, name) {
|
|
40
|
+
const wanted = name.toLowerCase();
|
|
41
|
+
const values = [];
|
|
42
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
43
|
+
if (value === undefined || key.toLowerCase() !== wanted)
|
|
44
|
+
continue;
|
|
45
|
+
if (typeof value === 'object')
|
|
46
|
+
values.push(...value);
|
|
47
|
+
else
|
|
48
|
+
values.push(String(value));
|
|
49
|
+
}
|
|
50
|
+
return values.length > 0 ? values.join(', ') : null;
|
|
51
|
+
}
|
|
52
|
+
/** The page's headers, less any the cache headers replace (by name, in any case), plus those */
|
|
53
|
+
function mergeHeaders(page, cache) {
|
|
54
|
+
const replaced = new Set(Object.keys(cache).map((name) => name.toLowerCase()));
|
|
55
|
+
const merged = {};
|
|
56
|
+
for (const [name, value] of Object.entries(page)) {
|
|
57
|
+
if (value === undefined || replaced.has(name.toLowerCase()))
|
|
58
|
+
continue;
|
|
59
|
+
merged[name] = value;
|
|
60
|
+
}
|
|
61
|
+
return { ...merged, ...cache };
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=page-cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page-cache.js","sourceRoot":"","sources":["../src/page-cache.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAqClD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,GAAoB,EACpB,MAAuC,EACvC,MAAmC;IAEnC,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,MAAM,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QAChF,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,OAAO,IAAI,EAAE,EAAE,IAAI,CAAC;KACxD,CAAC,CAAC,CAAA;IACH,OAAO;QACL,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,OAAO,EAAE,YAAY,CAAC,KAAK,CAAC,OAAO,IAAI,EAAE,EAAE,OAAO,CAAC;KACpD,CAAA;AACH,CAAC;AAED,wFAAwF;AACxF,SAAS,WAAW,CAAC,OAAoB,EAAE,IAAY;IACrD,MAAM,MAAM,GAAG,IAAI,CAAC,WAAW,EAAE,CAAA;IACjC,MAAM,MAAM,GAAa,EAAE,CAAA;IAC3B,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACnD,IAAI,KAAK,KAAK,SAAS,IAAI,GAAG,CAAC,WAAW,EAAE,KAAK,MAAM;YAAE,SAAQ;QACjE,IAAI,OAAO,KAAK,KAAK,QAAQ;YAAE,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,CAAA;;YAC/C,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAA;IACjC,CAAC;IACD,OAAO,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;AACrD,CAAC;AAED,gGAAgG;AAChG,SAAS,YAAY,CAAC,IAAiB,EAAE,KAA6B;IACpE,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAA;IAC9E,MAAM,MAAM,GAAsB,EAAE,CAAA;IACpC,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACjD,IAAI,KAAK,KAAK,SAAS,IAAI,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;YAAE,SAAQ;QACrE,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,CAAA;IACtB,CAAC;IACD,OAAO,EAAE,GAAG,MAAM,EAAE,GAAG,KAAK,EAAE,CAAA;AAChC,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { KranthoHandlerOptions } from '@krantho/server';
|
|
2
|
+
/** Options for the standalone production server */
|
|
3
|
+
export interface StandaloneOptions extends KranthoHandlerOptions {
|
|
4
|
+
/** Directory containing static files to serve (e.g., 'dist') */
|
|
5
|
+
staticDir?: string | undefined;
|
|
6
|
+
/** Port to listen on (default: 3000) */
|
|
7
|
+
port?: number | undefined;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Start a standalone HTTP server with API + optional static file serving.
|
|
11
|
+
*
|
|
12
|
+
* Requests to `/api/*` are routed to the krantho handler.
|
|
13
|
+
* All other requests serve static files from `staticDir` (if configured).
|
|
14
|
+
* Falls back to `index.html` for SPA client-side routing.
|
|
15
|
+
*/
|
|
16
|
+
export declare function startServer(options: StandaloneOptions): Promise<void>;
|
|
17
|
+
//# sourceMappingURL=standalone.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"standalone.d.ts","sourceRoot":"","sources":["../src/standalone.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAA;AAK5D,mDAAmD;AACnD,MAAM,WAAW,iBAAkB,SAAQ,qBAAqB;IAC9D,gEAAgE;IAChE,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC9B,wCAAwC;IACxC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAC1B;AAED;;;;;;GAMG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CA+C3E"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
2
|
+
import { createServer } from 'node:http';
|
|
3
|
+
import { extname, join, resolve, sep } from 'node:path';
|
|
4
|
+
import { createKranthoHandler, MIME_TYPES } from '@krantho/server';
|
|
5
|
+
import { toWebRequest } from './node-request.js';
|
|
6
|
+
import { writeWebResponse } from './node-response.js';
|
|
7
|
+
/**
|
|
8
|
+
* Start a standalone HTTP server with API + optional static file serving.
|
|
9
|
+
*
|
|
10
|
+
* Requests to `/api/*` are routed to the krantho handler.
|
|
11
|
+
* All other requests serve static files from `staticDir` (if configured).
|
|
12
|
+
* Falls back to `index.html` for SPA client-side routing.
|
|
13
|
+
*/
|
|
14
|
+
export async function startServer(options) {
|
|
15
|
+
const port = options.port ?? 3000;
|
|
16
|
+
const handler = await createKranthoHandler({ ...options, framework: 'node' });
|
|
17
|
+
const server = createServer(async (req, res) => {
|
|
18
|
+
const url = req.url ?? '/';
|
|
19
|
+
// API routes
|
|
20
|
+
if (url.startsWith('/api/')) {
|
|
21
|
+
try {
|
|
22
|
+
const webRequest = await toWebRequest(req);
|
|
23
|
+
const response = await handler.handle(webRequest);
|
|
24
|
+
await writeWebResponse(res, response);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
if (res.headersSent) {
|
|
28
|
+
res.destroy();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
res.statusCode = 500;
|
|
32
|
+
res.setHeader('content-type', 'application/json');
|
|
33
|
+
res.end(JSON.stringify({ error: 'Internal server error' }));
|
|
34
|
+
}
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
// Static files
|
|
38
|
+
if (options.staticDir) {
|
|
39
|
+
const served = await serveStatic(res, options.staticDir, url);
|
|
40
|
+
if (served)
|
|
41
|
+
return;
|
|
42
|
+
// SPA fallback — serve index.html for non-file paths
|
|
43
|
+
const indexServed = await serveStatic(res, options.staticDir, '/index.html');
|
|
44
|
+
if (indexServed)
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
res.statusCode = 404;
|
|
48
|
+
res.setHeader('content-type', 'text/plain');
|
|
49
|
+
res.end('Not Found');
|
|
50
|
+
});
|
|
51
|
+
server.listen(port, () => {
|
|
52
|
+
console.log(`krantho server listening on http://localhost:${port}`);
|
|
53
|
+
if (options.staticDir) {
|
|
54
|
+
console.log(` Static files: ${options.staticDir}`);
|
|
55
|
+
}
|
|
56
|
+
console.log(` API: http://localhost:${port}/api/`);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
async function serveStatic(res, staticDir, urlPath) {
|
|
60
|
+
const resolvedDir = resolve(staticDir);
|
|
61
|
+
const safeRoot = resolvedDir.endsWith(sep) ? resolvedDir : resolvedDir + sep;
|
|
62
|
+
const filePath = resolve(join(staticDir, urlPath));
|
|
63
|
+
// Prevent directory traversal — trailing separator prevents sibling-directory bypass
|
|
64
|
+
if (!filePath.startsWith(safeRoot) && filePath !== resolvedDir) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const stats = await stat(filePath);
|
|
69
|
+
if (!stats.isFile())
|
|
70
|
+
return false;
|
|
71
|
+
const content = await readFile(filePath);
|
|
72
|
+
const ext = extname(filePath);
|
|
73
|
+
const contentType = MIME_TYPES[ext] ?? 'application/octet-stream';
|
|
74
|
+
res.statusCode = 200;
|
|
75
|
+
res.setHeader('content-type', contentType);
|
|
76
|
+
res.end(content);
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=standalone.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"standalone.js","sourceRoot":"","sources":["../src/standalone.ts"],"names":[],"mappings":"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"}
|
package/dist/vite.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { KranthoHandlerOptions } from '@krantho/server';
|
|
2
|
+
import type { Plugin } from 'vite';
|
|
3
|
+
export type { KranthoHandlerOptions };
|
|
4
|
+
/**
|
|
5
|
+
* Vite plugin that adds krantho API middleware to the dev server.
|
|
6
|
+
*
|
|
7
|
+
* Intercepts requests matching `/api/*` and routes them through the
|
|
8
|
+
* krantho handler. All other requests pass through to Vite normally.
|
|
9
|
+
*/
|
|
10
|
+
export declare function kranthoPlugin(options: KranthoHandlerOptions): Plugin;
|
|
11
|
+
//# sourceMappingURL=vite.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["../src/vite.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAA;AAE5D,OAAO,KAAK,EAAE,MAAM,EAAiB,MAAM,MAAM,CAAA;AAIjD,YAAY,EAAE,qBAAqB,EAAE,CAAA;AAErC;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,qBAAqB,GAAG,MAAM,CAgEpE"}
|
package/dist/vite.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { createLazyHandler, createStudioServer } from '@krantho/server';
|
|
2
|
+
import { toWebRequest } from './node-request.js';
|
|
3
|
+
import { writeWebResponse } from './node-response.js';
|
|
4
|
+
/**
|
|
5
|
+
* Vite plugin that adds krantho API middleware to the dev server.
|
|
6
|
+
*
|
|
7
|
+
* Intercepts requests matching `/api/*` and routes them through the
|
|
8
|
+
* krantho handler. All other requests pass through to Vite normally.
|
|
9
|
+
*/
|
|
10
|
+
export function kranthoPlugin(options) {
|
|
11
|
+
const getHandler = createLazyHandler({ ...options, framework: 'node' });
|
|
12
|
+
const serveStudio = createStudioServer(options.config);
|
|
13
|
+
return {
|
|
14
|
+
name: 'krantho-api',
|
|
15
|
+
configureServer(server) {
|
|
16
|
+
// Serve Studio SPA
|
|
17
|
+
server.middlewares.use((req, res, next) => {
|
|
18
|
+
const url = req.url ?? '';
|
|
19
|
+
if (!url.startsWith('/studio')) {
|
|
20
|
+
next();
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
let response;
|
|
24
|
+
try {
|
|
25
|
+
response = serveStudio({
|
|
26
|
+
url: new URL(url, 'http://localhost'),
|
|
27
|
+
cookie: req.headers.cookie,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
next(err);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
res.statusCode = response.status;
|
|
35
|
+
for (const [key, value] of response.headers) {
|
|
36
|
+
res.setHeader(key, value);
|
|
37
|
+
}
|
|
38
|
+
response
|
|
39
|
+
.arrayBuffer()
|
|
40
|
+
.then((buf) => res.end(Buffer.from(buf)))
|
|
41
|
+
.catch(() => {
|
|
42
|
+
res.statusCode = 500;
|
|
43
|
+
res.end('Failed to read Studio asset');
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
// Serve krantho API
|
|
47
|
+
server.middlewares.use(async (req, res, next) => {
|
|
48
|
+
const url = req.url ?? '';
|
|
49
|
+
if (!url.startsWith('/api/')) {
|
|
50
|
+
next();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const handler = await getHandler();
|
|
55
|
+
const webRequest = await toWebRequest(req);
|
|
56
|
+
const response = await handler.handle(webRequest);
|
|
57
|
+
await writeWebResponse(res, response);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
if (res.headersSent) {
|
|
61
|
+
res.destroy();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
res.statusCode = 500;
|
|
65
|
+
res.setHeader('content-type', 'application/json');
|
|
66
|
+
res.end(JSON.stringify({ error: 'Internal server error' }));
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=vite.js.map
|
package/dist/vite.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vite.js","sourceRoot":"","sources":["../src/vite.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,56 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@krantho/server-node",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "The krantho adapter for Node.js, Express and Vite: a request handler, a Vite plugin and a standalone server.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://krantho.com",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/descoped/krantho.git",
|
|
10
|
+
"directory": "packages/server-node"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/descoped/krantho/issues"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"import": "./dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./vite": {
|
|
22
|
+
"types": "./dist/vite.d.ts",
|
|
23
|
+
"import": "./dist/vite.js"
|
|
24
|
+
},
|
|
25
|
+
"./standalone": {
|
|
26
|
+
"types": "./dist/standalone.d.ts",
|
|
27
|
+
"import": "./dist/standalone.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist"
|
|
32
|
+
],
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@krantho/server": "1.0.1"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"vite": ">=8.0.0"
|
|
38
|
+
},
|
|
39
|
+
"peerDependenciesMeta": {
|
|
40
|
+
"vite": {
|
|
41
|
+
"optional": true
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/node": "^25.9.9",
|
|
46
|
+
"typescript": "^6.0.3",
|
|
47
|
+
"vite": "^8.3.8",
|
|
48
|
+
"zod": "^4.6.7",
|
|
49
|
+
"@krantho/schema": "1.0.1"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "tsc",
|
|
53
|
+
"typecheck": "tsc --noEmit",
|
|
54
|
+
"clean": "rm -rf dist"
|
|
55
|
+
}
|
|
6
56
|
}
|
package/README.md
DELETED