diff --git a/src/presets/_all.gen.ts b/src/presets/_all.gen.ts index 0aabd48fde..5d6f7a8aca 100644 --- a/src/presets/_all.gen.ts +++ b/src/presets/_all.gen.ts @@ -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"; @@ -36,6 +37,7 @@ export default [ ..._awsLambda, ..._azure, ..._bun, + ..._bunny, ..._cleavr, ..._cloudflare, ..._deno, diff --git a/src/presets/_types.gen.ts b/src/presets/_types.gen.ts index 974925a21a..a52a7c9dde 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" | "bunny" | "bunny-edge-scripting" | "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 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" | "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" | "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 & {}); diff --git a/src/presets/bunny/preset.ts b/src/presets/bunny/preset.ts new file mode 100644 index 0000000000..b822d9f143 --- /dev/null +++ b/src/presets/bunny/preset.ts @@ -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; diff --git a/src/presets/bunny/runtime/edge-scripting.ts b/src/presets/bunny/runtime/edge-scripting.ts new file mode 100644 index 0000000000..993da94be1 --- /dev/null +++ b/src/presets/bunny/runtime/edge-scripting.ts @@ -0,0 +1,20 @@ +import "#nitro/virtual/polyfills"; +import { useNitroApp } from "nitro/app"; + +const nitroApp = useNitroApp(); + +// @ts-expect-error +if (typeof Bunny !== "undefined") { + // @ts-expect-error + Bunny.v1.serve(nitroApp.fetch); +} else { + const _parsedPort = Number.parseInt(process.env.NITRO_PORT ?? process.env.PORT ?? ""); + + Deno.serve( + { + port: Number.isNaN(_parsedPort) ? 3000 : _parsedPort, + hostname: process.env.NITRO_HOST || process.env.HOST, + }, + nitroApp.fetch + ); +} diff --git a/test/presets/bunny.test.ts b/test/presets/bunny.test.ts new file mode 100644 index 0000000000..b7feec494c --- /dev/null +++ b/test/presets/bunny.test.ts @@ -0,0 +1,67 @@ +import { promises as fsp } from "node:fs"; +import { execa, execaCommandSync } from "execa"; +import { getRandomPort, waitForPort } from "get-port-please"; +import { resolve } from "pathe"; +import { describe, expect, it } from "vitest"; +import { setupTest, testNitro } from "../tests.ts"; + +const hasDeno = + execaCommandSync("deno --version", { stdio: "ignore", reject: false }).exitCode === 0; + +describe.runIf(hasDeno)("nitro:preset:bunny", async () => { + const ctx = await setupTest("bunny-edge-scripting"); + + testNitro(ctx, async () => { + const port = await getRandomPort(); + const p = execa("deno", ["run", "-A", "./bunny-edge-scripting.mjs"], { + cwd: ctx.outDir, + stdio: "ignore", + env: { + NITRO_PORT: String(port), + NITRO_HOST: "127.0.0.1", + }, + }); + ctx.server = { + url: `http://127.0.0.1:${port}`, + close: () => p.kill(), + } as any; + await waitForPort(port, { delay: 1000, retries: 20, host: "127.0.0.1" }); + return async ({ url, ...opts }) => { + const res = await ctx.fetch(url, opts); + return res; + }; + }); + + 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"); + // Check file is actually minified - should have very few newlines relative to size + const newlineCount = (entry.match(/\n/g) || []).length; + const ratio = entry.length / Math.max(1, newlineCount); + // Fixture is small, so we expect a high ratio of characters to newlines in minified code + 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"); + }); +}); diff --git a/test/tests.ts b/test/tests.ts index 684406edc3..aaa3a30aa9 100644 --- a/test/tests.ts +++ b/test/tests.ts @@ -77,6 +77,7 @@ export async function setupTest( preset, isDev: preset === "nitro-dev", isWorker: [ + "bunny-edge-scripting", "cloudflare-worker", "cloudflare-module", "cloudflare-module-legacy", @@ -641,6 +642,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: "/error-stack" });