@pracht/adapter-node 0.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/dist/index.d.mts +39 -0
- package/dist/index.mjs +204 -0
- package/package.json +20 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ISGManifestEntry, ModuleRegistry, PrachtApp, ResolvedApiRoute } from "@pracht/core";
|
|
2
|
+
import { IncomingMessage, ServerResponse } from "node:http";
|
|
3
|
+
import { PrachtAdapter } from "@pracht/vite-plugin";
|
|
4
|
+
|
|
5
|
+
//#region src/index.d.ts
|
|
6
|
+
interface NodeAdapterContextArgs {
|
|
7
|
+
request: Request;
|
|
8
|
+
req: IncomingMessage;
|
|
9
|
+
res: ServerResponse;
|
|
10
|
+
}
|
|
11
|
+
interface NodeAdapterOptions<TContext = unknown> {
|
|
12
|
+
app: PrachtApp;
|
|
13
|
+
registry?: ModuleRegistry;
|
|
14
|
+
staticDir?: string;
|
|
15
|
+
viteManifest?: unknown;
|
|
16
|
+
isgManifest?: Record<string, ISGManifestEntry>;
|
|
17
|
+
apiRoutes?: ResolvedApiRoute[];
|
|
18
|
+
clientEntryUrl?: string;
|
|
19
|
+
cssUrls?: string[];
|
|
20
|
+
cssManifest?: Record<string, string[]>;
|
|
21
|
+
jsManifest?: Record<string, string[]>;
|
|
22
|
+
createContext?: (args: NodeAdapterContextArgs) => TContext | Promise<TContext>;
|
|
23
|
+
}
|
|
24
|
+
interface NodeServerEntryModuleOptions {
|
|
25
|
+
port?: number;
|
|
26
|
+
}
|
|
27
|
+
declare function createNodeRequestHandler<TContext = unknown>(options: NodeAdapterOptions<TContext>): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
28
|
+
declare function createNodeServerEntryModule(options?: NodeServerEntryModuleOptions): string;
|
|
29
|
+
/**
|
|
30
|
+
* Create a pracht adapter for Node.js.
|
|
31
|
+
*
|
|
32
|
+
* ```ts
|
|
33
|
+
* import { nodeAdapter } from "@pracht/adapter-node";
|
|
34
|
+
* pracht({ adapter: nodeAdapter() })
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
declare function nodeAdapter(options?: NodeServerEntryModuleOptions): PrachtAdapter;
|
|
38
|
+
//#endregion
|
|
39
|
+
export { NodeAdapterContextArgs, NodeAdapterOptions, NodeServerEntryModuleOptions, createNodeRequestHandler, createNodeServerEntryModule, nodeAdapter };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { applyDefaultSecurityHeaders, handlePrachtRequest } from "@pracht/core";
|
|
5
|
+
//#region src/index.ts
|
|
6
|
+
function createNodeRequestHandler(options) {
|
|
7
|
+
const isgManifest = options.isgManifest ?? {};
|
|
8
|
+
const staticDir = options.staticDir;
|
|
9
|
+
return async (req, res) => {
|
|
10
|
+
let request;
|
|
11
|
+
try {
|
|
12
|
+
request = await createWebRequest(req);
|
|
13
|
+
} catch (err) {
|
|
14
|
+
if (err instanceof Error && err.message === "Request body too large") {
|
|
15
|
+
res.statusCode = 413;
|
|
16
|
+
res.end("Payload Too Large");
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
throw err;
|
|
20
|
+
}
|
|
21
|
+
const url = new URL(request.url);
|
|
22
|
+
if (staticDir && request.method === "GET" && url.pathname in isgManifest) {
|
|
23
|
+
const entry = isgManifest[url.pathname];
|
|
24
|
+
const htmlPath = url.pathname === "/" ? join(staticDir, "index.html") : join(staticDir, url.pathname, "index.html");
|
|
25
|
+
if (existsSync(htmlPath)) {
|
|
26
|
+
const stat = statSync(htmlPath);
|
|
27
|
+
const ageMs = Date.now() - stat.mtimeMs;
|
|
28
|
+
const isStale = entry.revalidate.kind === "time" && ageMs > entry.revalidate.seconds * 1e3;
|
|
29
|
+
const html = await readFile(htmlPath, "utf-8");
|
|
30
|
+
res.statusCode = 200;
|
|
31
|
+
applyDefaultSecurityHeaders(new Headers({
|
|
32
|
+
"content-type": "text/html; charset=utf-8",
|
|
33
|
+
"x-pracht-isg": isStale ? "stale" : "fresh"
|
|
34
|
+
})).forEach((value, key) => {
|
|
35
|
+
res.setHeader(key, value);
|
|
36
|
+
});
|
|
37
|
+
res.end(html);
|
|
38
|
+
if (isStale) regenerateISGPage(options, url.pathname, htmlPath).catch((err) => {
|
|
39
|
+
console.error(`ISG regeneration failed for ${url.pathname}:`, err);
|
|
40
|
+
});
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const context = options.createContext ? await options.createContext({
|
|
45
|
+
request,
|
|
46
|
+
req,
|
|
47
|
+
res
|
|
48
|
+
}) : void 0;
|
|
49
|
+
const response = await handlePrachtRequest({
|
|
50
|
+
app: options.app,
|
|
51
|
+
context,
|
|
52
|
+
registry: options.registry,
|
|
53
|
+
request,
|
|
54
|
+
apiRoutes: options.apiRoutes,
|
|
55
|
+
clientEntryUrl: options.clientEntryUrl,
|
|
56
|
+
cssManifest: options.cssManifest,
|
|
57
|
+
jsManifest: options.jsManifest
|
|
58
|
+
});
|
|
59
|
+
if (staticDir && request.method === "GET" && url.pathname in isgManifest && response.status === 200) {
|
|
60
|
+
const html = await response.clone().text();
|
|
61
|
+
const htmlPath = url.pathname === "/" ? join(staticDir, "index.html") : join(staticDir, url.pathname, "index.html");
|
|
62
|
+
mkdirSync(dirname(htmlPath), { recursive: true });
|
|
63
|
+
writeFileSync(htmlPath, html, "utf-8");
|
|
64
|
+
}
|
|
65
|
+
await writeWebResponse(res, response);
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
async function regenerateISGPage(options, pathname, htmlPath) {
|
|
69
|
+
const url = new URL(pathname, "http://localhost");
|
|
70
|
+
const request = new Request(url, { method: "GET" });
|
|
71
|
+
const response = await handlePrachtRequest({
|
|
72
|
+
app: options.app,
|
|
73
|
+
registry: options.registry,
|
|
74
|
+
request,
|
|
75
|
+
clientEntryUrl: options.clientEntryUrl,
|
|
76
|
+
cssManifest: options.cssManifest,
|
|
77
|
+
jsManifest: options.jsManifest
|
|
78
|
+
});
|
|
79
|
+
if (response.status === 200) {
|
|
80
|
+
const html = await response.text();
|
|
81
|
+
mkdirSync(dirname(htmlPath), { recursive: true });
|
|
82
|
+
writeFileSync(htmlPath, html, "utf-8");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function createNodeServerEntryModule(options = {}) {
|
|
86
|
+
return [
|
|
87
|
+
"import { existsSync, readFileSync } from \"node:fs\";",
|
|
88
|
+
"import { createServer } from \"node:http\";",
|
|
89
|
+
"import { dirname, resolve } from \"node:path\";",
|
|
90
|
+
"import { fileURLToPath, pathToFileURL } from \"node:url\";",
|
|
91
|
+
"import { createNodeRequestHandler } from \"@pracht/adapter-node\";",
|
|
92
|
+
"",
|
|
93
|
+
"const serverDir = dirname(fileURLToPath(import.meta.url));",
|
|
94
|
+
"const staticDir = resolve(serverDir, \"../client\");",
|
|
95
|
+
"const isgManifestPath = resolve(serverDir, \"isg-manifest.json\");",
|
|
96
|
+
"const isgManifest = existsSync(isgManifestPath)",
|
|
97
|
+
" ? JSON.parse(readFileSync(isgManifestPath, \"utf-8\"))",
|
|
98
|
+
" : {};",
|
|
99
|
+
"",
|
|
100
|
+
"export const handler = createNodeRequestHandler({",
|
|
101
|
+
" app: resolvedApp,",
|
|
102
|
+
" registry,",
|
|
103
|
+
" staticDir,",
|
|
104
|
+
" isgManifest,",
|
|
105
|
+
" apiRoutes,",
|
|
106
|
+
" clientEntryUrl: clientEntryUrl ?? undefined,",
|
|
107
|
+
" cssManifest,",
|
|
108
|
+
" jsManifest,",
|
|
109
|
+
"});",
|
|
110
|
+
"",
|
|
111
|
+
"const entryHref = process.argv[1] ? pathToFileURL(process.argv[1]).href : null;",
|
|
112
|
+
"if (entryHref && import.meta.url === entryHref) {",
|
|
113
|
+
" const server = createServer(handler);",
|
|
114
|
+
` const port = Number(process.env.PORT ?? ${options.port ?? 3e3});`,
|
|
115
|
+
" server.listen(port, () => {",
|
|
116
|
+
" console.log(`pracht node server listening on http://localhost:${port}`);",
|
|
117
|
+
" });",
|
|
118
|
+
"}",
|
|
119
|
+
""
|
|
120
|
+
].join("\n");
|
|
121
|
+
}
|
|
122
|
+
async function createWebRequest(req) {
|
|
123
|
+
const protocol = getFirstHeaderValue(req.headers["x-forwarded-proto"]) ?? "http";
|
|
124
|
+
const host = getFirstHeaderValue(req.headers.host) ?? "localhost";
|
|
125
|
+
const url = new URL(req.url ?? "/", `${protocol}://${host}`);
|
|
126
|
+
const method = req.method ?? "GET";
|
|
127
|
+
const init = {
|
|
128
|
+
headers: createHeaders(req.headers),
|
|
129
|
+
method
|
|
130
|
+
};
|
|
131
|
+
if (!BODYLESS_METHODS.has(method.toUpperCase())) {
|
|
132
|
+
const body = await readRequestBody(req);
|
|
133
|
+
if (body.byteLength > 0) {
|
|
134
|
+
const exactBody = new Uint8Array(body.byteLength);
|
|
135
|
+
exactBody.set(body);
|
|
136
|
+
init.body = exactBody.buffer;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return new Request(url, init);
|
|
140
|
+
}
|
|
141
|
+
function createHeaders(headers) {
|
|
142
|
+
const result = new Headers();
|
|
143
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
144
|
+
if (typeof value === "undefined") continue;
|
|
145
|
+
if (Array.isArray(value)) {
|
|
146
|
+
for (const entry of value) result.append(key, entry);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
result.set(key, value);
|
|
150
|
+
}
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
const MAX_BODY_SIZE = 1024 * 1024;
|
|
154
|
+
async function readRequestBody(req) {
|
|
155
|
+
const chunks = [];
|
|
156
|
+
let totalSize = 0;
|
|
157
|
+
for await (const chunk of req) {
|
|
158
|
+
const buf = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
159
|
+
totalSize += buf.byteLength;
|
|
160
|
+
if (totalSize > MAX_BODY_SIZE) {
|
|
161
|
+
req.destroy();
|
|
162
|
+
throw new Error("Request body too large");
|
|
163
|
+
}
|
|
164
|
+
chunks.push(buf);
|
|
165
|
+
}
|
|
166
|
+
return Buffer.concat(chunks);
|
|
167
|
+
}
|
|
168
|
+
async function writeWebResponse(res, response) {
|
|
169
|
+
res.statusCode = response.status;
|
|
170
|
+
res.statusMessage = response.statusText;
|
|
171
|
+
response.headers.forEach((value, key) => {
|
|
172
|
+
res.setHeader(key, value);
|
|
173
|
+
});
|
|
174
|
+
if (!response.body) {
|
|
175
|
+
res.end();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const body = Buffer.from(await response.arrayBuffer());
|
|
179
|
+
res.end(body);
|
|
180
|
+
}
|
|
181
|
+
function getFirstHeaderValue(value) {
|
|
182
|
+
if (Array.isArray(value)) return value[0];
|
|
183
|
+
return value;
|
|
184
|
+
}
|
|
185
|
+
const BODYLESS_METHODS = new Set(["GET", "HEAD"]);
|
|
186
|
+
/**
|
|
187
|
+
* Create a pracht adapter for Node.js.
|
|
188
|
+
*
|
|
189
|
+
* ```ts
|
|
190
|
+
* import { nodeAdapter } from "@pracht/adapter-node";
|
|
191
|
+
* pracht({ adapter: nodeAdapter() })
|
|
192
|
+
* ```
|
|
193
|
+
*/
|
|
194
|
+
function nodeAdapter(options = {}) {
|
|
195
|
+
return {
|
|
196
|
+
id: "node",
|
|
197
|
+
serverImports: "import { resolveApp, resolveApiRoutes } from \"@pracht/core\";",
|
|
198
|
+
createServerEntryModule() {
|
|
199
|
+
return createNodeServerEntryModule(options);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
//#endregion
|
|
204
|
+
export { createNodeRequestHandler, createNodeServerEntryModule, nodeAdapter };
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pracht/adapter-node",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"files": [
|
|
5
|
+
"dist"
|
|
6
|
+
],
|
|
7
|
+
"type": "module",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.mts",
|
|
11
|
+
"default": "./dist/index.mjs"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@pracht/core": "0.0.0"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsdown"
|
|
19
|
+
}
|
|
20
|
+
}
|