From 4a8a2848149f75e375d2201342b4a3e9241a0272 Mon Sep 17 00:00:00 2001 From: Ryan Turnquist Date: Tue, 30 Jun 2026 21:49:12 +0000 Subject: [PATCH 1/3] 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 4ed6b9c9255e59bc77c68c0f484d5e9e4e71152a Mon Sep 17 00:00:00 2001 From: Ryan Turnquist Date: Tue, 30 Jun 2026 22:10:51 +0000 Subject: [PATCH 2/3] 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 34198b74f6d68cab741fe890f2916e3cdef63547 Mon Sep 17 00:00:00 2001 From: Ryan Turnquist Date: Tue, 30 Jun 2026 22:44:55 +0000 Subject: [PATCH 3/3] feat: add Vercel deployment adapter Add @marko/run-adapter-vercel for deploying Marko Run apps to Vercel via the Build Output API (v3), emitting a `.vercel/output` directory. - Node.js Serverless Functions by default; `{ edge: true }` targets the Edge runtime (webworker SSR target with the edge-light export condition) - Node mode wraps the fetch handler with createMiddleware as the function handler; static assets are served by Vercel's filesystem layer - Generates functions/index.func with the appropriate .vc-config.json, copies static assets to output/static, and writes a routing config.json - Exposes platform info per runtime via VercelEdgePlatformInfo / VercelNodePlatformInfo - Adds run-package test fixtures for both modes (preview skipped; it needs the Vercel CLI) --- .changeset/vercel-adapter.md | 5 + package-lock.json | 12 ++ packages/adapters/vercel/README.md | 75 +++++++ packages/adapters/vercel/package.json | 33 +++ packages/adapters/vercel/scripts/build.ts | 51 +++++ .../adapters/vercel/scripts/importMetaURL.js | 2 + .../adapters/vercel/src/default-edge-entry.ts | 13 ++ .../adapters/vercel/src/default-node-entry.ts | 7 + packages/adapters/vercel/src/index.ts | 194 ++++++++++++++++++ packages/adapters/vercel/src/types.ts | 20 ++ packages/adapters/vercel/tsconfig.json | 10 + .../fixtures/vercel-adapter-edge/.gitignore | 2 + .../.marko-run/routes.d.ts | 36 ++++ .../__snapshots__/dev.expected.md | 35 ++++ .../fixtures/vercel-adapter-edge/package.json | 5 + .../src/components/counter.marko | 11 + .../src/routes/+page.marko | 12 ++ .../vercel-adapter-edge/test.config.ts | 13 ++ .../vercel-adapter-edge/tsconfig.json | 4 + .../vercel-adapter-edge/vite.config.ts | 10 + .../fixtures/vercel-adapter-node/.gitignore | 2 + .../.marko-run/routes.d.ts | 36 ++++ .../__snapshots__/dev.expected.md | 35 ++++ .../fixtures/vercel-adapter-node/package.json | 5 + .../src/components/counter.marko | 11 + .../src/routes/+page.marko | 12 ++ .../vercel-adapter-node/test.config.ts | 13 ++ .../vercel-adapter-node/tsconfig.json | 4 + .../vercel-adapter-node/vite.config.ts | 10 + 29 files changed, 678 insertions(+) create mode 100644 .changeset/vercel-adapter.md create mode 100644 packages/adapters/vercel/README.md create mode 100644 packages/adapters/vercel/package.json create mode 100644 packages/adapters/vercel/scripts/build.ts create mode 100644 packages/adapters/vercel/scripts/importMetaURL.js create mode 100644 packages/adapters/vercel/src/default-edge-entry.ts create mode 100644 packages/adapters/vercel/src/default-node-entry.ts create mode 100644 packages/adapters/vercel/src/index.ts create mode 100644 packages/adapters/vercel/src/types.ts create mode 100644 packages/adapters/vercel/tsconfig.json create mode 100644 packages/run/src/__tests__/fixtures/vercel-adapter-edge/.gitignore create mode 100644 packages/run/src/__tests__/fixtures/vercel-adapter-edge/.marko-run/routes.d.ts create mode 100644 packages/run/src/__tests__/fixtures/vercel-adapter-edge/__snapshots__/dev.expected.md create mode 100644 packages/run/src/__tests__/fixtures/vercel-adapter-edge/package.json create mode 100644 packages/run/src/__tests__/fixtures/vercel-adapter-edge/src/components/counter.marko create mode 100644 packages/run/src/__tests__/fixtures/vercel-adapter-edge/src/routes/+page.marko create mode 100644 packages/run/src/__tests__/fixtures/vercel-adapter-edge/test.config.ts create mode 100644 packages/run/src/__tests__/fixtures/vercel-adapter-edge/tsconfig.json create mode 100644 packages/run/src/__tests__/fixtures/vercel-adapter-edge/vite.config.ts create mode 100644 packages/run/src/__tests__/fixtures/vercel-adapter-node/.gitignore create mode 100644 packages/run/src/__tests__/fixtures/vercel-adapter-node/.marko-run/routes.d.ts create mode 100644 packages/run/src/__tests__/fixtures/vercel-adapter-node/__snapshots__/dev.expected.md create mode 100644 packages/run/src/__tests__/fixtures/vercel-adapter-node/package.json create mode 100644 packages/run/src/__tests__/fixtures/vercel-adapter-node/src/components/counter.marko create mode 100644 packages/run/src/__tests__/fixtures/vercel-adapter-node/src/routes/+page.marko create mode 100644 packages/run/src/__tests__/fixtures/vercel-adapter-node/test.config.ts create mode 100644 packages/run/src/__tests__/fixtures/vercel-adapter-node/tsconfig.json create mode 100644 packages/run/src/__tests__/fixtures/vercel-adapter-node/vite.config.ts diff --git a/.changeset/vercel-adapter.md b/.changeset/vercel-adapter.md new file mode 100644 index 00000000..7ef227d4 --- /dev/null +++ b/.changeset/vercel-adapter.md @@ -0,0 +1,5 @@ +--- +"@marko/run-adapter-vercel": minor +--- + +Add a Vercel adapter for deploying @marko/run apps to Vercel Node.js Serverless Functions (default) or Edge Functions via the Build Output API. diff --git a/package-lock.json b/package-lock.json index 82313657..c872c658 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2507,6 +2507,10 @@ "resolved": "packages/adapters/static", "link": true }, + "node_modules/@marko/run-adapter-vercel": { + "resolved": "packages/adapters/vercel", + "link": true + }, "node_modules/@marko/run-explorer": { "resolved": "packages/explorer", "link": true @@ -26046,6 +26050,14 @@ "@marko/run": "^0.7.7||^0.8||^0.9||^0.10" } }, + "packages/adapters/vercel": { + "name": "@marko/run-adapter-vercel", + "version": "0.1.0", + "license": "MIT", + "peerDependencies": { + "@marko/run": "^0.7||^0.8||^0.9||^0.10" + } + }, "packages/explorer": { "name": "@marko/run-explorer", "version": "2.0.3", diff --git a/packages/adapters/vercel/README.md b/packages/adapters/vercel/README.md new file mode 100644 index 00000000..fca468bd --- /dev/null +++ b/packages/adapters/vercel/README.md @@ -0,0 +1,75 @@ +

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

+ +Preview and deploy [@marko/run](../../run/README.md) apps to Vercel Functions/Edge Functions + +## Installation + +```sh +npm install @marko/run-adapter-vercel +``` + +## 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 vercelAdapter from "@marko/run-adapter-vercel"; + +export default defineConfig({ + plugins: [ + marko({ + adapter: vercelAdapter(), + }), + ], +}); +``` + +Running `marko-run build` produces a [Vercel Build Output API](https://vercel.com/docs/build-output-api/v3) directory at `.vercel/output`, which Vercel uses directly when deploying. Set your project's build command to `marko-run build` and Vercel will pick it up — no output directory configuration required. + +## Edge Functions + +This adapter can be configured to build for Vercel [Edge Functions](https://vercel.com/docs/functions/edge-functions) instead of the default Node.js Serverless Functions: + +```ts +import { defineConfig } from "vite"; +import marko from "@marko/run/vite"; +import vercelAdapter from "@marko/run-adapter-vercel"; + +export default defineConfig({ + plugins: [ + marko({ + adapter: vercelAdapter({ edge: true }), + }), + ], +}); +``` + +## Platform info + +The route handler `platform` argument differs by runtime. + +For Edge Functions it exposes the request context: + +```ts +import type { VercelEdgePlatformInfo } from "@marko/run-adapter-vercel"; + +export const GET = (context, { platform }) => { + const { waitUntil } = platform as VercelEdgePlatformInfo; + waitUntil(logRequest()); + return new Response("Hello from the edge!"); +}; +``` + +For Node.js Serverless Functions it exposes the underlying Node request and response objects (`VercelNodePlatformInfo`). diff --git a/packages/adapters/vercel/package.json b/packages/adapters/vercel/package.json new file mode 100644 index 00000000..665fe43f --- /dev/null +++ b/packages/adapters/vercel/package.json @@ -0,0 +1,33 @@ +{ + "name": "@marko/run-adapter-vercel", + "version": "0.1.0", + "description": "Preview and deploy @marko/run apps to Vercel Functions/Edge Functions", + "keywords": [ + "marko" + ], + "homepage": "https://github.com/marko-js/run/tree/main/packages/adapters/vercel", + "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": "" + }, + "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/vercel/scripts/build.ts b/packages/adapters/vercel/scripts/build.ts new file mode 100644 index 00000000..2d7fcb91 --- /dev/null +++ b/packages/adapters/vercel/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-edge-entry.ts", "src/default-node-entry.ts"], + format: "esm", + outExtension: { ".js": ".mjs" }, + }), +]); diff --git a/packages/adapters/vercel/scripts/importMetaURL.js b/packages/adapters/vercel/scripts/importMetaURL.js new file mode 100644 index 00000000..c993f7f5 --- /dev/null +++ b/packages/adapters/vercel/scripts/importMetaURL.js @@ -0,0 +1,2 @@ +import { pathToFileURL } from "url"; +export const __importMetaURL = pathToFileURL(__filename); diff --git a/packages/adapters/vercel/src/default-edge-entry.ts b/packages/adapters/vercel/src/default-edge-entry.ts new file mode 100644 index 00000000..66e24e68 --- /dev/null +++ b/packages/adapters/vercel/src/default-edge-entry.ts @@ -0,0 +1,13 @@ +import { fetch } from "@marko/run/router"; + +import type { VercelEdgePlatformInfo } from "./types"; + +export default async function ( + request: Request, + context: VercelEdgePlatformInfo, +) { + return ( + (await fetch(request, context)) || + new Response(null, { status: 404 }) + ); +} diff --git a/packages/adapters/vercel/src/default-node-entry.ts b/packages/adapters/vercel/src/default-node-entry.ts new file mode 100644 index 00000000..8fbcfec4 --- /dev/null +++ b/packages/adapters/vercel/src/default-node-entry.ts @@ -0,0 +1,7 @@ +import { createMiddleware } from "@marko/run/adapter/middleware"; +import { fetch } from "@marko/run/router"; + +// Vercel's Node.js runtime invokes the default export as a standard +// `(req, res)` request listener. Static assets are served by Vercel's +// filesystem layer, so the function only needs to handle dynamic routes. +export default createMiddleware(fetch); diff --git a/packages/adapters/vercel/src/index.ts b/packages/adapters/vercel/src/index.ts new file mode 100644 index 00000000..8f03a033 --- /dev/null +++ b/packages/adapters/vercel/src/index.ts @@ -0,0 +1,194 @@ +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 { VercelEdgePlatformInfo, VercelNodePlatformInfo } from "./types"; + +const __dirname = fileURLToPath(path.dirname(import.meta.url)); +const defaultEdgeEntry = path.join(__dirname, "default-edge-entry"); +const defaultNodeEntry = path.join(__dirname, "default-node-entry"); + +/** Used for the generated Node Serverless Function. */ +const NODE_RUNTIME = "nodejs20.x"; + +export interface Options { + /** + * Build for Vercel's Edge runtime instead of the Node.js Serverless + * runtime. Edge Functions run on a web-standard runtime at the edge. + */ + edge?: boolean; +} + +export default function vercelAdapter(options: Options = {}): Adapter { + const { edge = false } = options; + const { startDev } = baseAdapter(); + const defaultEntry = edge ? defaultEdgeEntry : defaultNodeEntry; + + return { + name: "vercel-adapter", + + viteConfig(config) { + if (config.build?.ssr) { + return edge + ? { + ssr: { + target: "webworker", + resolve: { + dedupe: ["marko"], + conditions: [ + "edge-light", + "worker", + "browser", + "import", + "require", + "production", + "default", + ], + }, + noExternal: true, + }, + } + : { + ssr: { + target: "node", + noExternal: true, + }, + }; + } + }, + + getEntryFile() { + return defaultEntry; + }, + + startDev(event) { + return startDev!({ + ...event, + entry: event.entry === defaultEntry ? undefined : event.entry, + }); + }, + + async startPreview({ options: previewOptions }) { + assertVercelCLI(); + + const { port = 3000, cwd } = previewOptions; + + const args = ["dev", "--listen", port.toString()]; + args.push(...parseVercelArgs(previewOptions.args)); + + const proc = spawn("vercel", 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 server entry. + const publicDir = path.resolve(config.root, config.build.outDir); + + // Assemble a Vercel Build Output API (v3) directory. Vercel uses this + // directly when it exists after the build. + // https://vercel.com/docs/build-output-api/v3 + const outputDir = path.join(config.root, ".vercel", "output"); + const staticDir = path.join(outputDir, "static"); + const funcDir = path.join(outputDir, "functions", "index.func"); + + await fs.rm(outputDir, { recursive: true, force: true }); + await fs.mkdir(funcDir, { recursive: true }); + await fs.cp(publicDir, staticDir, { recursive: true }); + + await fs.rename(serverEntry, path.join(funcDir, "index.js")); + await fs.writeFile( + path.join(funcDir, "package.json"), + JSON.stringify({ type: "module" }, null, 2), + ); + await fs.writeFile( + path.join(funcDir, ".vc-config.json"), + JSON.stringify( + edge + ? { runtime: "edge", entrypoint: "index.js" } + : { + runtime: NODE_RUNTIME, + handler: "index.js", + launcherType: "Nodejs", + shouldAddHelpers: false, + }, + null, + 2, + ), + ); + + await fs.writeFile( + path.join(outputDir, "config.json"), + JSON.stringify( + { + version: 3, + routes: [ + { handle: "filesystem" }, + { src: "/(.*)", dest: "/index" }, + ], + }, + null, + 2, + ), + ); + }, + + typeInfo(writer) { + if (edge) { + writer( + `import type { VercelEdgePlatformInfo } from '@marko/run-adapter-vercel';`, + ); + return "VercelEdgePlatformInfo"; + } + writer( + `import type { VercelNodePlatformInfo } from '@marko/run-adapter-vercel';`, + ); + return "VercelNodePlatformInfo"; + }, + }; +} + +const devFlags = new RegExp( + ["debug", "env=.+", "build-env=.+", "yes", "token=.+", "scope=.+"] + .map((flag) => `--${flag}`) + .join("|"), +); + +function parseVercelArgs(args: string[]) { + return args.filter((arg) => devFlags.test(arg)); +} + +function assertVercelCLI() { + try { + execSync("vercel --version"); + } catch (error) { + console.warn( + `Vercel CLI not found. Please install it with \`npm install -g vercel\``, + ); + process.exit(1); + } +} diff --git a/packages/adapters/vercel/src/types.ts b/packages/adapters/vercel/src/types.ts new file mode 100644 index 00000000..36023c2c --- /dev/null +++ b/packages/adapters/vercel/src/types.ts @@ -0,0 +1,20 @@ +import type { NodePlatformInfo } from "@marko/run/adapter"; + +/** + * The platform object passed to Marko Run route handlers when deployed as a + * Vercel Edge Function. Matches the Edge runtime's request context. + */ +export interface VercelEdgePlatformInfo { + /** + * Extend the lifetime of the function until the given promise settles, + * without blocking the response. + */ + waitUntil(promise: Promise): void; +} + +/** + * The platform object passed to Marko Run route handlers when deployed as a + * Vercel (Node.js) Serverless Function. Exposes the underlying Node request + * and response objects. + */ +export interface VercelNodePlatformInfo extends NodePlatformInfo {} diff --git a/packages/adapters/vercel/tsconfig.json b/packages/adapters/vercel/tsconfig.json new file mode 100644 index 00000000..ed8bbf1a --- /dev/null +++ b/packages/adapters/vercel/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" + } +} diff --git a/packages/run/src/__tests__/fixtures/vercel-adapter-edge/.gitignore b/packages/run/src/__tests__/fixtures/vercel-adapter-edge/.gitignore new file mode 100644 index 00000000..c49ff664 --- /dev/null +++ b/packages/run/src/__tests__/fixtures/vercel-adapter-edge/.gitignore @@ -0,0 +1,2 @@ +dist +.vercel diff --git a/packages/run/src/__tests__/fixtures/vercel-adapter-edge/.marko-run/routes.d.ts b/packages/run/src/__tests__/fixtures/vercel-adapter-edge/.marko-run/routes.d.ts new file mode 100644 index 00000000..ddf25f4f --- /dev/null +++ b/packages/run/src/__tests__/fixtures/vercel-adapter-edge/.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 { VercelEdgePlatformInfo } from '@marko/run-adapter-vercel'; + +declare module "@marko/run" { + interface Platform extends VercelEdgePlatformInfo {} + + 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/vercel-adapter-edge/__snapshots__/dev.expected.md b/packages/run/src/__tests__/fixtures/vercel-adapter-edge/__snapshots__/dev.expected.md new file mode 100644 index 00000000..1ec2ae91 --- /dev/null +++ b/packages/run/src/__tests__/fixtures/vercel-adapter-edge/__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/vercel-adapter-edge/package.json b/packages/run/src/__tests__/fixtures/vercel-adapter-edge/package.json new file mode 100644 index 00000000..f123febd --- /dev/null +++ b/packages/run/src/__tests__/fixtures/vercel-adapter-edge/package.json @@ -0,0 +1,5 @@ +{ + "name": "vercel-adapter-edge", + "version": "1.0.0", + "scripts": {} +} diff --git a/packages/run/src/__tests__/fixtures/vercel-adapter-edge/src/components/counter.marko b/packages/run/src/__tests__/fixtures/vercel-adapter-edge/src/components/counter.marko new file mode 100644 index 00000000..436a9eb5 --- /dev/null +++ b/packages/run/src/__tests__/fixtures/vercel-adapter-edge/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/vercel-adapter-edge/src/routes/+page.marko b/packages/run/src/__tests__/fixtures/vercel-adapter-edge/src/routes/+page.marko new file mode 100644 index 00000000..5dd8370a --- /dev/null +++ b/packages/run/src/__tests__/fixtures/vercel-adapter-edge/src/routes/+page.marko @@ -0,0 +1,12 @@ + + + + + @marko/run Test Fixture + + + + + + + diff --git a/packages/run/src/__tests__/fixtures/vercel-adapter-edge/test.config.ts b/packages/run/src/__tests__/fixtures/vercel-adapter-edge/test.config.ts new file mode 100644 index 00000000..939b8cd2 --- /dev/null +++ b/packages/run/src/__tests__/fixtures/vercel-adapter-edge/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 Vercel 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/vercel-adapter-edge/tsconfig.json b/packages/run/src/__tests__/fixtures/vercel-adapter-edge/tsconfig.json new file mode 100644 index 00000000..03ff88a5 --- /dev/null +++ b/packages/run/src/__tests__/fixtures/vercel-adapter-edge/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/vercel-adapter-edge/vite.config.ts b/packages/run/src/__tests__/fixtures/vercel-adapter-edge/vite.config.ts new file mode 100644 index 00000000..c6175af5 --- /dev/null +++ b/packages/run/src/__tests__/fixtures/vercel-adapter-edge/vite.config.ts @@ -0,0 +1,10 @@ +import marko from "@marko/run/vite"; +import adapter from "@marko/run-adapter-vercel"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [marko({ adapter: adapter({ edge: true }) })], + server: { + hmr: false, + }, +}); diff --git a/packages/run/src/__tests__/fixtures/vercel-adapter-node/.gitignore b/packages/run/src/__tests__/fixtures/vercel-adapter-node/.gitignore new file mode 100644 index 00000000..c49ff664 --- /dev/null +++ b/packages/run/src/__tests__/fixtures/vercel-adapter-node/.gitignore @@ -0,0 +1,2 @@ +dist +.vercel diff --git a/packages/run/src/__tests__/fixtures/vercel-adapter-node/.marko-run/routes.d.ts b/packages/run/src/__tests__/fixtures/vercel-adapter-node/.marko-run/routes.d.ts new file mode 100644 index 00000000..13e057f6 --- /dev/null +++ b/packages/run/src/__tests__/fixtures/vercel-adapter-node/.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 { VercelNodePlatformInfo } from '@marko/run-adapter-vercel'; + +declare module "@marko/run" { + interface Platform extends VercelNodePlatformInfo {} + + 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/vercel-adapter-node/__snapshots__/dev.expected.md b/packages/run/src/__tests__/fixtures/vercel-adapter-node/__snapshots__/dev.expected.md new file mode 100644 index 00000000..1ec2ae91 --- /dev/null +++ b/packages/run/src/__tests__/fixtures/vercel-adapter-node/__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/vercel-adapter-node/package.json b/packages/run/src/__tests__/fixtures/vercel-adapter-node/package.json new file mode 100644 index 00000000..075a7cad --- /dev/null +++ b/packages/run/src/__tests__/fixtures/vercel-adapter-node/package.json @@ -0,0 +1,5 @@ +{ + "name": "vercel-adapter-node", + "version": "1.0.0", + "scripts": {} +} diff --git a/packages/run/src/__tests__/fixtures/vercel-adapter-node/src/components/counter.marko b/packages/run/src/__tests__/fixtures/vercel-adapter-node/src/components/counter.marko new file mode 100644 index 00000000..436a9eb5 --- /dev/null +++ b/packages/run/src/__tests__/fixtures/vercel-adapter-node/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/vercel-adapter-node/src/routes/+page.marko b/packages/run/src/__tests__/fixtures/vercel-adapter-node/src/routes/+page.marko new file mode 100644 index 00000000..5dd8370a --- /dev/null +++ b/packages/run/src/__tests__/fixtures/vercel-adapter-node/src/routes/+page.marko @@ -0,0 +1,12 @@ + + + + + @marko/run Test Fixture + + + + + + + diff --git a/packages/run/src/__tests__/fixtures/vercel-adapter-node/test.config.ts b/packages/run/src/__tests__/fixtures/vercel-adapter-node/test.config.ts new file mode 100644 index 00000000..939b8cd2 --- /dev/null +++ b/packages/run/src/__tests__/fixtures/vercel-adapter-node/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 Vercel 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/vercel-adapter-node/tsconfig.json b/packages/run/src/__tests__/fixtures/vercel-adapter-node/tsconfig.json new file mode 100644 index 00000000..03ff88a5 --- /dev/null +++ b/packages/run/src/__tests__/fixtures/vercel-adapter-node/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/vercel-adapter-node/vite.config.ts b/packages/run/src/__tests__/fixtures/vercel-adapter-node/vite.config.ts new file mode 100644 index 00000000..6bd8def2 --- /dev/null +++ b/packages/run/src/__tests__/fixtures/vercel-adapter-node/vite.config.ts @@ -0,0 +1,10 @@ +import marko from "@marko/run/vite"; +import adapter from "@marko/run-adapter-vercel"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [marko({ adapter: adapter() })], + server: { + hmr: false, + }, +});