From 5f22cb2bf95e9a1966d9a4fbb59210d94c6e2f2a Mon Sep 17 00:00:00 2001 From: Ryan Turnquist Date: Wed, 1 Jul 2026 03:33:52 +0000 Subject: [PATCH 1/3] feat: add AWS Lambda deployment adapter Add @marko/run-adapter-aws-lambda for deploying Marko Run apps to AWS Lambda behind a Function URL or API Gateway HTTP API. - Builds a self-contained handler (dist/index.mjs, exported as `handler`) for the payload format version 2.0; converts the event to a web Request and the Response back to a base64-encoded Lambda result (with cookies) - Serves the bundled static assets from dist/public when no route matches (immutable caching for /assets, path traversal guard) - `marko-run preview` runs the handler behind a local HTTP server, so it can be tested without deploying - Exposes the raw event and invocation context via AWSLambdaPlatformInfo - Adds a run-package fixture covering both dev and preview --- .changeset/aws-lambda-adapter.md | 5 + cspell.json | 2 + package-lock.json | 447 ++++++++++++++++++ packages/adapters/aws-lambda/README.md | 66 +++ packages/adapters/aws-lambda/package.json | 33 ++ packages/adapters/aws-lambda/scripts/build.ts | 51 ++ .../aws-lambda/scripts/importMetaURL.js | 2 + .../adapters/aws-lambda/src/default-entry.ts | 150 ++++++ packages/adapters/aws-lambda/src/index.ts | 186 ++++++++ packages/adapters/aws-lambda/src/types.ts | 67 +++ packages/adapters/aws-lambda/tsconfig.json | 10 + .../fixtures/aws-lambda-adapter/.gitignore | 1 + .../aws-lambda-adapter/.marko-run/routes.d.ts | 36 ++ .../__snapshots__/dev.expected.md | 35 ++ .../__snapshots__/preview.expected.md | 35 ++ .../fixtures/aws-lambda-adapter/package.json | 5 + .../src/components/counter.marko | 11 + .../aws-lambda-adapter/src/routes/+page.marko | 12 + .../aws-lambda-adapter/test.config.ts | 11 + .../fixtures/aws-lambda-adapter/tsconfig.json | 4 + .../aws-lambda-adapter/vite.config.ts | 10 + 21 files changed, 1179 insertions(+) create mode 100644 .changeset/aws-lambda-adapter.md create mode 100644 packages/adapters/aws-lambda/README.md create mode 100644 packages/adapters/aws-lambda/package.json create mode 100644 packages/adapters/aws-lambda/scripts/build.ts create mode 100644 packages/adapters/aws-lambda/scripts/importMetaURL.js create mode 100644 packages/adapters/aws-lambda/src/default-entry.ts create mode 100644 packages/adapters/aws-lambda/src/index.ts create mode 100644 packages/adapters/aws-lambda/src/types.ts create mode 100644 packages/adapters/aws-lambda/tsconfig.json create mode 100644 packages/run/src/__tests__/fixtures/aws-lambda-adapter/.gitignore create mode 100644 packages/run/src/__tests__/fixtures/aws-lambda-adapter/.marko-run/routes.d.ts create mode 100644 packages/run/src/__tests__/fixtures/aws-lambda-adapter/__snapshots__/dev.expected.md create mode 100644 packages/run/src/__tests__/fixtures/aws-lambda-adapter/__snapshots__/preview.expected.md create mode 100644 packages/run/src/__tests__/fixtures/aws-lambda-adapter/package.json create mode 100644 packages/run/src/__tests__/fixtures/aws-lambda-adapter/src/components/counter.marko create mode 100644 packages/run/src/__tests__/fixtures/aws-lambda-adapter/src/routes/+page.marko create mode 100644 packages/run/src/__tests__/fixtures/aws-lambda-adapter/test.config.ts create mode 100644 packages/run/src/__tests__/fixtures/aws-lambda-adapter/tsconfig.json create mode 100644 packages/run/src/__tests__/fixtures/aws-lambda-adapter/vite.config.ts diff --git a/.changeset/aws-lambda-adapter.md b/.changeset/aws-lambda-adapter.md new file mode 100644 index 00000000..e31e2b9c --- /dev/null +++ b/.changeset/aws-lambda-adapter.md @@ -0,0 +1,5 @@ +--- +"@marko/run-adapter-aws-lambda": minor +--- + +Add an AWS Lambda adapter for deploying @marko/run apps to Lambda (Function URLs / API Gateway HTTP API, payload format v2), including static asset serving and a local preview server. diff --git a/cspell.json b/cspell.json index 21f9d12d..ea4cd632 100644 --- a/cspell.json +++ b/cspell.json @@ -4,6 +4,8 @@ "words": [ "marko", "markojs", + "fileb", + "Millis", "codegen", "Vite", "lightgray", diff --git a/package-lock.json b/package-lock.json index a01363b5..dd693dfc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2485,6 +2485,10 @@ "resolved": "packages/run", "link": true }, + "node_modules/@marko/run-adapter-aws-lambda": { + "resolved": "packages/adapters/aws-lambda", + "link": true + }, "node_modules/@marko/run-adapter-netlify": { "resolved": "packages/adapters/netlify", "link": true @@ -14038,6 +14042,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 +17662,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 +21457,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 +25995,14 @@ "url": "https://github.com/sponsors/colinhacks" } }, + "packages/adapters/aws-lambda": { + "name": "@marko/run-adapter-aws-lambda", + "version": "0.1.0", + "license": "MIT", + "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/aws-lambda/README.md b/packages/adapters/aws-lambda/README.md new file mode 100644 index 00000000..93996393 --- /dev/null +++ b/packages/adapters/aws-lambda/README.md @@ -0,0 +1,66 @@ +

+ + + + + Marko Run Logo + +
+ @marko/run-adapter-aws-lambda +
+

+ +Preview and deploy [@marko/run](../../run/README.md) apps to AWS Lambda + +## Installation + +```sh +npm install @marko/run-adapter-aws-lambda +``` + +## 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 awsLambdaAdapter from "@marko/run-adapter-aws-lambda"; + +export default defineConfig({ + plugins: [ + marko({ + adapter: awsLambdaAdapter(), + }), + ], +}); +``` + +Running `marko-run build` produces a self-contained Lambda handler at `dist/index.mjs` (exported as `handler`) plus the static assets in `dist/public`. The handler targets the [API Gateway HTTP API / Lambda Function URL payload format version 2.0](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html), and serves the bundled static assets when no route matches. + +`marko-run preview` runs the handler behind a local HTTP server so you can test it without deploying. + +## Deploying + +Package the `dist` directory and deploy it as a Lambda function, using `index.handler` as the handler and a Node.js runtime (eg. `nodejs20.x`). Wire it up to a [Function URL](https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html) or an API Gateway HTTP API. For example, with the AWS CLI: + +```sh +marko-run build +( cd dist && zip -r ../function.zip . ) +aws lambda update-function-code --function-name my-app --zip-file fileb://function.zip +``` + +Any IaC tool works too (AWS SAM, CDK, Serverless Framework, Terraform) — point the function's handler at `index.handler` and its code at the `dist` directory. + +## Platform info + +The raw Lambda event and invocation context are available on the `platform` argument of your route handlers: + +```ts +import type { AWSLambdaPlatformInfo } from "@marko/run-adapter-aws-lambda"; + +export const GET = (context, { platform }) => { + const { event, context: lambdaContext } = platform as AWSLambdaPlatformInfo; + return new Response(`Request id: ${lambdaContext.awsRequestId}`); +}; +``` diff --git a/packages/adapters/aws-lambda/package.json b/packages/adapters/aws-lambda/package.json new file mode 100644 index 00000000..5795f394 --- /dev/null +++ b/packages/adapters/aws-lambda/package.json @@ -0,0 +1,33 @@ +{ + "name": "@marko/run-adapter-aws-lambda", + "version": "0.1.0", + "description": "Preview and deploy @marko/run apps to AWS Lambda (Function URLs / API Gateway HTTP API)", + "keywords": [ + "marko" + ], + "homepage": "https://github.com/marko-js/run/tree/main/packages/adapters/aws-lambda", + "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/aws-lambda/scripts/build.ts b/packages/adapters/aws-lambda/scripts/build.ts new file mode 100644 index 00000000..d70f08ec --- /dev/null +++ b/packages/adapters/aws-lambda/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/aws-lambda/scripts/importMetaURL.js b/packages/adapters/aws-lambda/scripts/importMetaURL.js new file mode 100644 index 00000000..c993f7f5 --- /dev/null +++ b/packages/adapters/aws-lambda/scripts/importMetaURL.js @@ -0,0 +1,2 @@ +import { pathToFileURL } from "url"; +export const __importMetaURL = pathToFileURL(__filename); diff --git a/packages/adapters/aws-lambda/src/default-entry.ts b/packages/adapters/aws-lambda/src/default-entry.ts new file mode 100644 index 00000000..32c4ab70 --- /dev/null +++ b/packages/adapters/aws-lambda/src/default-entry.ts @@ -0,0 +1,150 @@ +import { readFile } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; + +import { fetch } from "@marko/run/router"; + +import type { + APIGatewayProxyEventV2, + APIGatewayProxyResultV2, + AWSLambdaPlatformInfo, + LambdaContext, +} from "./types"; + +const publicDir = new URL("./public/", import.meta.url); + +const mimeTypes: Record = { + html: "text/html; charset=utf-8", + js: "text/javascript; charset=utf-8", + mjs: "text/javascript; charset=utf-8", + css: "text/css; charset=utf-8", + json: "application/json; charset=utf-8", + map: "application/json; charset=utf-8", + svg: "image/svg+xml", + png: "image/png", + jpg: "image/jpeg", + jpeg: "image/jpeg", + gif: "image/gif", + webp: "image/webp", + avif: "image/avif", + ico: "image/x-icon", + woff: "font/woff", + woff2: "font/woff2", + ttf: "font/ttf", + otf: "font/otf", + wasm: "application/wasm", + txt: "text/plain; charset=utf-8", + xml: "application/xml", +}; + +function eventToRequest(event: APIGatewayProxyEventV2): Request { + const { method } = event.requestContext.http; + const headers = new Headers(); + for (const [key, value] of Object.entries(event.headers)) { + if (value !== undefined) { + headers.set(key, value); + } + } + if (event.cookies?.length) { + headers.set("cookie", event.cookies.join("; ")); + } + + const host = + headers.get("x-forwarded-host") || + headers.get("host") || + event.requestContext.domainName; + const proto = headers.get("x-forwarded-proto") || "https"; + const query = event.rawQueryString ? `?${event.rawQueryString}` : ""; + const url = `${proto}://${host}${event.rawPath}${query}`; + + let body: Uint8Array | string | undefined; + if (method !== "GET" && method !== "HEAD" && event.body != null) { + body = event.isBase64Encoded + ? Buffer.from(event.body, "base64") + : event.body; + } + + return new Request(url, { method, headers, body }); +} + +async function responseToResult( + response: Response, +): Promise { + const headers: Record = {}; + response.headers.forEach((value, key) => { + if (key !== "set-cookie") { + headers[key] = value; + } + }); + const cookies = + "getSetCookie" in response.headers + ? response.headers.getSetCookie() + : undefined; + const body = Buffer.from(await response.arrayBuffer()); + + return { + statusCode: response.status, + headers, + cookies: cookies?.length ? cookies : undefined, + body: body.toString("base64"), + isBase64Encoded: true, + }; +} + +async function serveStatic( + pathname: string, +): Promise { + if (pathname.endsWith("/")) { + pathname += "index.html"; + } + + const fileUrl = new URL("." + pathname, publicDir); + // Guard against path traversal outside of the public directory. + if (!fileUrl.href.startsWith(publicDir.href)) { + return null; + } + + let data: Buffer; + try { + data = await readFile(fileURLToPath(fileUrl)); + } catch { + return null; + } + + const ext = pathname.slice(pathname.lastIndexOf(".") + 1).toLowerCase(); + const headers: Record = { + "content-type": mimeTypes[ext] || "application/octet-stream", + }; + if (pathname.startsWith("/assets/")) { + headers["cache-control"] = "public, max-age=31536000, immutable"; + } + + return { + statusCode: 200, + headers, + body: data.toString("base64"), + isBase64Encoded: true, + }; +} + +export async function handler( + event: APIGatewayProxyEventV2, + context: LambdaContext, +): Promise { + const request = eventToRequest(event); + const platform: AWSLambdaPlatformInfo = { event, context }; + + // Let matched routes respond first; only fall back to static assets when the + // router reports no match (the runtime returns a 404 response in that case). + const response = await fetch(request, platform); + if (response && response.status !== 404) { + return responseToResult(response); + } + + const { pathname } = new URL(request.url); + const staticResult = await serveStatic(decodeURIComponent(pathname)); + if (staticResult) { + return staticResult; + } + + return response ? responseToResult(response) : { statusCode: 404, body: "" }; +} diff --git a/packages/adapters/aws-lambda/src/index.ts b/packages/adapters/aws-lambda/src/index.ts new file mode 100644 index 00000000..cff970d2 --- /dev/null +++ b/packages/adapters/aws-lambda/src/index.ts @@ -0,0 +1,186 @@ +import { createServer, type IncomingMessage } from "node:http"; +import type { AddressInfo } from "node:net"; +import path from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +import baseAdapter, { type Adapter } from "@marko/run/adapter"; +import { loadEnv } from "@marko/run/vite"; + +import type { + APIGatewayProxyEventV2, + APIGatewayProxyResultV2, + LambdaContext, +} from "./types"; + +export type { + APIGatewayProxyEventV2, + APIGatewayProxyResultV2, + AWSLambdaPlatformInfo, + LambdaContext, +} from "./types"; + +const __dirname = fileURLToPath(path.dirname(import.meta.url)); +const defaultEntry = path.join(__dirname, "default-entry"); + +type Handler = ( + event: APIGatewayProxyEventV2, + context: LambdaContext, +) => Promise; + +export default function awsLambdaAdapter(): Adapter { + const { startDev } = baseAdapter(); + + return { + name: "aws-lambda-adapter", + + viteConfig(config) { + if (config.build?.ssr) { + return { + ssr: { + target: "node", + // Bundle everything so the Lambda deployment package is + // self-contained and needs no `node_modules`. + noExternal: true, + }, + }; + } + }, + + getEntryFile() { + return defaultEntry; + }, + + startDev(event) { + return startDev!({ + ...event, + entry: event.entry === defaultEntry ? undefined : event.entry, + }); + }, + + async startPreview({ entry, options }) { + const { port = 3000, dir, envFile } = options; + const entryFile = entry || path.join(dir, "index.mjs"); + + if (envFile) { + await loadEnv(envFile); + } + + // Invoke the built Lambda handler behind a local HTTP server so the app + // can be previewed without deploying (or running SAM). + const { handler } = (await import(pathToFileURL(entryFile).href)) as { + handler: Handler; + }; + + const server = createServer((req, res) => { + readBody(req) + .then(async (body) => { + const result = await handler(nodeRequestToEvent(req, body), { + functionName: "marko-run-preview", + functionVersion: "$LATEST", + invokedFunctionArn: "", + memoryLimitInMB: "128", + awsRequestId: "preview", + logGroupName: "", + logStreamName: "", + getRemainingTimeInMillis: () => 30000, + }); + + res.statusCode = result.statusCode; + for (const [key, value] of Object.entries(result.headers || {})) { + res.setHeader(key, value); + } + if (result.cookies?.length) { + res.setHeader("set-cookie", result.cookies); + } + res.end( + result.body + ? Buffer.from( + result.body, + result.isBase64Encoded ? "base64" : "utf8", + ) + : undefined, + ); + }) + .catch((error) => { + res.statusCode = 500; + res.end(String(error)); + }); + }); + + return new Promise((resolve) => { + const listener = server.listen(port, () => { + const address = listener.address() as AddressInfo; + console.log( + `Preview server started: http://localhost:${address.port}`, + ); + resolve({ + port: address.port, + close() { + listener.close(); + }, + }); + }); + }); + }, + + typeInfo(writer) { + writer( + `import type { AWSLambdaPlatformInfo } from '@marko/run-adapter-aws-lambda';`, + ); + return "AWSLambdaPlatformInfo"; + }, + }; +} + +function readBody(req: IncomingMessage): Promise { + return new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + req.on("data", (chunk) => chunks.push(chunk)); + req.on("end", () => resolve(Buffer.concat(chunks))); + req.on("error", reject); + }); +} + +function nodeRequestToEvent( + req: IncomingMessage, + body: Buffer, +): APIGatewayProxyEventV2 { + const url = new URL(req.url || "/", "http://localhost"); + const headers: Record = {}; + for (const [key, value] of Object.entries(req.headers)) { + if (value !== undefined) { + headers[key] = Array.isArray(value) ? value.join(", ") : value; + } + } + const cookies = req.headers.cookie + ? req.headers.cookie.split("; ") + : undefined; + + return { + version: "2.0", + routeKey: "$default", + rawPath: url.pathname, + rawQueryString: url.search.slice(1), + cookies, + headers, + requestContext: { + accountId: "preview", + apiId: "preview", + domainName: headers.host || "localhost", + domainPrefix: "preview", + http: { + method: req.method || "GET", + path: url.pathname, + protocol: "HTTP/1.1", + sourceIp: req.socket.remoteAddress || "127.0.0.1", + userAgent: headers["user-agent"] || "", + }, + requestId: "preview", + stage: "$default", + time: "", + timeEpoch: 0, + }, + body: body.length ? body.toString("base64") : undefined, + isBase64Encoded: true, + }; +} diff --git a/packages/adapters/aws-lambda/src/types.ts b/packages/adapters/aws-lambda/src/types.ts new file mode 100644 index 00000000..b3237624 --- /dev/null +++ b/packages/adapters/aws-lambda/src/types.ts @@ -0,0 +1,67 @@ +/** + * A subset of the AWS API Gateway HTTP API / Lambda Function URL request event + * (payload format version 2.0). + */ +export interface APIGatewayProxyEventV2 { + version: string; + routeKey: string; + rawPath: string; + rawQueryString: string; + cookies?: string[]; + headers: Record; + queryStringParameters?: Record; + requestContext: { + accountId: string; + apiId: string; + domainName: string; + domainPrefix: string; + http: { + method: string; + path: string; + protocol: string; + sourceIp: string; + userAgent: string; + }; + requestId: string; + stage: string; + time: string; + timeEpoch: number; + }; + body?: string; + pathParameters?: Record; + isBase64Encoded: boolean; + stageVariables?: Record; +} + +/** + * A structured Lambda response for the API Gateway HTTP API / Function URL + * payload format version 2.0. + */ +export interface APIGatewayProxyResultV2 { + statusCode: number; + headers?: Record; + cookies?: string[]; + body?: string; + isBase64Encoded?: boolean; +} + +/** The AWS Lambda invocation context. */ +export interface LambdaContext { + functionName: string; + functionVersion: string; + invokedFunctionArn: string; + memoryLimitInMB: string; + awsRequestId: string; + logGroupName: string; + logStreamName: string; + getRemainingTimeInMillis(): number; +} + +/** + * The platform object passed to Marko Run route handlers when deployed to AWS + * Lambda. Exposes the raw Lambda event and invocation context. + */ +export interface AWSLambdaPlatformInfo { + event: APIGatewayProxyEventV2; + context: LambdaContext; +} diff --git a/packages/adapters/aws-lambda/tsconfig.json b/packages/adapters/aws-lambda/tsconfig.json new file mode 100644 index 00000000..cd4f0966 --- /dev/null +++ b/packages/adapters/aws-lambda/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/aws-lambda-adapter/.gitignore b/packages/run/src/__tests__/fixtures/aws-lambda-adapter/.gitignore new file mode 100644 index 00000000..1521c8b7 --- /dev/null +++ b/packages/run/src/__tests__/fixtures/aws-lambda-adapter/.gitignore @@ -0,0 +1 @@ +dist diff --git a/packages/run/src/__tests__/fixtures/aws-lambda-adapter/.marko-run/routes.d.ts b/packages/run/src/__tests__/fixtures/aws-lambda-adapter/.marko-run/routes.d.ts new file mode 100644 index 00000000..ace8419e --- /dev/null +++ b/packages/run/src/__tests__/fixtures/aws-lambda-adapter/.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 { AWSLambdaPlatformInfo } from '@marko/run-adapter-aws-lambda'; + +declare module "@marko/run" { + interface Platform extends AWSLambdaPlatformInfo {} + + 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/aws-lambda-adapter/__snapshots__/dev.expected.md b/packages/run/src/__tests__/fixtures/aws-lambda-adapter/__snapshots__/dev.expected.md new file mode 100644 index 00000000..1ec2ae91 --- /dev/null +++ b/packages/run/src/__tests__/fixtures/aws-lambda-adapter/__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/aws-lambda-adapter/__snapshots__/preview.expected.md b/packages/run/src/__tests__/fixtures/aws-lambda-adapter/__snapshots__/preview.expected.md new file mode 100644 index 00000000..1ec2ae91 --- /dev/null +++ b/packages/run/src/__tests__/fixtures/aws-lambda-adapter/__snapshots__/preview.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/aws-lambda-adapter/package.json b/packages/run/src/__tests__/fixtures/aws-lambda-adapter/package.json new file mode 100644 index 00000000..428caf23 --- /dev/null +++ b/packages/run/src/__tests__/fixtures/aws-lambda-adapter/package.json @@ -0,0 +1,5 @@ +{ + "name": "aws-lambda-adapter", + "version": "1.0.0", + "scripts": {} +} diff --git a/packages/run/src/__tests__/fixtures/aws-lambda-adapter/src/components/counter.marko b/packages/run/src/__tests__/fixtures/aws-lambda-adapter/src/components/counter.marko new file mode 100644 index 00000000..436a9eb5 --- /dev/null +++ b/packages/run/src/__tests__/fixtures/aws-lambda-adapter/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/aws-lambda-adapter/src/routes/+page.marko b/packages/run/src/__tests__/fixtures/aws-lambda-adapter/src/routes/+page.marko new file mode 100644 index 00000000..5dd8370a --- /dev/null +++ b/packages/run/src/__tests__/fixtures/aws-lambda-adapter/src/routes/+page.marko @@ -0,0 +1,12 @@ + + + + + @marko/run Test Fixture + + + + + + + diff --git a/packages/run/src/__tests__/fixtures/aws-lambda-adapter/test.config.ts b/packages/run/src/__tests__/fixtures/aws-lambda-adapter/test.config.ts new file mode 100644 index 00000000..2b2c7170 --- /dev/null +++ b/packages/run/src/__tests__/fixtures/aws-lambda-adapter/test.config.ts @@ -0,0 +1,11 @@ +import { Step, StepContext } from "../../main.test"; + +export const steps: Step[] = [ + (ctx) => click(ctx), + (ctx) => click(ctx), + (ctx) => click(ctx), +]; + +async function click({ page }: StepContext) { + await page.click("button"); +} diff --git a/packages/run/src/__tests__/fixtures/aws-lambda-adapter/tsconfig.json b/packages/run/src/__tests__/fixtures/aws-lambda-adapter/tsconfig.json new file mode 100644 index 00000000..03ff88a5 --- /dev/null +++ b/packages/run/src/__tests__/fixtures/aws-lambda-adapter/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/aws-lambda-adapter/vite.config.ts b/packages/run/src/__tests__/fixtures/aws-lambda-adapter/vite.config.ts new file mode 100644 index 00000000..2fb0822c --- /dev/null +++ b/packages/run/src/__tests__/fixtures/aws-lambda-adapter/vite.config.ts @@ -0,0 +1,10 @@ +import marko from "@marko/run/vite"; +import adapter from "@marko/run-adapter-aws-lambda"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [marko({ adapter: adapter() })], + server: { + hmr: false, + }, +}); From 92d4d2e3383f64311b4a4615c8b5b2ee61f4fa8e Mon Sep 17 00:00:00 2001 From: Ryan Turnquist Date: Wed, 1 Jul 2026 04:55:46 +0000 Subject: [PATCH 2/3] fix: address review feedback for the AWS Lambda 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 - Guard decodeURIComponent so a malformed path returns 400 instead of failing the invocation - Force http scheme for preview events (the preview server is plain HTTP) so scheme-sensitive logic and absolute redirects are correct - Parse the preview Cookie header tolerant of `;` without a trailing space --- packages/adapters/aws-lambda/README.md | 5 +++-- packages/adapters/aws-lambda/package.toggle.json | 15 +++++++++++++++ .../adapters/aws-lambda/scripts/importMetaURL.js | 2 +- packages/adapters/aws-lambda/src/default-entry.ts | 8 +++++++- packages/adapters/aws-lambda/src/index.ts | 4 +++- 5 files changed, 29 insertions(+), 5 deletions(-) create mode 100644 packages/adapters/aws-lambda/package.toggle.json diff --git a/packages/adapters/aws-lambda/README.md b/packages/adapters/aws-lambda/README.md index 93996393..078189d7 100644 --- a/packages/adapters/aws-lambda/README.md +++ b/packages/adapters/aws-lambda/README.md @@ -59,8 +59,9 @@ The raw Lambda event and invocation context are available on the `platform` argu ```ts import type { AWSLambdaPlatformInfo } from "@marko/run-adapter-aws-lambda"; -export const GET = (context, { platform }) => { - const { event, context: lambdaContext } = platform as AWSLambdaPlatformInfo; +export const GET = (context) => { + const { event, context: lambdaContext } = + context.platform as AWSLambdaPlatformInfo; return new Response(`Request id: ${lambdaContext.awsRequestId}`); }; ``` diff --git a/packages/adapters/aws-lambda/package.toggle.json b/packages/adapters/aws-lambda/package.toggle.json new file mode 100644 index 00000000..a2f5767a --- /dev/null +++ b/packages/adapters/aws-lambda/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/aws-lambda/scripts/importMetaURL.js b/packages/adapters/aws-lambda/scripts/importMetaURL.js index c993f7f5..b1277af3 100644 --- a/packages/adapters/aws-lambda/scripts/importMetaURL.js +++ b/packages/adapters/aws-lambda/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/aws-lambda/src/default-entry.ts b/packages/adapters/aws-lambda/src/default-entry.ts index 32c4ab70..68d18611 100644 --- a/packages/adapters/aws-lambda/src/default-entry.ts +++ b/packages/adapters/aws-lambda/src/default-entry.ts @@ -141,7 +141,13 @@ export async function handler( } const { pathname } = new URL(request.url); - const staticResult = await serveStatic(decodeURIComponent(pathname)); + let decodedPathname: string; + try { + decodedPathname = decodeURIComponent(pathname); + } catch { + return { statusCode: 400, body: "" }; + } + const staticResult = await serveStatic(decodedPathname); if (staticResult) { return staticResult; } diff --git a/packages/adapters/aws-lambda/src/index.ts b/packages/adapters/aws-lambda/src/index.ts index cff970d2..6d0944db 100644 --- a/packages/adapters/aws-lambda/src/index.ts +++ b/packages/adapters/aws-lambda/src/index.ts @@ -152,8 +152,10 @@ function nodeRequestToEvent( headers[key] = Array.isArray(value) ? value.join(", ") : value; } } + // The preview server is plain HTTP; make sure the built request uses it too. + headers["x-forwarded-proto"] = "http"; const cookies = req.headers.cookie - ? req.headers.cookie.split("; ") + ? req.headers.cookie.split(/;\s*/).filter(Boolean) : undefined; return { From ac80bd85efd4765a66ee920e299f8ede8a30e652 Mon Sep 17 00:00:00 2001 From: Ryan Turnquist Date: Wed, 1 Jul 2026 19:18:53 +0000 Subject: [PATCH 3/3] docs: note adapter auto-discovery and reorder Deploying (AWS Lambda) Clarify that Marko Run automatically uses an installed adapter with no Vite config, and place the build + package + `aws lambda` deploy steps in a Deploying section right after installation. --- packages/adapters/aws-lambda/README.md | 28 ++++++-------------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/packages/adapters/aws-lambda/README.md b/packages/adapters/aws-lambda/README.md index 078189d7..c2a516d1 100644 --- a/packages/adapters/aws-lambda/README.md +++ b/packages/adapters/aws-lambda/README.md @@ -18,40 +18,24 @@ Preview and deploy [@marko/run](../../run/README.md) apps to AWS Lambda npm install @marko/run-adapter-aws-lambda ``` -## 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. -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 awsLambdaAdapter from "@marko/run-adapter-aws-lambda"; - -export default defineConfig({ - plugins: [ - marko({ - adapter: awsLambdaAdapter(), - }), - ], -}); -``` - -Running `marko-run build` produces a self-contained Lambda handler at `dist/index.mjs` (exported as `handler`) plus the static assets in `dist/public`. The handler targets the [API Gateway HTTP API / Lambda Function URL payload format version 2.0](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html), and serves the bundled static assets when no route matches. - -`marko-run preview` runs the handler behind a local HTTP server so you can test it without deploying. +`marko-run build` produces a self-contained Lambda handler at `dist/index.mjs` (exported as `handler`) plus the static assets in `dist/public`. The handler targets the [API Gateway HTTP API / Lambda Function URL payload format version 2.0](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html), and serves the bundled static assets when no route matches. ## Deploying -Package the `dist` directory and deploy it as a Lambda function, using `index.handler` as the handler and a Node.js runtime (eg. `nodejs20.x`). Wire it up to a [Function URL](https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html) or an API Gateway HTTP API. For example, with the AWS CLI: +Package the `dist` directory and deploy it as a Lambda function, using `index.handler` as the handler and a Node.js runtime (eg. `nodejs20.x`). Wire it up to a [Function URL](https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html) or an API Gateway HTTP API. For an existing function, with the [AWS CLI](https://docs.aws.amazon.com/cli/): ```sh -marko-run build +npm run build ( cd dist && zip -r ../function.zip . ) aws lambda update-function-code --function-name my-app --zip-file fileb://function.zip ``` Any IaC tool works too (AWS SAM, CDK, Serverless Framework, Terraform) — point the function's handler at `index.handler` and its code at the `dist` directory. +`marko-run preview` runs the handler behind a local HTTP server so you can test it without deploying. + ## Platform info The raw Lambda event and invocation context are available on the `platform` argument of your route handlers: