From 226d1d1477f1a324ca9f26295a68cc6d6282bba9 Mon Sep 17 00:00:00 2001 From: Ryan Turnquist Date: Tue, 30 Jun 2026 21:49:12 +0000 Subject: [PATCH 1/5] feat: add Cloudflare deployment adapter Add @marko/run-adapter-cloudflare for deploying Marko Run apps to Cloudflare Workers (default) or Cloudflare Pages. - Builds for the webworker SSR target with workerd export conditions - Workers mode emits a bundled `_worker.js` plus a generated wrangler.json (skipped when a project-level Wrangler config exists) - Pages mode emits an advanced-mode `_worker.js` and a `_routes.json` that excludes static assets from the function - Exposes env/ctx/cf to route handlers via CloudflarePlatformInfo - Previews via the Wrangler CLI (`wrangler dev` / `wrangler pages dev`) --- .changeset/cloudflare-adapter.md | 5 + cspell.json | 1 + package-lock.json | 456 ++++++++++++++++++ packages/adapters/cloudflare/README.md | 111 +++++ packages/adapters/cloudflare/package.json | 36 ++ packages/adapters/cloudflare/scripts/build.ts | 51 ++ .../cloudflare/scripts/importMetaURL.js | 2 + .../adapters/cloudflare/src/default-entry.ts | 30 ++ packages/adapters/cloudflare/src/index.ts | 238 +++++++++ packages/adapters/cloudflare/src/types.ts | 27 ++ packages/adapters/cloudflare/tsconfig.json | 10 + 11 files changed, 967 insertions(+) create mode 100644 .changeset/cloudflare-adapter.md create mode 100644 packages/adapters/cloudflare/README.md create mode 100644 packages/adapters/cloudflare/package.json create mode 100644 packages/adapters/cloudflare/scripts/build.ts create mode 100644 packages/adapters/cloudflare/scripts/importMetaURL.js create mode 100644 packages/adapters/cloudflare/src/default-entry.ts create mode 100644 packages/adapters/cloudflare/src/index.ts create mode 100644 packages/adapters/cloudflare/src/types.ts create mode 100644 packages/adapters/cloudflare/tsconfig.json diff --git a/.changeset/cloudflare-adapter.md b/.changeset/cloudflare-adapter.md new file mode 100644 index 00000000..518ecb70 --- /dev/null +++ b/.changeset/cloudflare-adapter.md @@ -0,0 +1,5 @@ +--- +"@marko/run-adapter-cloudflare": minor +--- + +Add a Cloudflare adapter for deploying @marko/run apps to Cloudflare Workers (default) or Cloudflare Pages. diff --git a/cspell.json b/cspell.json index 21f9d12d..e5973e9e 100644 --- a/cspell.json +++ b/cspell.json @@ -14,6 +14,7 @@ "Rels", "onopentag", "WHATWG", + "workerd", "rgba", "Neue", "Roboto", diff --git a/package-lock.json b/package-lock.json index a01363b5..82313657 100644 --- a/package-lock.json +++ b/package-lock.json @@ -673,6 +673,12 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/@cloudflare/workers-types": { + "version": "4.20260630.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260630.1.tgz", + "integrity": "sha512-yl+c9vwvko9UZ0frmtsHuwOh3BRHvNjLrfelAp5Akpqe1+Ho1UWekr3nmjJ1D64CH0Yb0K0oRMV4i7npOFzsog==", + "license": "MIT OR Apache-2.0" + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -2485,6 +2491,10 @@ "resolved": "packages/run", "link": true }, + "node_modules/@marko/run-adapter-cloudflare": { + "resolved": "packages/adapters/cloudflare", + "link": true + }, "node_modules/@marko/run-adapter-netlify": { "resolved": "packages/adapters/netlify", "link": true @@ -14038,6 +14048,381 @@ } } }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "peer": true + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "peer": true + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, "node_modules/netlify-cli/node_modules/@sec-ant/readable-stream": { "version": "0.4.1", "dev": true, @@ -17283,6 +17668,19 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/netlify-cli/node_modules/fsevents": { + "version": "2.3.3", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/netlify-cli/node_modules/function-bind": { "version": "1.1.2", "dev": true, @@ -21065,6 +21463,53 @@ "dev": true, "license": "MIT" }, + "node_modules/netlify-cli/node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, "node_modules/netlify-cli/node_modules/router": { "version": "2.2.0", "dev": true, @@ -25556,6 +26001,17 @@ "url": "https://github.com/sponsors/colinhacks" } }, + "packages/adapters/cloudflare": { + "name": "@marko/run-adapter-cloudflare", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@cloudflare/workers-types": "^4.20241106.0" + }, + "peerDependencies": { + "@marko/run": "^0.7||^0.8||^0.9||^0.10" + } + }, "packages/adapters/netlify": { "name": "@marko/run-adapter-netlify", "version": "3.0.3", diff --git a/packages/adapters/cloudflare/README.md b/packages/adapters/cloudflare/README.md new file mode 100644 index 00000000..2db64701 --- /dev/null +++ b/packages/adapters/cloudflare/README.md @@ -0,0 +1,111 @@ +

+ + + + + Marko Run Logo + +
+ @marko/run-adapter-cloudflare +
+

+ +Preview and deploy [@marko/run](../../run/README.md) apps to Cloudflare Workers/Pages + +## Installation + +```sh +npm install @marko/run-adapter-cloudflare +``` + +## Usage + +In your application's Vite config file (eg. `vite.config.js`), import and register this adapter with the `@marko/run` Vite plugin: + +```ts +import { defineConfig } from "vite"; +import marko from "@marko/run/vite"; +import cloudflareAdapter from "@marko/run-adapter-cloudflare"; + +export default defineConfig({ + plugins: [ + marko({ + adapter: cloudflareAdapter(), + }), + ], +}); +``` + +The adapter builds for [Cloudflare Workers](https://developers.cloudflare.com/workers/) by default. `wrangler` is used to preview and deploy your app, so install it as a dev dependency: + +```sh +npm install -D wrangler +``` + +## Workers (default) + +Running `marko-run build` produces the following in your output directory (`dist` by default): + +- `_worker.js` — the bundled server entry +- `public/` — your app's static assets +- `wrangler.json` — a generated starter config (only written when no `wrangler.toml`/`wrangler.json`/`wrangler.jsonc` exists at your project root) + +Deploy with: + +```sh +wrangler deploy --config dist/wrangler.json +``` + +To use your own bindings (KV, D1, R2, secrets, custom name, routes, etc.), add a Wrangler config at the root of your project. When present, the adapter will **not** generate one — point it at the build output yourself: + +```jsonc +// wrangler.jsonc +{ + "name": "my-app", + "main": "./dist/_worker.js", + "compatibility_date": "2024-11-01", + "compatibility_flags": ["nodejs_compat"], + "assets": { + "directory": "./dist/public", + "binding": "ASSETS", + }, +} +``` + +## Pages + +This adapter can target [Cloudflare Pages](https://developers.cloudflare.com/pages/) instead, building a [Pages Functions "advanced mode"](https://developers.cloudflare.com/pages/functions/advanced-mode/) `_worker.js`: + +```ts +import { defineConfig } from "vite"; +import marko from "@marko/run/vite"; +import cloudflareAdapter from "@marko/run-adapter-cloudflare"; + +export default defineConfig({ + plugins: [ + marko({ + adapter: cloudflareAdapter({ mode: "pages" }), + }), + ], +}); +``` + +This emits `_worker.js` and a `_routes.json` into `dist/public`. Deploy the assets directory with: + +```sh +wrangler pages deploy dist/public +``` + +## Platform info + +The Worker environment, execution context and Cloudflare request properties are available on the `platform` argument of your route handlers: + +```ts +import type { CloudflarePlatformInfo } from "@marko/run-adapter-cloudflare"; + +export const GET = (context, { platform }) => { + const { env, ctx, cf } = platform as CloudflarePlatformInfo; + ctx.waitUntil(logRequest(cf)); + return new Response(`Hello from ${cf?.city ?? "the edge"}!`); +}; +``` diff --git a/packages/adapters/cloudflare/package.json b/packages/adapters/cloudflare/package.json new file mode 100644 index 00000000..8f3978db --- /dev/null +++ b/packages/adapters/cloudflare/package.json @@ -0,0 +1,36 @@ +{ + "name": "@marko/run-adapter-cloudflare", + "version": "0.1.0", + "description": "Preview and deploy @marko/run apps to Cloudflare Workers/Pages", + "keywords": [ + "marko" + ], + "homepage": "https://github.com/marko-js/run/tree/main/packages/adapters/cloudflare", + "repository": { + "type": "git", + "url": "https://github.com/marko-js/run.git" + }, + "license": "MIT", + "author": "Ryan Turnquist ", + "type": "module", + "exports": { + ".": "./src/index.ts" + }, + "types": "./src/index.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc -b && tsx scripts/build.ts", + "test": "" + }, + "dependencies": { + "@cloudflare/workers-types": "^4.20241106.0" + }, + "peerDependencies": { + "@marko/run": "^0.7||^0.8||^0.9||^0.10" + }, + "logo": { + "url": "https://github.com/marko-js/run/raw/main/assets/marko-run.png" + } +} diff --git a/packages/adapters/cloudflare/scripts/build.ts b/packages/adapters/cloudflare/scripts/build.ts new file mode 100644 index 00000000..d70f08ec --- /dev/null +++ b/packages/adapters/cloudflare/scripts/build.ts @@ -0,0 +1,51 @@ +import { build, BuildOptions } from "esbuild"; +import path from "path"; + +const srcdir = path.resolve("src"); +const outdir = path.resolve("dist"); + +const opts: BuildOptions = { + entryPoints: ["src/index.ts"], + outdir, + outbase: srcdir, + platform: "node", + target: ["node14"], + bundle: true, + plugins: [ + { + name: "external-modules", + setup(build) { + build.onResolve( + { filter: /^[^./]|^\.[^./]|^\.\.[^/]/ }, + ({ path }) => ({ + path, + external: true, + }), + ); + }, + }, + ], +}; + +await Promise.all([ + build({ + ...opts, + format: "cjs", + outExtension: { ".js": ".cjs" }, + define: { + "import.meta.url": "__importMetaURL", + }, + inject: ["./scripts/importMetaURL.js"], + }), + build({ + ...opts, + format: "esm", + splitting: true, + }), + build({ + ...opts, + entryPoints: ["src/default-entry.ts"], + format: "esm", + outExtension: { ".js": ".mjs" }, + }), +]); diff --git a/packages/adapters/cloudflare/scripts/importMetaURL.js b/packages/adapters/cloudflare/scripts/importMetaURL.js new file mode 100644 index 00000000..c993f7f5 --- /dev/null +++ b/packages/adapters/cloudflare/scripts/importMetaURL.js @@ -0,0 +1,2 @@ +import { pathToFileURL } from "url"; +export const __importMetaURL = pathToFileURL(__filename); diff --git a/packages/adapters/cloudflare/src/default-entry.ts b/packages/adapters/cloudflare/src/default-entry.ts new file mode 100644 index 00000000..983317d3 --- /dev/null +++ b/packages/adapters/cloudflare/src/default-entry.ts @@ -0,0 +1,30 @@ +import type { + ExecutionContext, + Request as CfRequest, +} from "@cloudflare/workers-types"; +import { fetch } from "@marko/run/router"; + +import type { CloudflareEnv, CloudflarePlatformInfo } from "./types"; + +export default { + async fetch(request: CfRequest, env: CloudflareEnv, ctx: ExecutionContext) { + const response = await fetch(request as any, { + env, + ctx, + cf: request.cf, + }); + + if (response) { + return response; + } + + // Fall back to the static asset handler when no route matched. This is a + // safety net — Cloudflare normally serves matching assets before invoking + // the worker. + if (env.ASSETS) { + return env.ASSETS.fetch(request); + } + + return new Response(null, { status: 404 }); + }, +}; diff --git a/packages/adapters/cloudflare/src/index.ts b/packages/adapters/cloudflare/src/index.ts new file mode 100644 index 00000000..a873d5e4 --- /dev/null +++ b/packages/adapters/cloudflare/src/index.ts @@ -0,0 +1,238 @@ +import baseAdapter, { type Adapter } from "@marko/run/adapter"; +import { execSync, spawn } from "child_process"; +import fs from "fs/promises"; +import path from "path"; +import { fileURLToPath } from "url"; + +export type { CloudflareEnv, CloudflarePlatformInfo } from "./types"; + +const __dirname = fileURLToPath(path.dirname(import.meta.url)); +const defaultEntry = path.join(__dirname, "default-entry"); + +/** Generated when no Wrangler config is found at the project root. */ +const COMPATIBILITY_DATE = "2024-11-01"; + +export interface Options { + /** + * The Cloudflare product to target. + * + * - `"workers"` (default) builds a Worker with a static assets binding, + * deployed with `wrangler deploy`. + * - `"pages"` builds a Cloudflare Pages "advanced mode" `_worker.js` + * alongside the static assets, deployed with `wrangler pages deploy`. + */ + mode?: "workers" | "pages"; +} + +export default function cloudflareAdapter(options: Options = {}): Adapter { + const { mode = "workers" } = options; + const { startDev } = baseAdapter(); + + return { + name: "cloudflare-adapter", + + viteConfig(config) { + if (config.build?.ssr) { + return { + ssr: { + target: "webworker", + resolve: { + dedupe: ["marko"], + conditions: [ + "workerd", + "worker", + "browser", + "import", + "require", + "production", + "default", + ], + }, + noExternal: true, + }, + }; + } + }, + + getEntryFile() { + return defaultEntry; + }, + + startDev(event) { + return startDev!({ + ...event, + entry: event.entry === defaultEntry ? undefined : event.entry, + }); + }, + + async startPreview({ options: previewOptions }) { + assertWranglerCLI(); + + const { port = 3000, cwd, dir } = previewOptions; + const publicDir = path.join(dir, "public"); + + const args = + mode === "pages" + ? ["pages", "dev", publicDir, "--port", port.toString()] + : [ + "dev", + "--config", + path.join(dir, "wrangler.json"), + "--port", + port.toString(), + ]; + + args.push(...parseWranglerArgs(previewOptions.args)); + + const proc = spawn("wrangler", args, { + cwd, + env: process.env, + shell: true, + }); + + if (process.env.NODE_ENV !== "test") { + proc.stdout.pipe(process.stdout); + } + proc.stderr.pipe(process.stderr); + + return { + port, + close() { + proc.unref(); + proc.kill(); + }, + }; + }, + + async buildEnd({ config, builtEntries }) { + const serverEntry = builtEntries[0]; + if (!serverEntry) { + return; + } + + // During the client build `outDir` points at the static assets dir + // (`/public`); its parent is the SSR build dir that holds the + // built worker entry. + const publicDir = path.resolve(config.root, config.build.outDir); + const distDir = path.dirname(publicDir); + + if (mode === "pages") { + // Advanced mode: a single `_worker.js` sits next to the static assets + // and a `_routes.json` keeps static files from invoking the worker. + const exclude = await collectStaticRoutes(publicDir); + await fs.rename(serverEntry, path.join(publicDir, "_worker.js")); + await fs.writeFile( + path.join(publicDir, "_routes.json"), + JSON.stringify({ version: 1, include: ["/*"], exclude }, null, 2), + ); + } else { + // Workers: keep the worker bundle in the dist root and point a + // generated Wrangler config at it and the static assets. An existing + // project-level Wrangler config is left untouched. + const workerEntry = path.join(distDir, "_worker.js"); + if (path.resolve(serverEntry) !== workerEntry) { + await fs.rename(serverEntry, workerEntry); + } + + if (!(await hasRootWranglerConfig(config.root))) { + await fs.writeFile( + path.join(distDir, "wrangler.json"), + JSON.stringify( + { + name: "marko-run-app", + main: "_worker.js", + compatibility_date: COMPATIBILITY_DATE, + compatibility_flags: ["nodejs_compat"], + assets: { + directory: "./public", + binding: "ASSETS", + }, + }, + null, + 2, + ), + ); + } + } + }, + + typeInfo(writer) { + writer( + `import type { CloudflarePlatformInfo } from '@marko/run-adapter-cloudflare';`, + ); + return "CloudflarePlatformInfo"; + }, + }; +} + +/** + * Build a `_routes.json` exclude list from the top-level entries of the static + * assets directory so Cloudflare Pages serves them directly instead of + * invoking the worker. Cloudflare allows at most 100 exclude rules. + */ +async function collectStaticRoutes(publicDir: string): Promise { + let entries: import("fs").Dirent[]; + try { + entries = await fs.readdir(publicDir, { withFileTypes: true }); + } catch { + return []; + } + + const exclude: string[] = []; + for (const entry of entries) { + if (entry.name === "_worker.js" || entry.name === "_routes.json") { + continue; + } + exclude.push(entry.isDirectory() ? `/${entry.name}/*` : `/${entry.name}`); + if (exclude.length >= 100) { + break; + } + } + return exclude; +} + +async function hasRootWranglerConfig(root: string): Promise { + for (const name of ["wrangler.toml", "wrangler.json", "wrangler.jsonc"]) { + try { + await fs.access(path.join(root, name)); + return true; + } catch { + // not found, keep looking + } + } + return false; +} + +const devFlags = new RegExp( + [ + "compatibility-date=.+", + "compatibility-flags=.+", + "env=.+", + "ip=.+", + "local-protocol=.+", + "https-key-path=.+", + "https-cert-path=.+", + "inspector-port=.+", + "live-reload", + "remote", + "local", + "show-interactive-dev-session", + ] + .map((flag) => `--${flag}`) + .join("|"), +); + +function parseWranglerArgs(args: string[]) { + return args.filter((arg) => devFlags.test(arg)); +} + +function assertWranglerCLI() { + try { + execSync("wrangler --version"); + } catch (error) { + console.warn( + `Wrangler CLI not found. Please install it with \`npm install -D wrangler\``, + ); + process.exit(1); + } +} diff --git a/packages/adapters/cloudflare/src/types.ts b/packages/adapters/cloudflare/src/types.ts new file mode 100644 index 00000000..8f957d26 --- /dev/null +++ b/packages/adapters/cloudflare/src/types.ts @@ -0,0 +1,27 @@ +import type { + ExecutionContext, + Fetcher, + IncomingRequestCfProperties, +} from "@cloudflare/workers-types"; + +/** + * The bindings available to the worker. The `ASSETS` binding is provided + * automatically by Cloudflare Pages and by Workers configured with a static + * assets directory, and is used to serve the app's static files. + */ +export interface CloudflareEnv { + ASSETS: Fetcher; + [key: string]: unknown; +} + +/** + * The platform object passed to Marko Run route handlers when deployed to + * Cloudflare. Exposes the Worker's environment bindings, the execution + * context (for `waitUntil`/`passThroughOnException`) and the Cloudflare + * request properties (`request.cf`). + */ +export interface CloudflarePlatformInfo { + env: Env; + ctx: ExecutionContext; + cf?: IncomingRequestCfProperties; +} diff --git a/packages/adapters/cloudflare/tsconfig.json b/packages/adapters/cloudflare/tsconfig.json new file mode 100644 index 00000000..cd4f0966 --- /dev/null +++ b/packages/adapters/cloudflare/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.json", + "include": ["src/**/*"], + "exclude": ["src/**/__tests__/**/*", "src/default-entry.ts"], + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "tsBuildInfoFile": "dist/.tsbuildinfo" + } +} From d0aa640ef9c8f5d2dbec82d8653956a3005ca001 Mon Sep 17 00:00:00 2001 From: Ryan Turnquist Date: Tue, 30 Jun 2026 22:10:51 +0000 Subject: [PATCH 2/5] test: add Cloudflare adapter fixtures for workers and pages modes Add dev test fixtures for both Cloudflare adapter modes, mirroring the existing Netlify adapter fixtures. Preview is skipped because it requires the Wrangler CLI (unavailable in CI), matching the Netlify edge fixture. Verified the build output for each mode: - workers: dist/_worker.js (default fetch export with ASSETS fallback) and a generated dist/wrangler.json pointing at the worker and public assets - pages: dist/public/_worker.js plus dist/public/_routes.json excluding the static assets dir --- .../cloudflare-adapter-pages/.gitignore | 1 + .../.marko-run/routes.d.ts | 36 +++++++++++++++++++ .../__snapshots__/dev.expected.md | 35 ++++++++++++++++++ .../cloudflare-adapter-pages/package.json | 5 +++ .../src/components/counter.marko | 11 ++++++ .../src/routes/+page.marko | 12 +++++++ .../cloudflare-adapter-pages/test.config.ts | 13 +++++++ .../cloudflare-adapter-pages/tsconfig.json | 4 +++ .../cloudflare-adapter-pages/vite.config.ts | 10 ++++++ .../cloudflare-adapter-workers/.gitignore | 1 + .../.marko-run/routes.d.ts | 36 +++++++++++++++++++ .../__snapshots__/dev.expected.md | 35 ++++++++++++++++++ .../cloudflare-adapter-workers/package.json | 5 +++ .../src/components/counter.marko | 11 ++++++ .../src/routes/+page.marko | 12 +++++++ .../cloudflare-adapter-workers/test.config.ts | 13 +++++++ .../cloudflare-adapter-workers/tsconfig.json | 4 +++ .../cloudflare-adapter-workers/vite.config.ts | 10 ++++++ 18 files changed, 254 insertions(+) create mode 100644 packages/run/src/__tests__/fixtures/cloudflare-adapter-pages/.gitignore create mode 100644 packages/run/src/__tests__/fixtures/cloudflare-adapter-pages/.marko-run/routes.d.ts create mode 100644 packages/run/src/__tests__/fixtures/cloudflare-adapter-pages/__snapshots__/dev.expected.md create mode 100644 packages/run/src/__tests__/fixtures/cloudflare-adapter-pages/package.json create mode 100644 packages/run/src/__tests__/fixtures/cloudflare-adapter-pages/src/components/counter.marko create mode 100644 packages/run/src/__tests__/fixtures/cloudflare-adapter-pages/src/routes/+page.marko create mode 100644 packages/run/src/__tests__/fixtures/cloudflare-adapter-pages/test.config.ts create mode 100644 packages/run/src/__tests__/fixtures/cloudflare-adapter-pages/tsconfig.json create mode 100644 packages/run/src/__tests__/fixtures/cloudflare-adapter-pages/vite.config.ts create mode 100644 packages/run/src/__tests__/fixtures/cloudflare-adapter-workers/.gitignore create mode 100644 packages/run/src/__tests__/fixtures/cloudflare-adapter-workers/.marko-run/routes.d.ts create mode 100644 packages/run/src/__tests__/fixtures/cloudflare-adapter-workers/__snapshots__/dev.expected.md create mode 100644 packages/run/src/__tests__/fixtures/cloudflare-adapter-workers/package.json create mode 100644 packages/run/src/__tests__/fixtures/cloudflare-adapter-workers/src/components/counter.marko create mode 100644 packages/run/src/__tests__/fixtures/cloudflare-adapter-workers/src/routes/+page.marko create mode 100644 packages/run/src/__tests__/fixtures/cloudflare-adapter-workers/test.config.ts create mode 100644 packages/run/src/__tests__/fixtures/cloudflare-adapter-workers/tsconfig.json create mode 100644 packages/run/src/__tests__/fixtures/cloudflare-adapter-workers/vite.config.ts diff --git a/packages/run/src/__tests__/fixtures/cloudflare-adapter-pages/.gitignore b/packages/run/src/__tests__/fixtures/cloudflare-adapter-pages/.gitignore new file mode 100644 index 00000000..1521c8b7 --- /dev/null +++ b/packages/run/src/__tests__/fixtures/cloudflare-adapter-pages/.gitignore @@ -0,0 +1 @@ +dist diff --git a/packages/run/src/__tests__/fixtures/cloudflare-adapter-pages/.marko-run/routes.d.ts b/packages/run/src/__tests__/fixtures/cloudflare-adapter-pages/.marko-run/routes.d.ts new file mode 100644 index 00000000..fb83b4f5 --- /dev/null +++ b/packages/run/src/__tests__/fixtures/cloudflare-adapter-pages/.marko-run/routes.d.ts @@ -0,0 +1,36 @@ +/* + WARNING: This file is automatically generated and any changes made to it will be overwritten without warning. + Do NOT manually edit this file or your changes will be lost. +*/ + +import { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform } from "@marko/run/namespace"; +import type * as Run from "@marko/run"; +import type { CloudflarePlatformInfo } from '@marko/run-adapter-cloudflare'; + +declare module "@marko/run" { + interface Platform extends CloudflarePlatformInfo {} + + interface AppData extends Run.DefineApp<{ + routes: { + "/": { verb: "get"; }; + } + }> {} +} + +declare module "../src/routes/+page.marko" { + namespace MarkoRun { + export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; + export type Route = Run.Routes["/"]; + export type Context = Run.MultiRouteContext & Marko.Global; + export type Handler = Run.HandlerLike; + export type GET = Run.HandlerLike; + export type HEAD = Run.HandlerLike; + export type POST = Run.HandlerLike; + export type PUT = Run.HandlerLike; + export type DELETE = Run.HandlerLike; + export type PATCH = Run.HandlerLike; + export type OPTIONS = Run.HandlerLike; + /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ + export const route: Run.HandlerTypeFn; + } +} diff --git a/packages/run/src/__tests__/fixtures/cloudflare-adapter-pages/__snapshots__/dev.expected.md b/packages/run/src/__tests__/fixtures/cloudflare-adapter-pages/__snapshots__/dev.expected.md new file mode 100644 index 00000000..1ec2ae91 --- /dev/null +++ b/packages/run/src/__tests__/fixtures/cloudflare-adapter-pages/__snapshots__/dev.expected.md @@ -0,0 +1,35 @@ +# Loading + +```html + +``` + +# Step 0 +ctx=>click(ctx) + +```diff +- 0 ++ 1 + +``` + +# Step 1 +ctx=>click(ctx) + +```diff +- 1 ++ 2 + +``` + +# Step 2 +ctx=>click(ctx) + +```diff +- 2 ++ 3 + +``` + diff --git a/packages/run/src/__tests__/fixtures/cloudflare-adapter-pages/package.json b/packages/run/src/__tests__/fixtures/cloudflare-adapter-pages/package.json new file mode 100644 index 00000000..ba6a3cc0 --- /dev/null +++ b/packages/run/src/__tests__/fixtures/cloudflare-adapter-pages/package.json @@ -0,0 +1,5 @@ +{ + "name": "cloudflare-adapter-pages", + "version": "1.0.0", + "scripts": {} +} diff --git a/packages/run/src/__tests__/fixtures/cloudflare-adapter-pages/src/components/counter.marko b/packages/run/src/__tests__/fixtures/cloudflare-adapter-pages/src/components/counter.marko new file mode 100644 index 00000000..436a9eb5 --- /dev/null +++ b/packages/run/src/__tests__/fixtures/cloudflare-adapter-pages/src/components/counter.marko @@ -0,0 +1,11 @@ +class { + declare state: { count: number }; + onCreate() { + this.state = { count: 0 }; + } + handleIncrement() { + this.state.count++; + } +} + + \ No newline at end of file diff --git a/packages/run/src/__tests__/fixtures/cloudflare-adapter-pages/src/routes/+page.marko b/packages/run/src/__tests__/fixtures/cloudflare-adapter-pages/src/routes/+page.marko new file mode 100644 index 00000000..5dd8370a --- /dev/null +++ b/packages/run/src/__tests__/fixtures/cloudflare-adapter-pages/src/routes/+page.marko @@ -0,0 +1,12 @@ + + + + + @marko/run Test Fixture + + + + + + + diff --git a/packages/run/src/__tests__/fixtures/cloudflare-adapter-pages/test.config.ts b/packages/run/src/__tests__/fixtures/cloudflare-adapter-pages/test.config.ts new file mode 100644 index 00000000..d21d639a --- /dev/null +++ b/packages/run/src/__tests__/fixtures/cloudflare-adapter-pages/test.config.ts @@ -0,0 +1,13 @@ +import { Step, StepContext } from "../../main.test"; + +export const steps: Step[] = [ + (ctx) => click(ctx), + (ctx) => click(ctx), + (ctx) => click(ctx), +]; +// Preview requires the Wrangler CLI, which isn't available in CI. +export const skip_preview = true; + +async function click({ page }: StepContext) { + await page.click("button"); +} diff --git a/packages/run/src/__tests__/fixtures/cloudflare-adapter-pages/tsconfig.json b/packages/run/src/__tests__/fixtures/cloudflare-adapter-pages/tsconfig.json new file mode 100644 index 00000000..03ff88a5 --- /dev/null +++ b/packages/run/src/__tests__/fixtures/cloudflare-adapter-pages/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig-base.json", + "include": ["src/**/*", ".marko-run/*"], +} \ No newline at end of file diff --git a/packages/run/src/__tests__/fixtures/cloudflare-adapter-pages/vite.config.ts b/packages/run/src/__tests__/fixtures/cloudflare-adapter-pages/vite.config.ts new file mode 100644 index 00000000..594176f8 --- /dev/null +++ b/packages/run/src/__tests__/fixtures/cloudflare-adapter-pages/vite.config.ts @@ -0,0 +1,10 @@ +import marko from "@marko/run/vite"; +import adapter from "@marko/run-adapter-cloudflare"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [marko({ adapter: adapter({ mode: "pages" }) })], + server: { + hmr: false, + }, +}); diff --git a/packages/run/src/__tests__/fixtures/cloudflare-adapter-workers/.gitignore b/packages/run/src/__tests__/fixtures/cloudflare-adapter-workers/.gitignore new file mode 100644 index 00000000..1521c8b7 --- /dev/null +++ b/packages/run/src/__tests__/fixtures/cloudflare-adapter-workers/.gitignore @@ -0,0 +1 @@ +dist diff --git a/packages/run/src/__tests__/fixtures/cloudflare-adapter-workers/.marko-run/routes.d.ts b/packages/run/src/__tests__/fixtures/cloudflare-adapter-workers/.marko-run/routes.d.ts new file mode 100644 index 00000000..fb83b4f5 --- /dev/null +++ b/packages/run/src/__tests__/fixtures/cloudflare-adapter-workers/.marko-run/routes.d.ts @@ -0,0 +1,36 @@ +/* + WARNING: This file is automatically generated and any changes made to it will be overwritten without warning. + Do NOT manually edit this file or your changes will be lost. +*/ + +import { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform } from "@marko/run/namespace"; +import type * as Run from "@marko/run"; +import type { CloudflarePlatformInfo } from '@marko/run-adapter-cloudflare'; + +declare module "@marko/run" { + interface Platform extends CloudflarePlatformInfo {} + + interface AppData extends Run.DefineApp<{ + routes: { + "/": { verb: "get"; }; + } + }> {} +} + +declare module "../src/routes/+page.marko" { + namespace MarkoRun { + export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; + export type Route = Run.Routes["/"]; + export type Context = Run.MultiRouteContext & Marko.Global; + export type Handler = Run.HandlerLike; + export type GET = Run.HandlerLike; + export type HEAD = Run.HandlerLike; + export type POST = Run.HandlerLike; + export type PUT = Run.HandlerLike; + export type DELETE = Run.HandlerLike; + export type PATCH = Run.HandlerLike; + export type OPTIONS = Run.HandlerLike; + /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ + export const route: Run.HandlerTypeFn; + } +} diff --git a/packages/run/src/__tests__/fixtures/cloudflare-adapter-workers/__snapshots__/dev.expected.md b/packages/run/src/__tests__/fixtures/cloudflare-adapter-workers/__snapshots__/dev.expected.md new file mode 100644 index 00000000..1ec2ae91 --- /dev/null +++ b/packages/run/src/__tests__/fixtures/cloudflare-adapter-workers/__snapshots__/dev.expected.md @@ -0,0 +1,35 @@ +# Loading + +```html + +``` + +# Step 0 +ctx=>click(ctx) + +```diff +- 0 ++ 1 + +``` + +# Step 1 +ctx=>click(ctx) + +```diff +- 1 ++ 2 + +``` + +# Step 2 +ctx=>click(ctx) + +```diff +- 2 ++ 3 + +``` + diff --git a/packages/run/src/__tests__/fixtures/cloudflare-adapter-workers/package.json b/packages/run/src/__tests__/fixtures/cloudflare-adapter-workers/package.json new file mode 100644 index 00000000..b4198c30 --- /dev/null +++ b/packages/run/src/__tests__/fixtures/cloudflare-adapter-workers/package.json @@ -0,0 +1,5 @@ +{ + "name": "cloudflare-adapter-workers", + "version": "1.0.0", + "scripts": {} +} diff --git a/packages/run/src/__tests__/fixtures/cloudflare-adapter-workers/src/components/counter.marko b/packages/run/src/__tests__/fixtures/cloudflare-adapter-workers/src/components/counter.marko new file mode 100644 index 00000000..436a9eb5 --- /dev/null +++ b/packages/run/src/__tests__/fixtures/cloudflare-adapter-workers/src/components/counter.marko @@ -0,0 +1,11 @@ +class { + declare state: { count: number }; + onCreate() { + this.state = { count: 0 }; + } + handleIncrement() { + this.state.count++; + } +} + + \ No newline at end of file diff --git a/packages/run/src/__tests__/fixtures/cloudflare-adapter-workers/src/routes/+page.marko b/packages/run/src/__tests__/fixtures/cloudflare-adapter-workers/src/routes/+page.marko new file mode 100644 index 00000000..5dd8370a --- /dev/null +++ b/packages/run/src/__tests__/fixtures/cloudflare-adapter-workers/src/routes/+page.marko @@ -0,0 +1,12 @@ + + + + + @marko/run Test Fixture + + + + + + + diff --git a/packages/run/src/__tests__/fixtures/cloudflare-adapter-workers/test.config.ts b/packages/run/src/__tests__/fixtures/cloudflare-adapter-workers/test.config.ts new file mode 100644 index 00000000..d21d639a --- /dev/null +++ b/packages/run/src/__tests__/fixtures/cloudflare-adapter-workers/test.config.ts @@ -0,0 +1,13 @@ +import { Step, StepContext } from "../../main.test"; + +export const steps: Step[] = [ + (ctx) => click(ctx), + (ctx) => click(ctx), + (ctx) => click(ctx), +]; +// Preview requires the Wrangler CLI, which isn't available in CI. +export const skip_preview = true; + +async function click({ page }: StepContext) { + await page.click("button"); +} diff --git a/packages/run/src/__tests__/fixtures/cloudflare-adapter-workers/tsconfig.json b/packages/run/src/__tests__/fixtures/cloudflare-adapter-workers/tsconfig.json new file mode 100644 index 00000000..03ff88a5 --- /dev/null +++ b/packages/run/src/__tests__/fixtures/cloudflare-adapter-workers/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig-base.json", + "include": ["src/**/*", ".marko-run/*"], +} \ No newline at end of file diff --git a/packages/run/src/__tests__/fixtures/cloudflare-adapter-workers/vite.config.ts b/packages/run/src/__tests__/fixtures/cloudflare-adapter-workers/vite.config.ts new file mode 100644 index 00000000..604d276d --- /dev/null +++ b/packages/run/src/__tests__/fixtures/cloudflare-adapter-workers/vite.config.ts @@ -0,0 +1,10 @@ +import marko from "@marko/run/vite"; +import adapter from "@marko/run-adapter-cloudflare"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [marko({ adapter: adapter() })], + server: { + hmr: false, + }, +}); From 253219f82e9fd35399feb4287ca2868219ff0091 Mon Sep 17 00:00:00 2001 From: Ryan Turnquist Date: Wed, 1 Jul 2026 04:57:24 +0000 Subject: [PATCH 3/5] fix: address review feedback for the Cloudflare adapter - Add package.toggle.json so publish points exports/types at dist/ - Make the injected import.meta.url shim a string (.href) for the CJS build - Fix the README platform example to read from context.platform - Workers preview: only pass --config when the generated wrangler.json exists (it is skipped when the project has its own Wrangler config), otherwise let Wrangler discover the project config - Load the --env-file into the Wrangler preview environment - Throw instead of exiting the process when the Wrangler CLI is missing --- packages/adapters/cloudflare/README.md | 4 ++-- .../adapters/cloudflare/package.toggle.json | 15 ++++++++++++ .../cloudflare/scripts/importMetaURL.js | 2 +- packages/adapters/cloudflare/src/index.ts | 23 +++++++++++++------ 4 files changed, 34 insertions(+), 10 deletions(-) create mode 100644 packages/adapters/cloudflare/package.toggle.json diff --git a/packages/adapters/cloudflare/README.md b/packages/adapters/cloudflare/README.md index 2db64701..be1785b0 100644 --- a/packages/adapters/cloudflare/README.md +++ b/packages/adapters/cloudflare/README.md @@ -103,8 +103,8 @@ The Worker environment, execution context and Cloudflare request properties are ```ts import type { CloudflarePlatformInfo } from "@marko/run-adapter-cloudflare"; -export const GET = (context, { platform }) => { - const { env, ctx, cf } = platform as CloudflarePlatformInfo; +export const GET = (context) => { + const { env, ctx, cf } = context.platform as CloudflarePlatformInfo; ctx.waitUntil(logRequest(cf)); return new Response(`Hello from ${cf?.city ?? "the edge"}!`); }; diff --git a/packages/adapters/cloudflare/package.toggle.json b/packages/adapters/cloudflare/package.toggle.json new file mode 100644 index 00000000..a2f5767a --- /dev/null +++ b/packages/adapters/cloudflare/package.toggle.json @@ -0,0 +1,15 @@ +{ + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + }, + "types": "./dist/index.d.ts", + "typesVersions": { + "*": { + "*": ["./dist/index.d.ts"] + } + } +} diff --git a/packages/adapters/cloudflare/scripts/importMetaURL.js b/packages/adapters/cloudflare/scripts/importMetaURL.js index c993f7f5..b1277af3 100644 --- a/packages/adapters/cloudflare/scripts/importMetaURL.js +++ b/packages/adapters/cloudflare/scripts/importMetaURL.js @@ -1,2 +1,2 @@ import { pathToFileURL } from "url"; -export const __importMetaURL = pathToFileURL(__filename); +export const __importMetaURL = pathToFileURL(__filename).href; diff --git a/packages/adapters/cloudflare/src/index.ts b/packages/adapters/cloudflare/src/index.ts index a873d5e4..4a82d07d 100644 --- a/packages/adapters/cloudflare/src/index.ts +++ b/packages/adapters/cloudflare/src/index.ts @@ -1,5 +1,7 @@ import baseAdapter, { type Adapter } from "@marko/run/adapter"; +import { loadEnv } from "@marko/run/vite"; import { execSync, spawn } from "child_process"; +import { existsSync } from "fs"; import fs from "fs/promises"; import path from "path"; import { fileURLToPath } from "url"; @@ -68,16 +70,24 @@ export default function cloudflareAdapter(options: Options = {}): Adapter { async startPreview({ options: previewOptions }) { assertWranglerCLI(); - const { port = 3000, cwd, dir } = previewOptions; + const { port = 3000, cwd, dir, envFile } = previewOptions; + if (envFile) { + await loadEnv(envFile); + } + const publicDir = path.join(dir, "public"); + // buildEnd only generates this when the project has no Wrangler config of + // its own; otherwise let Wrangler discover the project's config. + const generatedConfig = path.join(dir, "wrangler.json"); const args = mode === "pages" ? ["pages", "dev", publicDir, "--port", port.toString()] : [ "dev", - "--config", - path.join(dir, "wrangler.json"), + ...(existsSync(generatedConfig) + ? ["--config", generatedConfig] + : []), "--port", port.toString(), ]; @@ -229,10 +239,9 @@ function parseWranglerArgs(args: string[]) { function assertWranglerCLI() { try { execSync("wrangler --version"); - } catch (error) { - console.warn( - `Wrangler CLI not found. Please install it with \`npm install -D wrangler\``, + } catch { + throw new Error( + "Wrangler CLI not found. Please install it with `npm install -D wrangler`.", ); - process.exit(1); } } From b52047211e97d5a5454444a5540ce731d251908a Mon Sep 17 00:00:00 2001 From: Ryan Turnquist Date: Wed, 1 Jul 2026 16:05:01 +0000 Subject: [PATCH 4/5] fix: don't spawn the Wrangler preview through a shell on POSIX Use `shell: true` only on Windows (where `wrangler` is a `.cmd` shim that requires it). On POSIX the CLI is spawned directly, so `close()` kills Wrangler itself instead of an orphaned shell wrapper and argument values are no longer re-parsed by the shell. Also handle the child's error event so a failed launch doesn't crash the parent. --- packages/adapters/cloudflare/src/index.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/adapters/cloudflare/src/index.ts b/packages/adapters/cloudflare/src/index.ts index 4a82d07d..3ac1c017 100644 --- a/packages/adapters/cloudflare/src/index.ts +++ b/packages/adapters/cloudflare/src/index.ts @@ -94,10 +94,18 @@ export default function cloudflareAdapter(options: Options = {}): Adapter { args.push(...parseWranglerArgs(previewOptions.args)); + // Spawn through a shell only on Windows, where `wrangler` is a `.cmd` + // shim that can't be spawned directly. On POSIX, spawning directly means + // `close()` terminates Wrangler itself (not a shell wrapper) and avoids + // the shell re-parsing the argument values. const proc = spawn("wrangler", args, { cwd, env: process.env, - shell: true, + shell: process.platform === "win32", + }); + + proc.on("error", (err) => { + console.error("Failed to start wrangler preview server:", err); }); if (process.env.NODE_ENV !== "test") { From 153dc226cf08163de8fc8af593f510d02ef566a8 Mon Sep 17 00:00:00 2001 From: Ryan Turnquist Date: Wed, 1 Jul 2026 19:14:37 +0000 Subject: [PATCH 5/5] docs: note adapter auto-discovery and add a Deploying section (Cloudflare) Clarify that Marko Run automatically uses an installed adapter with no Vite config, and add a Deploying section right after installation showing `npm run build` + `npx wrangler deploy -c dist/wrangler.json`. --- packages/adapters/cloudflare/README.md | 46 +++++++++----------------- 1 file changed, 16 insertions(+), 30 deletions(-) diff --git a/packages/adapters/cloudflare/README.md b/packages/adapters/cloudflare/README.md index be1785b0..29c4c62c 100644 --- a/packages/adapters/cloudflare/README.md +++ b/packages/adapters/cloudflare/README.md @@ -18,45 +18,30 @@ Preview and deploy [@marko/run](../../run/README.md) apps to Cloudflare Workers/ npm install @marko/run-adapter-cloudflare ``` -## Usage +That's all the setup required — Marko Run automatically discovers an installed adapter and uses it, so you **don't** need to register it in your Vite config. See [Configuration](#configuration) if you want to build for Cloudflare Pages instead of the default Workers target. -In your application's Vite config file (eg. `vite.config.js`), import and register this adapter with the `@marko/run` Vite plugin: +`wrangler` is used to preview and deploy your app; install it as a dev dependency: -```ts -import { defineConfig } from "vite"; -import marko from "@marko/run/vite"; -import cloudflareAdapter from "@marko/run-adapter-cloudflare"; - -export default defineConfig({ - plugins: [ - marko({ - adapter: cloudflareAdapter(), - }), - ], -}); +```sh +npm install -D wrangler ``` -The adapter builds for [Cloudflare Workers](https://developers.cloudflare.com/workers/) by default. `wrangler` is used to preview and deploy your app, so install it as a dev dependency: +## Deploying + +Build your app and deploy the output with the [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/): ```sh -npm install -D wrangler +npm run build +npx wrangler deploy -c dist/wrangler.json ``` -## Workers (default) - -Running `marko-run build` produces the following in your output directory (`dist` by default): +`marko-run build` produces the following in your output directory (`dist` by default): - `_worker.js` — the bundled server entry - `public/` — your app's static assets - `wrangler.json` — a generated starter config (only written when no `wrangler.toml`/`wrangler.json`/`wrangler.jsonc` exists at your project root) -Deploy with: - -```sh -wrangler deploy --config dist/wrangler.json -``` - -To use your own bindings (KV, D1, R2, secrets, custom name, routes, etc.), add a Wrangler config at the root of your project. When present, the adapter will **not** generate one — point it at the build output yourself: +To use your own bindings (KV, D1, R2, secrets, custom name, routes, etc.), add a Wrangler config at the root of your project. When present, the adapter will **not** generate one — point it at the build output yourself and deploy with `npx wrangler deploy`: ```jsonc // wrangler.jsonc @@ -72,9 +57,9 @@ To use your own bindings (KV, D1, R2, secrets, custom name, routes, etc.), add a } ``` -## Pages +## Configuration -This adapter can target [Cloudflare Pages](https://developers.cloudflare.com/pages/) instead, building a [Pages Functions "advanced mode"](https://developers.cloudflare.com/pages/functions/advanced-mode/) `_worker.js`: +Marko Run uses this adapter automatically, but you can register it in your Vite config (eg. `vite.config.js`) to pass options — for example, to target [Cloudflare Pages](https://developers.cloudflare.com/pages/) instead of the default [Workers](https://developers.cloudflare.com/workers/): ```ts import { defineConfig } from "vite"; @@ -90,10 +75,11 @@ export default defineConfig({ }); ``` -This emits `_worker.js` and a `_routes.json` into `dist/public`. Deploy the assets directory with: +In `"pages"` mode the adapter builds a [Pages Functions "advanced mode"](https://developers.cloudflare.com/pages/functions/advanced-mode/) `_worker.js` and a `_routes.json` into `dist/public`. Deploy the assets directory instead: ```sh -wrangler pages deploy dist/public +npm run build +npx wrangler pages deploy dist/public ``` ## Platform info