From f8c6e3a10428c16f4d474921e9b3d8875ab5f61c Mon Sep 17 00:00:00 2001 From: Selwyn Date: Fri, 23 Jan 2026 16:04:31 +0100 Subject: [PATCH 1/3] Add Scaleway serverless function preset --- docs/2.deploy/20.providers/scaleway.md | 7 +++++ src/presets/_all.gen.ts | 2 ++ src/presets/_types.gen.ts | 4 +-- src/presets/scaleway/preset.ts | 18 +++++++++++ .../scaleway/runtime/scaleway-serverless.ts | 31 +++++++++++++++++++ test/presets/scaleway.test.ts | 27 ++++++++++++++++ 6 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 docs/2.deploy/20.providers/scaleway.md create mode 100644 src/presets/scaleway/preset.ts create mode 100644 src/presets/scaleway/runtime/scaleway-serverless.ts create mode 100644 test/presets/scaleway.test.ts diff --git a/docs/2.deploy/20.providers/scaleway.md b/docs/2.deploy/20.providers/scaleway.md new file mode 100644 index 0000000000..18d1a6c8bf --- /dev/null +++ b/docs/2.deploy/20.providers/scaleway.md @@ -0,0 +1,7 @@ +# Scaleway + +> Deploy Nitro apps to Scaleway. + +**Preset:** `scaleway-serverless` + +:read-more{title="Scaleway Serverless Functions" to="https://www.scaleway.com/en/docs/serverless-functions/"} diff --git a/src/presets/_all.gen.ts b/src/presets/_all.gen.ts index 0aabd48fde..cfd26efbb9 100644 --- a/src/presets/_all.gen.ts +++ b/src/presets/_all.gen.ts @@ -21,6 +21,7 @@ import _netlify from "./netlify/preset.ts"; import _node from "./node/preset.ts"; import _platformSh from "./platform.sh/preset.ts"; import _renderCom from "./render.com/preset.ts"; +import _scaleway from "./scaleway/preset.ts"; import _standard from "./standard/preset.ts"; import _stormkit from "./stormkit/preset.ts"; import _vercel from "./vercel/preset.ts"; @@ -50,6 +51,7 @@ export default [ ..._node, ..._platformSh, ..._renderCom, + ..._scaleway, ..._standard, ..._stormkit, ..._vercel, diff --git a/src/presets/_types.gen.ts b/src/presets/_types.gen.ts index 974925a21a..93beb8818d 100644 --- a/src/presets/_types.gen.ts +++ b/src/presets/_types.gen.ts @@ -20,6 +20,6 @@ export interface PresetOptions { export const presetsWithConfig = ["awsAmplify","awsLambda","azure","cloudflare","firebase","netlify","vercel"] as const; -export type PresetName = "alwaysdata" | "aws-amplify" | "aws-lambda" | "azure-swa" | "base-worker" | "bun" | "cleavr" | "cloudflare-dev" | "cloudflare-durable" | "cloudflare-module" | "cloudflare-pages" | "cloudflare-pages-static" | "deno" | "deno-deploy" | "deno-server" | "digital-ocean" | "firebase-app-hosting" | "flight-control" | "genezio" | "github-pages" | "gitlab-pages" | "heroku" | "iis-handler" | "iis-node" | "koyeb" | "netlify" | "netlify-edge" | "netlify-static" | "nitro-dev" | "nitro-prerender" | "node" | "node-cluster" | "node-middleware" | "node-server" | "platform-sh" | "render-com" | "standard" | "static" | "stormkit" | "vercel" | "vercel-static" | "winterjs" | "zeabur" | "zeabur-static" | "zerops" | "zerops-static"; +export type PresetName = "alwaysdata" | "aws-amplify" | "aws-lambda" | "azure-swa" | "base-worker" | "bun" | "cleavr" | "cloudflare-dev" | "cloudflare-durable" | "cloudflare-module" | "cloudflare-pages" | "cloudflare-pages-static" | "deno" | "deno-deploy" | "deno-server" | "digital-ocean" | "firebase-app-hosting" | "flight-control" | "genezio" | "github-pages" | "gitlab-pages" | "heroku" | "iis-handler" | "iis-node" | "koyeb" | "netlify" | "netlify-edge" | "netlify-static" | "nitro-dev" | "nitro-prerender" | "node" | "node-cluster" | "node-middleware" | "node-server" | "platform-sh" | "render-com" | "scaleway-serverless" | "standard" | "static" | "stormkit" | "vercel" | "vercel-static" | "winterjs" | "zeabur" | "zeabur-static" | "zerops" | "zerops-static"; -export type PresetNameInput = "alwaysdata" | "aws-amplify" | "awsAmplify" | "aws_amplify" | "aws-lambda" | "awsLambda" | "aws_lambda" | "azure-swa" | "azureSwa" | "azure_swa" | "base-worker" | "baseWorker" | "base_worker" | "bun" | "cleavr" | "cloudflare-dev" | "cloudflareDev" | "cloudflare_dev" | "cloudflare-durable" | "cloudflareDurable" | "cloudflare_durable" | "cloudflare-module" | "cloudflareModule" | "cloudflare_module" | "cloudflare-pages" | "cloudflarePages" | "cloudflare_pages" | "cloudflare-pages-static" | "cloudflarePagesStatic" | "cloudflare_pages_static" | "deno" | "deno-deploy" | "denoDeploy" | "deno_deploy" | "deno-server" | "denoServer" | "deno_server" | "digital-ocean" | "digitalOcean" | "digital_ocean" | "firebase-app-hosting" | "firebaseAppHosting" | "firebase_app_hosting" | "flight-control" | "flightControl" | "flight_control" | "genezio" | "github-pages" | "githubPages" | "github_pages" | "gitlab-pages" | "gitlabPages" | "gitlab_pages" | "heroku" | "iis-handler" | "iisHandler" | "iis_handler" | "iis-node" | "iisNode" | "iis_node" | "koyeb" | "netlify" | "netlify-edge" | "netlifyEdge" | "netlify_edge" | "netlify-static" | "netlifyStatic" | "netlify_static" | "nitro-dev" | "nitroDev" | "nitro_dev" | "nitro-prerender" | "nitroPrerender" | "nitro_prerender" | "node" | "node-cluster" | "nodeCluster" | "node_cluster" | "node-middleware" | "nodeMiddleware" | "node_middleware" | "node-server" | "nodeServer" | "node_server" | "platform-sh" | "platformSh" | "platform_sh" | "render-com" | "renderCom" | "render_com" | "standard" | "static" | "stormkit" | "vercel" | "vercel-static" | "vercelStatic" | "vercel_static" | "winterjs" | "zeabur" | "zeabur-static" | "zeaburStatic" | "zeabur_static" | "zerops" | "zerops-static" | "zeropsStatic" | "zerops_static" | (string & {}); +export type PresetNameInput = "alwaysdata" | "aws-amplify" | "awsAmplify" | "aws_amplify" | "aws-lambda" | "awsLambda" | "aws_lambda" | "azure-swa" | "azureSwa" | "azure_swa" | "base-worker" | "baseWorker" | "base_worker" | "bun" | "cleavr" | "cloudflare-dev" | "cloudflareDev" | "cloudflare_dev" | "cloudflare-durable" | "cloudflareDurable" | "cloudflare_durable" | "cloudflare-module" | "cloudflareModule" | "cloudflare_module" | "cloudflare-pages" | "cloudflarePages" | "cloudflare_pages" | "cloudflare-pages-static" | "cloudflarePagesStatic" | "cloudflare_pages_static" | "deno" | "deno-deploy" | "denoDeploy" | "deno_deploy" | "deno-server" | "denoServer" | "deno_server" | "digital-ocean" | "digitalOcean" | "digital_ocean" | "firebase-app-hosting" | "firebaseAppHosting" | "firebase_app_hosting" | "flight-control" | "flightControl" | "flight_control" | "genezio" | "github-pages" | "githubPages" | "github_pages" | "gitlab-pages" | "gitlabPages" | "gitlab_pages" | "heroku" | "iis-handler" | "iisHandler" | "iis_handler" | "iis-node" | "iisNode" | "iis_node" | "koyeb" | "netlify" | "netlify-edge" | "netlifyEdge" | "netlify_edge" | "netlify-static" | "netlifyStatic" | "netlify_static" | "nitro-dev" | "nitroDev" | "nitro_dev" | "nitro-prerender" | "nitroPrerender" | "nitro_prerender" | "node" | "node-cluster" | "nodeCluster" | "node_cluster" | "node-middleware" | "nodeMiddleware" | "node_middleware" | "node-server" | "nodeServer" | "node_server" | "platform-sh" | "platformSh" | "platform_sh" | "render-com" | "renderCom" | "render_com" | "scaleway-serverless" | "scalewayServerless" | "scaleway_serverless" | "standard" | "static" | "stormkit" | "vercel" | "vercel-static" | "vercelStatic" | "vercel_static" | "winterjs" | "zeabur" | "zeabur-static" | "zeaburStatic" | "zeabur_static" | "zerops" | "zerops-static" | "zeropsStatic" | "zerops_static" | (string & {}); diff --git a/src/presets/scaleway/preset.ts b/src/presets/scaleway/preset.ts new file mode 100644 index 0000000000..5adb709177 --- /dev/null +++ b/src/presets/scaleway/preset.ts @@ -0,0 +1,18 @@ +import { defineNitroPreset } from "../_utils/preset.ts"; + +const scalewayServerless = defineNitroPreset( + { + entry: "./scaleway/runtime/scaleway-serverless", + rollupConfig: { + output: { + entryFileNames: "index.mjs", + format: "esm", + }, + }, + }, + { + name: "scaleway-serverless" as const, + } +); + +export default [scalewayServerless] as const; diff --git a/src/presets/scaleway/runtime/scaleway-serverless.ts b/src/presets/scaleway/runtime/scaleway-serverless.ts new file mode 100644 index 0000000000..0a97c624e0 --- /dev/null +++ b/src/presets/scaleway/runtime/scaleway-serverless.ts @@ -0,0 +1,31 @@ +import "#nitro/virtual/polyfills"; +import { useNitroApp } from "nitro/app"; +import { joinURL, withQuery } from "ufo"; +import type { serveHandler } from "@scaleway/serverless-functions"; + +const nitroApp = useNitroApp(); + +type Event = Parameters[0]>[0]; +type Context = Parameters[0]>[1]; + +export async function handler(event: Event, context: Context) { + const headers = Object.fromEntries( + Object.entries(event.headers!).map(([key, value]) => [key, String(value)]) + ); + + const url = withQuery( + joinURL( + headers?.["X-Forwarded-Proto"] === "http" ? "http://" : "https://", + headers.host, + event.path + ), + event.queryStringParameters! + ); + + const request = new Request(url, { + method: event.httpMethod, + headers, + body: event.httpMethod === "GET" ? undefined : event.body, + }); + return nitroApp.fetch(request); +} diff --git a/test/presets/scaleway.test.ts b/test/presets/scaleway.test.ts new file mode 100644 index 0000000000..f71ec0d9a1 --- /dev/null +++ b/test/presets/scaleway.test.ts @@ -0,0 +1,27 @@ +import { describe } from "vitest"; +import { resolve } from "pathe"; +import { serveHandler } from "@scaleway/serverless-functions"; +import { getRandomPort, waitForPort } from "get-port-please"; +import { setupTest, testNitro } from "../tests.ts"; + +describe("nitro:preset:scaleway", async () => { + const ctx = await setupTest("scaleway-serverless"); + + testNitro(ctx, async () => { + const { handler } = await import(resolve(ctx.outDir, "server/index.mjs")); + const port = await getRandomPort(); + const server = serveHandler(handler, port); + + ctx.server = { + url: `http://127.0.0.1:${port}`, + close: () => server.close(), + }; + + await waitForPort(port, { host: "127.0.0.1" }); + + return async ({ url, ...options }) => { + const response = await ctx.fetch(url, options); + return response; + }; + }); +}); From 9b1dc03a60bf27be9b70f2c08b198cb2f8f2ac6d Mon Sep 17 00:00:00 2001 From: Selwyn Date: Fri, 23 Jan 2026 16:51:50 +0100 Subject: [PATCH 2/3] Handle undefined query string parameters --- src/presets/scaleway/runtime/scaleway-serverless.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/presets/scaleway/runtime/scaleway-serverless.ts b/src/presets/scaleway/runtime/scaleway-serverless.ts index 0a97c624e0..57d7871097 100644 --- a/src/presets/scaleway/runtime/scaleway-serverless.ts +++ b/src/presets/scaleway/runtime/scaleway-serverless.ts @@ -19,7 +19,7 @@ export async function handler(event: Event, context: Context) { headers.host, event.path ), - event.queryStringParameters! + event.queryStringParameters ?? {} ); const request = new Request(url, { From ab873d4f75ba2db3db031625a68390817b1706f6 Mon Sep 17 00:00:00 2001 From: Selwyn Date: Fri, 23 Jan 2026 16:54:48 +0100 Subject: [PATCH 3/3] Handle header casing sensitivity --- src/presets/scaleway/runtime/scaleway-serverless.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/presets/scaleway/runtime/scaleway-serverless.ts b/src/presets/scaleway/runtime/scaleway-serverless.ts index 57d7871097..e89a16ea7e 100644 --- a/src/presets/scaleway/runtime/scaleway-serverless.ts +++ b/src/presets/scaleway/runtime/scaleway-serverless.ts @@ -9,14 +9,16 @@ type Event = Parameters[0]>[0]; type Context = Parameters[0]>[1]; export async function handler(event: Event, context: Context) { - const headers = Object.fromEntries( - Object.entries(event.headers!).map(([key, value]) => [key, String(value)]) + const headers = new Headers( + Object.fromEntries( + Object.entries(event.headers!).map(([key, value]) => [key, String(value)]) + ) ); const url = withQuery( joinURL( - headers?.["X-Forwarded-Proto"] === "http" ? "http://" : "https://", - headers.host, + headers.get("X-Forwarded-Proto") === "http" ? "http://" : "https://", + headers.get("Host")!, event.path ), event.queryStringParameters ?? {}