@tschk/moonshine-deploy-cloudflare 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 +26 -0
  2. package/package.json +14 -8
  3. package/src/index.ts +91 -88
package/README.md CHANGED
@@ -18,3 +18,29 @@ Cache API limits apply.
18
18
 
19
19
  See [docs/DEPLOYMENT.md](../../docs/DEPLOYMENT.md) and
20
20
  [docs/MANIFEST.md](../../docs/MANIFEST.md).
21
+
22
+ Native apps use `renderer: "native"`, `runtime: "cloudflare"`, and
23
+ `adapter: "cloudflare"`. The adapter emits a bundled `server.js`, public assets,
24
+ and `wrangler.jsonc`. Run the generated artifact locally with an installed
25
+ Wrangler:
26
+
27
+ ```bash
28
+ wrangler dev --local --config .moonshine/wrangler.jsonc
29
+ ```
30
+
31
+ The native worker uses the bundled module table and independent renderer, without
32
+ React, Solid, a global module registry, or source-file imports. SSR, static output,
33
+ API routes and SPA routes with explicit `clientFile` use this path. Public files
34
+ are served through the `ASSETS` binding. The generated config invokes the worker
35
+ first, enables Node compatibility for shared server imports, and uses the build
36
+ date as its compatibility date. A recent Wrangler/workerd is required; older local
37
+ runtimes may require an isolated test configuration with an older supported date.
38
+
39
+ Native hydration/islands, opt-in request context, and route caching/revalidation
40
+ are rejected explicitly. The legacy `cloudflareFetch` helper and React renderer
41
+ remain available for existing apps; native apps use the generated worker.
42
+
43
+ The optional native artifact test discovers an existing Wrangler/Miniflare
44
+ installation and runs real workerd. It skips when that tool is unavailable and
45
+ does not install it. The installed 2026-06-16 emulator baseline was verified with
46
+ explicit Node compatibility; this does not establish production deployment.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tschk/moonshine-deploy-cloudflare",
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-router": "^0.4.1",
22
- "@tschk/moonshine-server": "^0.4.1"
18
+ "@tschk/moonshine-framework": "^0.4.4",
19
+ "@tschk/moonshine-router": "^0.4.4",
20
+ "@tschk/moonshine-server": "^0.4.4"
23
21
  },
24
22
  "devDependencies": {
25
23
  "@types/bun": "1.3.14",
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,17 +1,20 @@
1
- import { copyFile, mkdir, writeFile } from "node:fs/promises";
2
- import { dirname, isAbsolute, resolve } from "node:path";
1
+ import { writeFile, mkdir } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
3
  import {
4
4
  createRequestHandler,
5
5
  tryServeStatic,
6
6
  type RouteModule,
7
7
  } from "@tschk/moonshine-server";
8
- import { matchRoutes, createRouteGraph } from "@tschk/moonshine-router";
9
- import { reactRenderer, registerRouteModules } from "@tschk/moonshine-react";
10
- import type {
11
- DeploymentAdapter,
12
- MoonshineManifest,
13
- Harness,
14
- HarnessFactory,
8
+ import { matchRoutes, createRouteGraph } from "@tschk/moonshine-router/graph";
9
+ import {
10
+ type DeploymentAdapter,
11
+ type MoonshineManifest,
12
+ type Harness,
13
+ type HarnessFactory,
14
+ resolveManifestFiles,
15
+ copyAssets,
16
+ validateRuntime,
17
+ validateCapabilities,
15
18
  } from "@tschk/moonshine-framework";
16
19
 
17
20
  export type CloudflareEnv = {
@@ -41,6 +44,8 @@ export async function cloudflareFetch(
41
44
  if (assetRes.status !== 404) return assetRes;
42
45
  }
43
46
 
47
+ const { reactRenderer, registerRouteModules } =
48
+ await import("@tschk/moonshine-react");
44
49
  const graph = createRouteGraph(manifest.routes);
45
50
  const match = matchRoutes(graph, pathname);
46
51
  registerRouteModules(modules);
@@ -81,90 +86,64 @@ export async function cloudflareFetch(
81
86
 
82
87
  export default cloudflareFetch;
83
88
 
84
- function resolveManifestFiles(
89
+ async function buildNativeWorker(
85
90
  manifest: MoonshineManifest,
86
- projectRoot: string,
87
- ): MoonshineManifest {
88
- return {
89
- ...manifest,
90
- routes: manifest.routes.map((route) => ({
91
- ...route,
92
- file: isAbsolute(route.file)
93
- ? route.file
94
- : resolve(projectRoot, route.file),
95
- ...(route.dataFile && {
96
- dataFile: isAbsolute(route.dataFile)
97
- ? route.dataFile
98
- : resolve(projectRoot, route.dataFile),
99
- }),
100
- ...(route.layouts && {
101
- layouts: route.layouts.map((layout) =>
102
- isAbsolute(layout) ? layout : resolve(projectRoot, layout),
103
- ),
104
- }),
105
- ...(route.middleware && {
106
- middleware: route.middleware.map((middleware) =>
107
- isAbsolute(middleware)
108
- ? middleware
109
- : resolve(projectRoot, middleware),
110
- ),
111
- }),
112
- ...(route.errorBoundary && {
113
- errorBoundary: isAbsolute(route.errorBoundary)
114
- ? route.errorBoundary
115
- : resolve(projectRoot, route.errorBoundary),
116
- }),
117
- })),
118
- };
119
- }
120
-
121
- async function copyAssets(
122
- assets: MoonshineManifest["assets"],
123
91
  outDir: string,
124
92
  ): Promise<void> {
125
- await Promise.all(
126
- assets.map(async (asset) => {
127
- const source = isAbsolute(asset.file)
128
- ? asset.file
129
- : resolve(outDir, asset.file);
130
- const target = resolve(
131
- outDir,
132
- (asset.path ?? asset.file).replace(/^\/+/, ""),
133
- );
134
- await mkdir(dirname(target), { recursive: true });
135
- if (source !== target) {
136
- await copyFile(source, target);
137
- }
138
- }),
93
+ await writeFile(
94
+ resolve(outDir, "manifest.json"),
95
+ JSON.stringify(manifest, null, 2) + "\n",
139
96
  );
140
- }
141
-
142
- function validateRuntime(
143
- adapter: DeploymentAdapter,
144
- manifest: MoonshineManifest,
145
- ): void {
146
- const errors: string[] = [];
147
- for (const route of manifest.routes) {
148
- if (!adapter.runtimes.includes(route.runtime)) {
149
- errors.push(
150
- `${adapter.name} adapter does not support route "${route.path}" with runtime "${route.runtime}"; supported runtimes: ${adapter.runtimes.join(", ")}`,
151
- );
152
- }
153
- }
154
- if (errors.length) throw new Error(errors.join("\n"));
155
- }
97
+ await copyAssets(manifest.assets, outDir);
98
+ await mkdir(resolve(outDir, "public"), { recursive: true });
99
+ const entry = `import { createRequestHandler } from "@tschk/moonshine-server";
100
+ import { modules, renderer } from "./dist/server.js";
101
+ import manifest from "./manifest.json" with { type: "json" };
156
102
 
157
- function validateCapabilities(
158
- adapter: DeploymentAdapter,
159
- manifest: MoonshineManifest,
160
- ): void {
161
- for (const capability of manifest.capabilities) {
162
- if (!adapter.capabilities.includes(capability)) {
163
- throw new Error(
164
- `${adapter.name} adapter does not support capability "${capability}"; supported capabilities: ${adapter.capabilities.join(", ")}`,
165
- );
103
+ const handler = createRequestHandler({ manifest, modules, renderer });
104
+ export default {
105
+ async fetch(request, env) {
106
+ if (env.ASSETS && (request.method === "GET" || request.method === "HEAD") && new URL(request.url).pathname !== "/") {
107
+ const response = await env.ASSETS.fetch(request);
108
+ if (response.status !== 404) return response;
109
+ if (response.body) void response.body.cancel().catch(() => {});
166
110
  }
167
- }
111
+ return handler(request);
112
+ },
113
+ };
114
+ `;
115
+ const entryPath = resolve(outDir, "server.ts");
116
+ await writeFile(entryPath, entry);
117
+ const result = await Bun.build({
118
+ entrypoints: [entryPath],
119
+ outdir: outDir,
120
+ target: "browser",
121
+ external: ["node:*"],
122
+ packages: "bundle",
123
+ });
124
+ if (!result.success)
125
+ throw new AggregateError(
126
+ result.logs,
127
+ "Native Cloudflare worker bundle failed",
128
+ );
129
+ await writeFile(
130
+ resolve(outDir, "wrangler.jsonc"),
131
+ JSON.stringify(
132
+ {
133
+ name: "moonshine-native",
134
+ main: "server.js",
135
+ compatibility_date: new Date().toISOString().slice(0, 10),
136
+ compatibility_flags: ["nodejs_compat"],
137
+ assets: {
138
+ directory: "./public",
139
+ binding: "ASSETS",
140
+ run_worker_first: true,
141
+ },
142
+ },
143
+ null,
144
+ 2,
145
+ ) + "\n",
146
+ );
168
147
  }
169
148
 
170
149
  export const cloudflareAdapter: DeploymentAdapter = {
@@ -172,10 +151,29 @@ export const cloudflareAdapter: DeploymentAdapter = {
172
151
  runtimes: ["cloudflare"],
173
152
  capabilities: ["streaming", "islands", "edge", "revalidation"],
174
153
  build: async (manifest, outDir) => {
154
+ if (manifest.requestContext)
155
+ throw new Error("Cloudflare request context is not supported");
156
+ if (
157
+ manifest.renderer === "native" &&
158
+ (manifest.routes.some((route) => route.cache) ||
159
+ manifest.capabilities.includes("revalidation"))
160
+ )
161
+ throw new Error(
162
+ "Native Cloudflare route caching/revalidation is not implemented",
163
+ );
164
+ if (
165
+ manifest.renderer === "native" &&
166
+ manifest.routes.some((route) => route.mode === "island")
167
+ )
168
+ throw new Error("Native island hydration is not implemented");
175
169
  validateRuntime(cloudflareAdapter, manifest);
176
170
  validateCapabilities(cloudflareAdapter, manifest);
177
171
 
178
172
  await mkdir(outDir, { recursive: true });
173
+ if (manifest.renderer === "native") {
174
+ await buildNativeWorker(manifest, outDir);
175
+ return;
176
+ }
179
177
  const projectRoot = resolve(outDir, "..");
180
178
  const resolvedManifest = resolveManifestFiles(manifest, projectRoot);
181
179
 
@@ -213,7 +211,12 @@ export const cloudflareHarness: HarnessFactory = (fetch, options) => {
213
211
  (request.method === "GET" || request.method === "HEAD") &&
214
212
  pathname !== "/"
215
213
  ) {
216
- const res = await tryServeStatic(options.staticDir, pathname);
214
+ const res = await tryServeStatic(
215
+ options.staticDir,
216
+ pathname,
217
+ undefined,
218
+ request.method,
219
+ );
217
220
  if (res) return res;
218
221
  }
219
222
  return fetch(request);