@tschk/moonshine-deploy-node 0.4.1 → 0.4.4
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/README.md +7 -0
- package/package.json +13 -7
- package/src/index.ts +48 -91
package/README.md
CHANGED
|
@@ -11,6 +11,13 @@ await nodeAdapter.build(manifest, ".moonshine");
|
|
|
11
11
|
- Target runtime: `node`.
|
|
12
12
|
- Capabilities: `streaming`, `islands`, `revalidation`.
|
|
13
13
|
- Output: `server.ts` using `node:http` and a `Request` / `Response` bridge.
|
|
14
|
+
- Native builds also emit a bundled `server.js`; deploy it beside `public/` and run
|
|
15
|
+
`node server.js`. The bundle includes its manifest and needs no runtime packages.
|
|
16
|
+
This path is verified on Node 22. Building still requires Bun.
|
|
17
|
+
|
|
18
|
+
Create an independent native app with `moonshine new app --adapter node`, then
|
|
19
|
+
run `bun install`, `bun run build`, and `bun run start`. Native Node supports SSR,
|
|
20
|
+
static rendering, and explicit SPA clients; hydration and streaming are unfinished.
|
|
14
21
|
|
|
15
22
|
See [docs/DEPLOYMENT.md](../../docs/DEPLOYMENT.md) and
|
|
16
23
|
[docs/MANIFEST.md](../../docs/MANIFEST.md).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tschk/moonshine-deploy-node",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"license": "ISC",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -15,18 +15,24 @@
|
|
|
15
15
|
"test": "bun test"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@tschk/moonshine-
|
|
19
|
-
"@tschk/moonshine-
|
|
20
|
-
"@tschk/moonshine-react": "^0.4.1",
|
|
21
|
-
"@tschk/moonshine-server": "^0.4.1"
|
|
18
|
+
"@tschk/moonshine-framework": "^0.4.4",
|
|
19
|
+
"@tschk/moonshine-server": "^0.4.4"
|
|
22
20
|
},
|
|
23
21
|
"devDependencies": {
|
|
24
22
|
"@types/bun": "1.3.14",
|
|
25
23
|
"@types/node": "^22.0.0",
|
|
26
24
|
"typescript": "7.0.2",
|
|
27
|
-
"@tschk/moonshine-adapter-conformance": "^0.4.
|
|
25
|
+
"@tschk/moonshine-adapter-conformance": "^0.4.4"
|
|
28
26
|
},
|
|
29
27
|
"files": [
|
|
30
28
|
"src"
|
|
31
|
-
]
|
|
29
|
+
],
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"@tschk/moonshine-react": "^0.4.4"
|
|
32
|
+
},
|
|
33
|
+
"peerDependenciesMeta": {
|
|
34
|
+
"@tschk/moonshine-react": {
|
|
35
|
+
"optional": true
|
|
36
|
+
}
|
|
37
|
+
}
|
|
32
38
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,23 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { copyFile, mkdir, stat, writeFile } from "node:fs/promises";
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
2
|
import {
|
|
4
3
|
createServer,
|
|
5
4
|
type IncomingMessage,
|
|
6
5
|
type ServerResponse,
|
|
7
6
|
} from "node:http";
|
|
8
|
-
import {
|
|
7
|
+
import { resolve } from "node:path";
|
|
9
8
|
import { Readable } from "node:stream";
|
|
10
9
|
import {
|
|
11
10
|
type DeploymentAdapter,
|
|
12
11
|
type MoonshineManifest,
|
|
13
12
|
type Harness,
|
|
14
13
|
type HarnessFactory,
|
|
14
|
+
copyAsset,
|
|
15
|
+
validateRuntime,
|
|
15
16
|
} from "@tschk/moonshine-framework";
|
|
16
|
-
import {
|
|
17
|
-
isContained,
|
|
18
|
-
resolveStaticPath,
|
|
19
|
-
staticFileHeaders,
|
|
20
|
-
} from "@tschk/moonshine-server";
|
|
17
|
+
import { tryServeStatic } from "@tschk/moonshine-server";
|
|
21
18
|
|
|
22
19
|
const HOP_BY_HOP = new Set([
|
|
23
20
|
"connection",
|
|
@@ -35,67 +32,17 @@ function isHopByHopHeader(key: string): boolean {
|
|
|
35
32
|
return HOP_BY_HOP.has(key.toLowerCase());
|
|
36
33
|
}
|
|
37
34
|
|
|
38
|
-
const MIME: Record<string, string> = {
|
|
39
|
-
".css": "text/css; charset=utf-8",
|
|
40
|
-
".gif": "image/gif",
|
|
41
|
-
".html": "text/html; charset=utf-8",
|
|
42
|
-
".ico": "image/x-icon",
|
|
43
|
-
".jpeg": "image/jpeg",
|
|
44
|
-
".jpg": "image/jpeg",
|
|
45
|
-
".js": "text/javascript; charset=utf-8",
|
|
46
|
-
".json": "application/json; charset=utf-8",
|
|
47
|
-
".map": "application/json; charset=utf-8",
|
|
48
|
-
".mjs": "text/javascript; charset=utf-8",
|
|
49
|
-
".png": "image/png",
|
|
50
|
-
".svg": "image/svg+xml",
|
|
51
|
-
".txt": "text/plain; charset=utf-8",
|
|
52
|
-
".wasm": "application/wasm",
|
|
53
|
-
".webp": "image/webp",
|
|
54
|
-
".woff": "font/woff",
|
|
55
|
-
".woff2": "font/woff2",
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
function extOf(path: string): string {
|
|
59
|
-
const i = path.lastIndexOf(".");
|
|
60
|
-
return i >= 0 ? path.slice(i).toLowerCase() : "";
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async function tryServeNodeStatic(
|
|
64
|
-
staticDir: string,
|
|
65
|
-
pathname: string,
|
|
66
|
-
): Promise<Response | null> {
|
|
67
|
-
const filePath = resolveStaticPath(staticDir, pathname);
|
|
68
|
-
if (!filePath) return null;
|
|
69
|
-
let stats = null;
|
|
70
|
-
try {
|
|
71
|
-
stats = await stat(filePath);
|
|
72
|
-
} catch {
|
|
73
|
-
return null;
|
|
74
|
-
}
|
|
75
|
-
if (!stats.isFile()) return null;
|
|
76
|
-
try {
|
|
77
|
-
if (!isContained(staticDir, filePath)) return null;
|
|
78
|
-
} catch {
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
const type = MIME[extOf(filePath)] ?? "application/octet-stream";
|
|
82
|
-
const stream = createReadStream(filePath);
|
|
83
|
-
const headers = staticFileHeaders(type);
|
|
84
|
-
headers.set("content-length", String(stats.size));
|
|
85
|
-
return new Response(Readable.toWeb(stream) as unknown as ReadableStream, {
|
|
86
|
-
headers,
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
|
|
90
35
|
async function sendNodeResponse(
|
|
91
36
|
res: ServerResponse,
|
|
92
37
|
response: Response,
|
|
93
38
|
): Promise<void> {
|
|
94
|
-
const headers: Record<string, string> = {};
|
|
39
|
+
const headers: Record<string, string | string[]> = {};
|
|
95
40
|
for (const [key, value] of response.headers.entries()) {
|
|
96
|
-
if (isHopByHopHeader(key)) continue;
|
|
41
|
+
if (isHopByHopHeader(key) || key.toLowerCase() === "set-cookie") continue;
|
|
97
42
|
headers[key] = value;
|
|
98
43
|
}
|
|
44
|
+
const cookies = response.headers.getSetCookie();
|
|
45
|
+
if (cookies.length > 0) headers["set-cookie"] = cookies;
|
|
99
46
|
res.writeHead(response.status, headers);
|
|
100
47
|
if (!response.body) {
|
|
101
48
|
res.end();
|
|
@@ -142,7 +89,12 @@ export function createNodeHandler(
|
|
|
142
89
|
(method === "GET" || method === "HEAD") &&
|
|
143
90
|
pathname !== "/"
|
|
144
91
|
) {
|
|
145
|
-
const staticRes = await
|
|
92
|
+
const staticRes = await tryServeStatic(
|
|
93
|
+
options.staticDir,
|
|
94
|
+
pathname,
|
|
95
|
+
undefined,
|
|
96
|
+
method,
|
|
97
|
+
);
|
|
146
98
|
if (staticRes) {
|
|
147
99
|
await sendNodeResponse(res, staticRes);
|
|
148
100
|
return;
|
|
@@ -224,29 +176,12 @@ export const nodeHarness: HarnessFactory = (fetch, options) => {
|
|
|
224
176
|
} as Harness;
|
|
225
177
|
};
|
|
226
178
|
|
|
227
|
-
async function copyAsset(
|
|
228
|
-
asset: MoonshineManifest["assets"][number],
|
|
229
|
-
outDir: string,
|
|
230
|
-
): Promise<string> {
|
|
231
|
-
const source = isAbsolute(asset.file)
|
|
232
|
-
? asset.file
|
|
233
|
-
: resolve(outDir, asset.file);
|
|
234
|
-
const target = resolve(
|
|
235
|
-
outDir,
|
|
236
|
-
(asset.path ?? asset.file).replace(/^\/+/, ""),
|
|
237
|
-
);
|
|
238
|
-
await mkdir(dirname(target), { recursive: true });
|
|
239
|
-
if (source !== target) {
|
|
240
|
-
await copyFile(source, target);
|
|
241
|
-
}
|
|
242
|
-
return (asset.path ?? asset.file).replace(/^\/+/, "");
|
|
243
|
-
}
|
|
244
|
-
|
|
245
179
|
export const nodeAdapter: DeploymentAdapter = {
|
|
246
180
|
name: "node",
|
|
247
181
|
runtimes: ["node"],
|
|
248
182
|
capabilities: ["streaming", "islands", "revalidation"],
|
|
249
183
|
build: async (manifest, outDir) => {
|
|
184
|
+
validateRuntime(nodeAdapter, manifest);
|
|
250
185
|
await mkdir(outDir, { recursive: true });
|
|
251
186
|
const assets = await Promise.all(
|
|
252
187
|
manifest.assets.map(async (asset) => {
|
|
@@ -259,15 +194,21 @@ export const nodeAdapter: DeploymentAdapter = {
|
|
|
259
194
|
resolve(outDir, "manifest.json"),
|
|
260
195
|
JSON.stringify(builtManifest, null, 2) + "\n",
|
|
261
196
|
);
|
|
197
|
+
const native = manifest.renderer === "native";
|
|
198
|
+
const rendererImport = native
|
|
199
|
+
? 'import { modules, renderer } from "./dist/server.js";'
|
|
200
|
+
: 'import { modules } from "./dist/server.js";\nimport { reactRenderer as renderer } from "@tschk/moonshine-react";';
|
|
262
201
|
const entry = `import { createServer } from "node:http";
|
|
263
202
|
import { createRequestHandler } from "@tschk/moonshine-server";
|
|
264
|
-
import { reactRenderer } from "@tschk/moonshine-react";
|
|
265
203
|
import { createNodeHandler } from "@tschk/moonshine-deploy-node";
|
|
266
|
-
import { resolve } from "node:path";
|
|
204
|
+
import { dirname, resolve } from "node:path";
|
|
205
|
+
import { fileURLToPath } from "node:url";
|
|
267
206
|
import manifest from "./manifest.json" with { type: "json" };
|
|
268
|
-
|
|
207
|
+
${rendererImport}
|
|
208
|
+
${manifest.requestContext ? 'import { requestScope } from "./dist/server.js";' : ""}
|
|
269
209
|
|
|
270
|
-
const
|
|
210
|
+
const artifactDir = dirname(fileURLToPath(import.meta.url));
|
|
211
|
+
const projectDir = resolve(artifactDir, "..");
|
|
271
212
|
const abs = (p) => (p ? resolve(projectDir, p) : undefined);
|
|
272
213
|
const resolvedManifest = {
|
|
273
214
|
...manifest,
|
|
@@ -282,17 +223,33 @@ const resolvedManifest = {
|
|
|
282
223
|
};
|
|
283
224
|
|
|
284
225
|
const fetch = createRequestHandler({
|
|
285
|
-
manifest: resolvedManifest,
|
|
226
|
+
manifest: ${native ? "manifest" : "resolvedManifest"},
|
|
286
227
|
modules,
|
|
287
|
-
renderer:
|
|
288
|
-
staticDir:
|
|
228
|
+
renderer,${manifest.requestContext ? "\n requestScope," : ""}
|
|
229
|
+
staticDir: artifactDir + "/public",
|
|
289
230
|
});
|
|
290
|
-
const server = createServer(createNodeHandler({ fetch
|
|
291
|
-
|
|
231
|
+
export const server = createServer(createNodeHandler({ fetch }));
|
|
232
|
+
const envPort = Number(process.env.PORT);
|
|
233
|
+
server.listen(Number.isInteger(envPort) && envPort >= 0 && envPort <= 65535 ? envPort : 0, () => {
|
|
292
234
|
const address = server.address();
|
|
293
235
|
const port = typeof address === "object" && address ? address.port : 0;
|
|
236
|
+
console.log("http://localhost:" + port + "/");
|
|
294
237
|
});
|
|
295
238
|
`;
|
|
296
|
-
|
|
239
|
+
const entryPath = resolve(outDir, "server.ts");
|
|
240
|
+
await writeFile(entryPath, entry);
|
|
241
|
+
if (native) {
|
|
242
|
+
const result = await Bun.build({
|
|
243
|
+
entrypoints: [entryPath],
|
|
244
|
+
outdir: outDir,
|
|
245
|
+
target: "node",
|
|
246
|
+
packages: "bundle",
|
|
247
|
+
});
|
|
248
|
+
if (!result.success)
|
|
249
|
+
throw new AggregateError(
|
|
250
|
+
result.logs,
|
|
251
|
+
"Native Node server bundle failed",
|
|
252
|
+
);
|
|
253
|
+
}
|
|
297
254
|
},
|
|
298
255
|
};
|