Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions docs/2.deploy/20.providers/bunny.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Bunny

> Deploy Nitro apps to Bunny Edge Scripting.

**Preset:** `bunny`

:read-more{title="Bunny Edge Scripting" to="https://bunny.net/edge-scripting"}

## Deploy

Build your app with the `bunny` preset:

```bash
NITRO_PRESET=bunny npm run build
```

The build output is a single file at `.output/bunny-edge-scripting.mjs`. You can upload it to Bunny Edge Scripting either manually from the Bunny dashboard or via CI using Bunny's API.

:read-more{title="Bunny Scripting Docs" to="https://docs.bunny.net/scripting"}
2 changes: 2 additions & 0 deletions src/presets/_all.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import _awsAmplify from "./aws-amplify/preset.ts";
import _awsLambda from "./aws-lambda/preset.ts";
import _azure from "./azure/preset.ts";
import _bun from "./bun/preset.ts";
import _bunny from "./bunny/preset.ts";
import _cleavr from "./cleavr/preset.ts";
import _cloudflare from "./cloudflare/preset.ts";
import _deno from "./deno/preset.ts";
Expand Down Expand Up @@ -38,6 +39,7 @@ export default [
..._awsLambda,
..._azure,
..._bun,
..._bunny,
..._cleavr,
..._cloudflare,
..._deno,
Expand Down
4 changes: 2 additions & 2 deletions src/presets/_types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@ export interface PresetOptions {

export const presetsWithConfig = ["awsAmplify","awsLambda","azure","cloudflare","firebase","netlify","vercel","zephyr"] 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" | "edgeone" | "edgeone-pages" | "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-dev" | "vercel-static" | "winterjs" | "zeabur" | "zeabur-static" | "zephyr" | "zerops" | "zerops-static";
export type PresetName = "alwaysdata" | "aws-amplify" | "aws-lambda" | "azure-swa" | "base-worker" | "bun" | "bunny" | "bunny-edge-scripting" | "cleavr" | "cloudflare-dev" | "cloudflare-durable" | "cloudflare-module" | "cloudflare-pages" | "cloudflare-pages-static" | "deno" | "deno-deploy" | "deno-server" | "digital-ocean" | "edgeone" | "edgeone-pages" | "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-dev" | "vercel-static" | "winterjs" | "zeabur" | "zeabur-static" | "zephyr" | "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" | "edgeone" | "edgeone-pages" | "edgeonePages" | "edgeone_pages" | "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-dev" | "vercelDev" | "vercel_dev" | "vercel-static" | "vercelStatic" | "vercel_static" | "winterjs" | "zeabur" | "zeabur-static" | "zeaburStatic" | "zeabur_static" | "zephyr" | "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" | "bunny" | "bunny-edge-scripting" | "bunnyEdgeScripting" | "bunny_edge_scripting" | "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" | "edgeone" | "edgeone-pages" | "edgeonePages" | "edgeone_pages" | "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-dev" | "vercelDev" | "vercel_dev" | "vercel-static" | "vercelStatic" | "vercel_static" | "winterjs" | "zeabur" | "zeabur-static" | "zeaburStatic" | "zeabur_static" | "zephyr" | "zerops" | "zerops-static" | "zeropsStatic" | "zerops_static" | (string & {});
60 changes: 60 additions & 0 deletions src/presets/bunny/preset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { defineNitroPreset } from "../_utils/preset.ts";
import type { Nitro } from "nitro/types";
import { builtinModules } from "node:module";
import { rm } from "node:fs/promises";

const edgeScripting = defineNitroPreset(
{
entry: "./bunny/runtime/edge-scripting",

exportConditions: ["deno"],
commands: {
preview: "deno -A ./bunny-edge-scripting.mjs",
},

output: {
dir: "{{ rootDir }}/.output",
serverDir: "{{ output.dir }}",
publicDir: "{{ output.dir }}/public",
},

rollupConfig: {
output: {
format: "esm",
entryFileNames: "bunny-edge-scripting.mjs",
inlineDynamicImports: true,
hoistTransitiveImports: false,
},
external: (id: string) =>
id.startsWith("https://") || id.startsWith("node:") || builtinModules.includes(id),
},

serveStatic: "inline",
minify: true,

hooks: {
"build:before": (nitro: Nitro) => {
if (nitro.options.serveStatic !== "inline" && nitro.options.serveStatic !== false) {
nitro.options.serveStatic = "inline";
nitro.logger.warn(
"Bunny Edge Scripting preset requires `serveStatic` to be `inline` or `false`. Overriding to `inline`."
);
}
},
async compiled(nitro: Nitro) {
// Remove public dir when inlined, usecase is for
// managing assets directly in Bunny Storage
if (nitro.options.serveStatic === "inline") {
const publicDir = nitro.options.output.publicDir;
await rm(publicDir, { recursive: true, force: true });
}
},
},
},
{
aliases: ["bunny"],
name: "bunny-edge-scripting" as const,
}
);

export default [edgeScripting] as const;
29 changes: 29 additions & 0 deletions src/presets/bunny/runtime/edge-scripting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import "#nitro/virtual/polyfills";
import type { ServerRequest } from "srvx";
import { serve } from "srvx/bunny";
import wsAdapter from "crossws/adapters/bunny";

import { useNitroApp } from "nitro/app";
import { resolveWebsocketHooks } from "#nitro/runtime/app";
import { tracingSrvxPlugins } from "#nitro/virtual/tracing";

const nitroApp = useNitroApp();

let _fetch = nitroApp.fetch;

if (import.meta._websocket) {
const { handleUpgrade } = wsAdapter({ resolve: resolveWebsocketHooks });
_fetch = (req: ServerRequest) => {
if (req.headers.get("upgrade") === "websocket") {
return handleUpgrade(req);
}
return nitroApp.fetch(req);
};
}

serve({
fetch: _fetch,
plugins: [...tracingSrvxPlugins],
});

export default {};
79 changes: 79 additions & 0 deletions test/presets/bunny.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { promises as fsp } from "node:fs";
import { RunnerManager } from "env-runner";
import { SelfEnvRunner } from "env-runner/runners/self";
import { resolve } from "pathe";
import { describe, expect, it } from "vitest";
import { setupTest, testNitro } from "../tests.ts";

describe("nitro:preset:bunny", async () => {
const ctx = await setupTest("bunny-edge-scripting");

testNitro(ctx, async () => {
const wrapperPath = resolve(ctx.outDir, "bunny-edge-scripting.entry.mjs");
await fsp.writeFile(
wrapperPath,
[
"let _fetch;",
"globalThis.Bunny = {",
" v1: { serve: (fn) => { _fetch = fn; } },",
" unstable: { waitUntil: (p) => p },",
"};",
'await import("./bunny-edge-scripting.mjs");',
"export default { fetch: (req) => _fetch(req) };",
"",
].join("\n")
);

const runner = new SelfEnvRunner({
name: "bunny",
data: { entry: wrapperPath },
});
const manager = new RunnerManager(runner);
await runner.waitForReady(10_000);

ctx.server = {
url: "http://localhost",
close: () => manager.close(),
};

return async ({ url, headers, method, body }) => {
return await manager.fetch("http://localhost" + url, {
headers: headers || {},
method: method || "GET",
redirect: "manual",
body,
});
};
});

it("should generate the bunny-edge-scripting.mjs file", async () => {
const serverFiles = await fsp.readdir(resolve(ctx.outDir));
expect(serverFiles).toContain("bunny-edge-scripting.mjs");
});

it("should not have a separate server directory", async () => {
const serverFiles = await fsp.readdir(resolve(ctx.outDir));
expect(serverFiles).not.toContain("server");
});

it("should not have a public directory (assets should be inlined)", async () => {
const serverFiles = await fsp.readdir(resolve(ctx.outDir));
if (ctx.nitro?.options.serveStatic === "inline") {
expect(serverFiles).not.toContain("public");
} else {
expect(serverFiles).toContain("public");
}
});

it("should have minified output", async () => {
const entry = await fsp.readFile(resolve(ctx.outDir, "bunny-edge-scripting.mjs"), "utf8");
const newlineCount = (entry.match(/\n/g) || []).length;
const ratio = entry.length / Math.max(1, newlineCount);
expect(ratio).toBeGreaterThan(500);
});

it("should contain the Bunny.v1.serve call", async () => {
const entry = await fsp.readFile(resolve(ctx.outDir, "bunny-edge-scripting.mjs"), "utf8");
expect(entry).toContain("Bunny");
});
});
4 changes: 3 additions & 1 deletion test/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export async function setupTest(
preset,
isDev: preset === "nitro-dev",
isWorker: [
"bunny-edge-scripting",
"cloudflare-worker",
"cloudflare-module",
"cloudflare-module-legacy",
Expand Down Expand Up @@ -685,6 +686,7 @@ export function testNitro(
// TODO: Investigate
ctx.preset === "bun" ||
ctx.preset === "deno-server" ||
ctx.preset === "bunny-edge-scripting" ||
ctx.preset === "nitro-dev"
)("sourcemap works", async () => {
const { data } = await callHandler({ url: "/errors/stack" });
Expand All @@ -696,7 +698,7 @@ export function testNitro(
const res = await callHandler({ url: `/errors/throw?handled&action=${errorAction}` });
expect(res).toMatchObject({
status: 503,
statusText: /deno|bun/.test(ctx.preset)
statusText: /^(deno|bun)($|-)/.test(ctx.preset)
? "Service Unavailable"
: /aws/.test(ctx.preset)
? ""
Expand Down
Loading