@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.
- package/README.md +26 -0
- package/package.json +14 -8
- 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.
|
|
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-
|
|
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.
|
|
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 {
|
|
2
|
-
import {
|
|
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 {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
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
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
)
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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(
|
|
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);
|