@octanejs/adapter-cloudflare 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dominic Gannaway
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,79 @@
1
+ # `@octanejs/adapter-cloudflare`
2
+
3
+ Deploy an Octane full-stack build to Cloudflare Workers with Workers Static
4
+ Assets. The adapter keeps the existing `dist/client` and `dist/server` layout
5
+ and adds a module Worker entry at `dist/server/worker.js`.
6
+
7
+ ```bash
8
+ pnpm add @octanejs/adapter-cloudflare
9
+ pnpm add -D wrangler
10
+ ```
11
+
12
+ ## Configure Octane
13
+
14
+ ```ts
15
+ import { cloudflare } from '@octanejs/adapter-cloudflare';
16
+ import { defineConfig, RenderRoute } from '@octanejs/vite-plugin';
17
+
18
+ export default defineConfig({
19
+ adapter: cloudflare(),
20
+ router: {
21
+ routes: [new RenderRoute({ path: '/*path', entry: '/src/App.tsrx' })],
22
+ },
23
+ });
24
+ ```
25
+
26
+ The same adapter contract is supported by `@octanejs/rsbuild-plugin`.
27
+
28
+ ## Configure Wrangler
29
+
30
+ Keep `wrangler.jsonc` in your app as the source of truth so bindings, routes,
31
+ secrets, environments, placement, and observability remain under your control:
32
+
33
+ ```jsonc
34
+ {
35
+ "$schema": "./node_modules/wrangler/config-schema.json",
36
+ "name": "my-octane-app",
37
+ "main": "./dist/server/worker.js",
38
+ "compatibility_date": "2026-07-14",
39
+ "compatibility_flags": ["nodejs_compat"],
40
+ "assets": {
41
+ "directory": "./dist/client",
42
+ "binding": "ASSETS",
43
+ },
44
+ }
45
+ ```
46
+
47
+ `nodejs_compat` is currently required for the synchronous SHA-256 and
48
+ `AsyncLocalStorage` primitives used by Octane server functions and request-local
49
+ fetch handling. Use a current compatibility date when creating a new app.
50
+
51
+ Build first, then run or deploy the exact production output:
52
+
53
+ ```bash
54
+ pnpm vite build
55
+ pnpm wrangler dev
56
+ pnpm wrangler deploy
57
+ ```
58
+
59
+ Cloudflare's default asset-first routing is the intended setup: exact files in
60
+ `dist/client` are served without invoking the Worker, and every miss reaches
61
+ Octane SSR. Leave `assets.not_found_handling` unset (the default `"none"`) or
62
+ set it explicitly to `"none"`: both `"single-page-application"` and
63
+ `"404-page"` can prevent browser-navigation misses from reaching SSR. Leave
64
+ `run_worker_first` unset/`false` unless every asset request intentionally needs
65
+ Worker logic.
66
+
67
+ The generated Worker forwards Cloudflare's request-scoped `{ env, ctx }` as
68
+ `context.platform` to middleware and `ServerRoute` handlers:
69
+
70
+ ```ts
71
+ import type { CloudflarePlatform } from '@octanejs/adapter-cloudflare';
72
+
73
+ const platform = context.platform as CloudflarePlatform<Env>;
74
+ await platform.env.MY_KV.get('key');
75
+ platform.ctx.waitUntil(writeAnalytics());
76
+ ```
77
+
78
+ Streaming SSR remains a Web `ReadableStream` from Octane through the Worker;
79
+ the adapter does not buffer or translate the response body.
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@octanejs/adapter-cloudflare",
3
+ "version": "0.0.1",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=22"
8
+ },
9
+ "description": "Cloudflare Workers adapter for Octane apps",
10
+ "author": {
11
+ "name": "Dominic Gannaway",
12
+ "email": "dg@domgan.com"
13
+ },
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/octanejs/octane.git",
20
+ "directory": "packages/adapter-cloudflare"
21
+ },
22
+ "files": [
23
+ "src",
24
+ "types",
25
+ "README.md",
26
+ "LICENSE"
27
+ ],
28
+ "module": "src/index.js",
29
+ "main": "src/index.js",
30
+ "exports": {
31
+ ".": {
32
+ "types": "./types/index.d.ts",
33
+ "worker": "./src/worker.js",
34
+ "browser": "./src/worker.js",
35
+ "import": "./src/index.js",
36
+ "default": "./src/index.js"
37
+ }
38
+ },
39
+ "peerDependencies": {
40
+ "@octanejs/app-core": "0.0.7"
41
+ },
42
+ "devDependencies": {
43
+ "@rsbuild/core": "^2.1.5",
44
+ "@types/node": "^24.3.0",
45
+ "miniflare": "4.20260714.0",
46
+ "vite": "^8.0.16",
47
+ "@octanejs/app-core": "0.0.7",
48
+ "@octanejs/rsbuild-plugin": "0.1.6",
49
+ "@octanejs/vite-plugin": "0.1.11",
50
+ "octane": "0.1.11"
51
+ }
52
+ }
package/src/adapt.js ADDED
@@ -0,0 +1,86 @@
1
+ // @ts-check
2
+ /**
3
+ * Cloudflare module Worker emitter.
4
+ *
5
+ * Octane's web-worker server build exports `createWebWorkerHandler()` and is
6
+ * already self-contained. This final build step embeds the transformed HTML
7
+ * template (and Rsbuild's late client asset map, when present) into a tiny
8
+ * module Worker entry. Cloudflare serves `dist/client` asset-first; only misses
9
+ * reach this handler, so the client index remains private under dist/server.
10
+ */
11
+
12
+ /** @import { AdaptContext } from '@octanejs/app-core' */
13
+
14
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
15
+ import { join } from 'node:path';
16
+
17
+ const CLIENT_ASSET_MAP = 'octane-client-assets.json';
18
+ const WORKER_ENTRY = 'worker.js';
19
+ const GENERATED_WORKER_HEADER = '// Auto-generated by @octanejs/adapter-cloudflare';
20
+
21
+ /**
22
+ * @param {string} htmlTemplate
23
+ * @param {Record<string, unknown> | undefined} clientAssets
24
+ * @returns {string}
25
+ */
26
+ function generateWorkerSource(htmlTemplate, clientAssets) {
27
+ const options = { htmlTemplate, ...(clientAssets === undefined ? null : { clientAssets }) };
28
+ return `${GENERATED_WORKER_HEADER} — do not edit.
29
+ import { createWebWorkerHandler } from './entry.js';
30
+
31
+ let handler;
32
+
33
+ export default {
34
+ async fetch(request, env, ctx) {
35
+ try {
36
+ handler ??= createWebWorkerHandler(${JSON.stringify(options, null, '\t')});
37
+ return await handler(request, { env, ctx });
38
+ } catch (error) {
39
+ console.error('[octane] Cloudflare Worker handler error:', error);
40
+ return new Response('Internal Server Error', { status: 500 });
41
+ }
42
+ },
43
+ };
44
+ `;
45
+ }
46
+
47
+ /**
48
+ * @param {AdaptContext} ctx
49
+ * @returns {Promise<void>}
50
+ */
51
+ export async function emitCloudflareWorker(ctx) {
52
+ const { clientDir, serverDir } = ctx;
53
+ const log = ctx.log ?? ((/** @type {string} */ message) => console.log(message));
54
+ const serverEntry = join(serverDir, 'entry.js');
55
+ const htmlTemplate = join(serverDir, 'index.html');
56
+
57
+ if (!existsSync(clientDir)) {
58
+ throw new Error(
59
+ `[@octanejs/adapter-cloudflare] Client build output not found at ${clientDir}.`,
60
+ );
61
+ }
62
+ if (!existsSync(serverEntry)) {
63
+ throw new Error(`[@octanejs/adapter-cloudflare] Server entry not found at ${serverEntry}.`);
64
+ }
65
+ if (!existsSync(htmlTemplate)) {
66
+ throw new Error(
67
+ `[@octanejs/adapter-cloudflare] Server HTML template not found at ${htmlTemplate}.`,
68
+ );
69
+ }
70
+
71
+ const clientAssetMap = join(serverDir, CLIENT_ASSET_MAP);
72
+ const clientAssets = existsSync(clientAssetMap)
73
+ ? JSON.parse(readFileSync(clientAssetMap, 'utf-8'))
74
+ : undefined;
75
+ const output = join(serverDir, WORKER_ENTRY);
76
+ if (existsSync(output) && !readFileSync(output, 'utf-8').startsWith(GENERATED_WORKER_HEADER)) {
77
+ throw new Error(
78
+ `[@octanejs/adapter-cloudflare] Refusing to replace ${output}: the server build already emitted worker.js. Configure asynchronous server chunks under a subdirectory.`,
79
+ );
80
+ }
81
+ writeFileSync(output, generateWorkerSource(readFileSync(htmlTemplate, 'utf-8'), clientAssets));
82
+
83
+ log(
84
+ `[@octanejs/adapter-cloudflare] Worker entry written to ${join(ctx.outDir, 'server', WORKER_ENTRY)}`,
85
+ );
86
+ }
package/src/index.js ADDED
@@ -0,0 +1,35 @@
1
+ // @ts-check
2
+ /**
3
+ * @octanejs/adapter-cloudflare — Cloudflare Workers adapter for Octane apps.
4
+ *
5
+ * The production integrations see `serverTarget: 'webworker'` and emit a
6
+ * template-free Worker bundle. After both bundles exist, adapt() embeds the
7
+ * built HTML template in `dist/server/worker.js`, the module Worker entry that
8
+ * a user-owned wrangler.jsonc deploys beside `dist/client` static assets.
9
+ */
10
+
11
+ /** @import { AdaptContext, OctaneAdapter } from '@octanejs/app-core' */
12
+
13
+ import { emitCloudflareWorker } from './adapt.js';
14
+ import { runtime } from './runtime.js';
15
+
16
+ /**
17
+ * Emit the Cloudflare module Worker entry after an Octane production build.
18
+ * @param {AdaptContext} ctx
19
+ * @returns {Promise<void>}
20
+ */
21
+ export async function adapt(ctx) {
22
+ await emitCloudflareWorker(ctx);
23
+ }
24
+
25
+ /**
26
+ * @returns {OctaneAdapter}
27
+ */
28
+ export function cloudflare() {
29
+ return {
30
+ name: 'cloudflare',
31
+ serverTarget: 'webworker',
32
+ runtime,
33
+ adapt,
34
+ };
35
+ }
package/src/runtime.js ADDED
@@ -0,0 +1,21 @@
1
+ // @ts-check
2
+ /** Cloudflare-compatible primitives consumed by app-core's production handler. */
3
+
4
+ /** @import { OctaneAdapter } from '@octanejs/app-core' */
5
+
6
+ import { AsyncLocalStorage } from 'node:async_hooks';
7
+ import { createHash } from 'node:crypto';
8
+
9
+ /** @type {NonNullable<OctaneAdapter['runtime']>} */
10
+ export const runtime = {
11
+ hash(value) {
12
+ return createHash('sha256').update(value).digest('hex').slice(0, 8);
13
+ },
14
+ createAsyncContext() {
15
+ const storage = new AsyncLocalStorage();
16
+ return {
17
+ run: (store, callback) => storage.run(store, callback),
18
+ getStore: () => storage.getStore(),
19
+ };
20
+ },
21
+ };
package/src/worker.js ADDED
@@ -0,0 +1,29 @@
1
+ // @ts-check
2
+ /**
3
+ * Runtime-only facade selected while bundling for a browser/Worker target.
4
+ * Build-time filesystem emission stays in the default Node export.
5
+ */
6
+
7
+ /** @import { AdaptContext, OctaneAdapter } from '@octanejs/app-core' */
8
+
9
+ import { runtime } from './runtime.js';
10
+
11
+ /**
12
+ * This facade is bundled into the deployed Worker; adaptation only runs in the
13
+ * parent Node build, so reaching it here indicates a build integration bug.
14
+ * @param {AdaptContext} _ctx
15
+ * @returns {Promise<void>}
16
+ */
17
+ export async function adapt(_ctx) {
18
+ throw new Error('[octane] Cloudflare adapt() is only available during the Node build.');
19
+ }
20
+
21
+ /** @returns {OctaneAdapter} */
22
+ export function cloudflare() {
23
+ return {
24
+ name: 'cloudflare',
25
+ serverTarget: 'webworker',
26
+ runtime,
27
+ adapt,
28
+ };
29
+ }
@@ -0,0 +1,23 @@
1
+ import type { AdaptContext, OctaneAdapter } from '@octanejs/app-core';
2
+
3
+ /** The minimal execution-context surface used by the adapter. */
4
+ export interface CloudflareExecutionContext {
5
+ waitUntil(promise: Promise<unknown>): void;
6
+ passThroughOnException(): void;
7
+ props?: unknown;
8
+ }
9
+
10
+ /** Available as `context.platform` in Octane middleware and ServerRoutes. */
11
+ export interface CloudflarePlatform<
12
+ Env = Record<string, unknown>,
13
+ ExecutionContext extends CloudflareExecutionContext = CloudflareExecutionContext,
14
+ > {
15
+ env: Env;
16
+ ctx: ExecutionContext;
17
+ }
18
+
19
+ /** Select Cloudflare's Worker build target and emit dist/server/worker.js. */
20
+ export function cloudflare(): OctaneAdapter;
21
+
22
+ /** Emit dist/server/worker.js from completed Octane client/server bundles. */
23
+ export function adapt(ctx: AdaptContext): Promise<void>;