@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.
Files changed (3) hide show
  1. package/README.md +7 -0
  2. package/package.json +13 -7
  3. 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.1",
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-compiler": "^0.4.1",
19
- "@tschk/moonshine-framework": "^0.4.1",
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.1"
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 { createReadStream } from "node:fs";
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 { dirname, isAbsolute, resolve } from "node:path";
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 tryServeNodeStatic(options.staticDir, pathname);
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
- import { modules } from "./dist/server.js";
207
+ ${rendererImport}
208
+ ${manifest.requestContext ? 'import { requestScope } from "./dist/server.js";' : ""}
269
209
 
270
- const projectDir = resolve(import.meta.dir, "..");
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: reactRenderer,
288
- staticDir: import.meta.dir + "/public",
228
+ renderer,${manifest.requestContext ? "\n requestScope," : ""}
229
+ staticDir: artifactDir + "/public",
289
230
  });
290
- const server = createServer(createNodeHandler({ fetch, staticDir: import.meta.dir + "/public" }));
291
- server.listen(Number(process.env.PORT) || 0, () => {
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
- await writeFile(resolve(outDir, "server.ts"), entry);
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
  };