diff --git a/.gitignore b/.gitignore index e7e6ecc..c4c04db 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,6 @@ .vercel .DS_Store bootstrap +dist node_modules project.json \ No newline at end of file diff --git a/Makefile b/Makefile deleted file mode 100644 index 20fa73f..0000000 --- a/Makefile +++ /dev/null @@ -1,10 +0,0 @@ -.PHONY: build - -build: - docker run \ - --platform linux/arm64 \ - --rm \ - -v $(CURDIR):/app \ - -w /app \ - oven/bun \ - bash -cl "bun build bootstrap.ts --compile --minify --outfile .vercel/output/functions/App.func/bootstrap" \ No newline at end of file diff --git a/README.md b/README.md index 94cf799..e757a4e 100644 --- a/README.md +++ b/README.md @@ -7,16 +7,14 @@ Run [Bun](https://bun.sh) on Vercel Serverless Functions > This is an experimental project and should not be used in production. This project is not endorsed by Vercel. ```typescript -// src/main.ts -import { type Server } from "bun" - -export default { - async fetch(request: Request, server: Server) { - return new Response("Hello from Bun on Vercel", { - status: 200, - headers: { "Content-Type": "text/plain" } - }) - } +// api/index.ts +export default function handler(req: Request) { + return new Response( + JSON.stringify({ message: `Hello from bun@${Bun.version}` }), + { + headers: { "Content-Type": "application/json" }, + }, + ) } ``` @@ -29,42 +27,22 @@ There are two ways to deploy your project to Vercel: ### GitHub Integration -> Note: An issue in Bun must be resolved before this method is fully operational - -#### 1. Update your package.json with `bun-vercel` - -```json -{ - "devDependencies": { - "bun-vercel": "^1.0.0-alpha.4", - } -} -``` - -#### 2. Update the `build` script run bun-vercel +Update your vercel.json with `bun-vercel` ```json { - "scripts": { - "build": "bun-vercel ./src/main.ts" + "$schema": "https://openapi.vercel.sh/vercel.json", + "functions": { + "api/index.ts": { + "runtime": "bun-vercel@1.0.0-alpha.6" + } } } ``` -#### 3. Add a `vercel.json` with your build command - -```json -{ - "buildCommand": "bun run build" -} -``` - ### Manually -Before starting you should follow the same setup steps as the GitHub integration, as they will be needed regardless. - -> Note: To deploy from your computer you must have Docker installed so we can build for Amazon Linux - 1. Install the Vercel CLI and run `vercel link` -2. Run `bun run build` -3. Run `vercel deploy --prebuilt --prod` +2. Run `vercel pull` +3. Run `vercel build` +4. Run `vercel deploy --prebuilt --prod` diff --git a/bootstrap.ts b/bootstrap.ts deleted file mode 100644 index e99fd63..0000000 --- a/bootstrap.ts +++ /dev/null @@ -1,362 +0,0 @@ -import type { Server } from "bun" -import main from "./example/main" - -type Lambda = { - fetch: (request: Request, server: Server) => Promise - error?: (error: unknown) => Promise -} - -let requestId: string | undefined -let traceId: string | undefined -let functionArn: string | undefined - -let logger = console.log - -function log(level: string, ...args: any[]): void { - if (!args.length) { - return - } - const message = Bun.inspect(args).replace(/\n/g, "\r") - if (requestId === undefined) { - logger(level, message) - } else { - logger(level, `RequestId: ${requestId}`, message) - } -} - -console.log = (...args: any[]) => log("INFO", ...args) -console.info = (...args: any[]) => log("INFO", ...args) -console.warn = (...args: any[]) => log("WARN", ...args) -console.error = (...args: any[]) => log("ERROR", ...args) -console.debug = (...args: any[]) => log("DEBUG", ...args) -console.trace = (...args: any[]) => log("TRACE", ...args) - -let warnings: Set | undefined - -function warnOnce(message: string, ...args: any[]): void { - if (warnings === undefined) { - warnings = new Set() - } - if (warnings.has(message)) { - return - } - warnings.add(message) - console.warn(message, ...args) -} - -function reset(): void { - requestId = undefined - traceId = undefined - warnings = undefined -} - -function exit(...cause: any[]): never { - console.error(...cause) - process.exit(1) -} - -function env(name: string, fallback?: string): string { - const value = process.env[name] ?? fallback ?? null - if (value === null) { - exit(`Runtime failed to find the '${name}' environment variable`) - } - return value -} - -const runtimeUrl = new URL( - `http://${env("AWS_LAMBDA_RUNTIME_API")}/2018-06-01/`, -) - -async function fetch(url: string, options?: RequestInit): Promise { - const { href } = new URL(url, runtimeUrl) - const response = await globalThis.fetch(href, { - ...options, - timeout: false, - }) - if (!response.ok) { - exit( - `Runtime failed to send request to Lambda [status: ${response.status}]`, - ) - } - return response -} - -type LambdaError = { - readonly errorType: string - readonly errorMessage: string - readonly stackTrace?: string[] -} - -function formatError(error: unknown): LambdaError { - if (error instanceof Error) { - return { - errorType: error.name, - errorMessage: error.message, - stackTrace: error.stack - ?.split("\n") - .filter((line) => !line.includes(" /opt/runtime.ts")), - } - } - return { - errorType: "Error", - errorMessage: Bun.inspect(error), - } -} - -async function sendError(type: string, cause: unknown): Promise { - console.error(cause) - await fetch( - requestId === undefined - ? "runtime/init/error" - : `runtime/invocation/${requestId}/error`, - { - method: "POST", - headers: { - "Content-Type": "application/vnd.aws.lambda.error+json", - "Lambda-Runtime-Function-Error-Type": `Bun.${type}`, - }, - body: JSON.stringify(formatError(cause)), - }, - ) -} - -async function throwError(type: string, cause: unknown): Promise { - await sendError(type, cause) - exit() -} - -type LambdaRequest = { - readonly requestId: string - readonly traceId: string - readonly functionArn: string - readonly deadlineMs: number | null - readonly event: E -} - -async function receiveRequest(): Promise { - const response = await fetch("runtime/invocation/next") - requestId = response.headers.get("Lambda-Runtime-Aws-Request-Id") ?? undefined - if (requestId === undefined) { - exit("Runtime received a request without a request ID") - } - traceId = response.headers.get("Lambda-Runtime-Trace-Id") ?? undefined - if (traceId === undefined) { - exit("Runtime received a request without a trace ID") - } - process.env["_X_AMZN_TRACE_ID"] = traceId - functionArn = - response.headers.get("Lambda-Runtime-Invoked-Function-Arn") ?? undefined - if (functionArn === undefined) { - exit("Runtime received a request without a function ARN") - } - const deadlineMs = - parseInt(response.headers.get("Lambda-Runtime-Deadline-Ms") ?? "0") || null - let event - try { - event = await response.json() - } catch (cause) { - exit("Runtime received a request with invalid JSON", cause) - } - return { - requestId, - traceId, - functionArn, - deadlineMs, - event, - } -} - -type LambdaResponse = { - readonly statusCode: number - readonly headers?: Record - readonly encoding?: "base64" - readonly body?: string -} - -async function formatResponse(response: Response): Promise { - const statusCode = response.status - const headers = response.headers.toJSON() - const mime = headers["content-type"] - const isBase64Encoded = - !mime || (!mime.startsWith("text/") && !mime.startsWith("application/json")) - const body = isBase64Encoded - ? Buffer.from(await response.arrayBuffer()).toString("base64") - : await response.text() - return { - statusCode, - headers, - encoding: isBase64Encoded ? "base64" : undefined, - body, - } -} - -async function sendResponse(response: unknown): Promise { - if (requestId === undefined) { - exit("Runtime attempted to send a response without a request ID") - } - await fetch(`runtime/invocation/${requestId}/response`, { - method: "POST", - body: response === null ? null : JSON.stringify(response), - }) -} - -type VercelEvent = { - readonly body: string -} - -type VercelEventParsedBody = { - readonly method: string - readonly headers: any - readonly path: string - readonly body?: string - readonly encoding?: string -} - -function formatVercelEvent(event: VercelEvent): Request { - const payload = JSON.parse(event.body) as VercelEventParsedBody - const host = payload.headers["x-forwarded-host"] - return new Request(`https://${host}${payload.path}`, { - method: payload.method, - body: payload.body, - headers: payload.headers, - }) -} - -function formatRequest(input: LambdaRequest): Request { - const { event, requestId, traceId, functionArn, deadlineMs } = input - let request = formatVercelEvent(event) - request.headers.set("x-amzn-requestid", requestId) - request.headers.set("x-amzn-trace-id", traceId) - request.headers.set("x-amzn-function-arn", functionArn) - if (deadlineMs !== null) { - request.headers.set("x-amzn-deadline-ms", `${deadlineMs}`) - } - // @ts-ignore: Attach the original event to the Request - request.aws = event - return request -} - -class LambdaServer implements Server { - #lambda: Lambda - #upgrade: Response | null - pendingRequests: number - pendingWebSockets: number - port: number - hostname: string - development: boolean - id: string - - constructor(lambda: Lambda) { - this.#lambda = lambda - this.#upgrade = null - this.pendingRequests = 0 - this.pendingWebSockets = 0 - this.port = 80 - this.hostname = "lambda" - this.development = false - this.id = "lambda" - } - - async accept(request: LambdaRequest): Promise { - const deadlineMs = - request.deadlineMs === null ? Date.now() + 60_000 : request.deadlineMs - const durationMs = Math.max(1, deadlineMs - Date.now()) - let response: unknown - try { - response = await Promise.race([ - new Promise((resolve) => setTimeout(resolve, durationMs)), - this.#acceptRequest(request), - ]) - } catch (cause) { - await sendError("RequestError", cause) - return - } - if (response === undefined) { - await sendError("TimeoutError", "Function timed out") - return - } - return response - } - - async #acceptRequest(event: LambdaRequest): Promise { - const request = formatRequest(event) - const response = await this.fetch(request) - if (response === undefined) { - return { - statusCode: 200, - } - } - if (!request?.headers.has("Host")) { - return response.text() - } - return formatResponse(response) - } - - stop(): void { - exit("Runtime exited because Server.stop() was called") - } - - reload(options: any): void { - this.#lambda = { - fetch: options.fetch ?? this.#lambda.fetch, - error: options.error ?? this.#lambda.error, - } - this.port = - typeof options.port === "number" - ? options.port - : typeof options.port === "string" - ? parseInt(options.port) - : this.port - this.hostname = options.hostname ?? this.hostname - this.development = options.development ?? this.development - } - - async fetch(request: Request): Promise { - this.pendingRequests++ - try { - let response = await this.#lambda.fetch(request, this as any) - if (response instanceof Response) { - return response - } - if (response === undefined && this.#upgrade !== null) { - return this.#upgrade - } - throw new Error("fetch() did not return a Response") - } catch (cause) { - console.error(cause) - if (this.#lambda.error !== undefined) { - try { - return await this.#lambda.error(cause) - } catch (cause) { - console.error(cause) - } - } - return new Response(null, { status: 500 }) - } finally { - this.pendingRequests-- - this.#upgrade = null - } - } - - upgrade(): boolean { - return false - } - - publish(): number { - return 0 - } -} - -const server = new LambdaServer(main) - -while (true) { - try { - const request = await receiveRequest() - const response = await server.accept(request) - if (response !== undefined) { - await sendResponse(response) - } - } finally { - reset() - } -} diff --git a/build.sh b/build.sh deleted file mode 100755 index 9a1d648..0000000 --- a/build.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -bun run node_modules/bun-vercel/build.ts $@ \ No newline at end of file diff --git a/build.ts b/build.ts deleted file mode 100644 index 68353af..0000000 --- a/build.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { exists, mkdir, unlink } from "node:fs/promises" - -// Ex. ./src/main.ts -const mainModulePath = process.argv[2] - -// Ensure file exists -if ((await exists(mainModulePath)) !== true) { - throw new Error(`module not found: ${mainModulePath}`) -} - -// Get current architecture for build -const arch = process.arch === 'arm64' ? 'arm64' : 'x86_64' - -// Bootstrap source should be in same directory as main -const bootstrapSourcePath = mainModulePath.replace( - /\.(ts|js|cjs|mjs)$/, - ".bootstrap.ts", -) - -// Read in bootstrap source -const bootstrapSource = await Bun.file("node_modules/bun-vercel/bootstrap.ts") - .text() - .catch(() => Bun.file("bootstrap.ts").text()) - -// Write boostrap source to bootstrap file -await Bun.write( - bootstrapSourcePath, - bootstrapSource.replace( - 'import main from "./example/main"', - `import main from "./${mainModulePath.split("/").pop()}"`, - ), -) - -// Create output directory -await mkdir("./.vercel/output/functions/App.func", { - recursive: true, -}) - -// Create function config file -await Bun.write( - "./.vercel/output/functions/App.func/.vc-config.json", - JSON.stringify( - { - architecture: arch, - handler: "bootstrap", - maxDuration: 10, - memory: 1024, - runtime: "provided.al2", - supportsWrapper: false, - }, - null, - 2, - ), -) - -// Create routing config file -await Bun.write( - "./.vercel/output/config.json", - JSON.stringify( - { - framework: { - version: Bun.version, - }, - overrides: {}, - routes: [ - { - headers: { - Location: "/$1", - }, - src: "^(?:/((?:[^/]+?)(?:/(?:[^/]+?))*))/$", - status: 308, - }, - { - handle: "filesystem", - }, - { - check: true, - dest: "App", - src: "^.*$", - }, - ], - version: 3, - }, - null, - 2, - ), -) - -// Compile to a single bun executable -if (await exists("/etc/system-release")) { - await Bun.spawnSync({ - cmd: [ - "bun", - "build", - bootstrapSourcePath, - "--compile", - "--minify", - "--outfile", - ".vercel/output/functions/App.func/bootstrap", - ], - stdout: "pipe", - }) -} else { - await Bun.spawnSync({ - cmd: [ - "docker", - "run", - "--platform", - `linux/${arch}`, - "--rm", - "-v", - `${process.cwd()}:/app`, - "-w", - "/app", - "oven/bun", - "bash", - "-cl", - `bun build ${bootstrapSourcePath} --compile --minify --outfile .vercel/output/functions/App.func/bootstrap`, - ], - stdout: "pipe", - }) -} - -// Cleanup bootstrap file -await unlink(bootstrapSourcePath) diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..99b079e --- /dev/null +++ b/bun.lock @@ -0,0 +1,60 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "bun-vercel", + "dependencies": { + "jszip": "^3.10.1", + }, + "devDependencies": { + "@types/bun": "latest", + "prettier": "^3.0.3", + }, + "peerDependencies": { + "@vercel/build-utils": "^10.5.1", + "typescript": "^5.2.2", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.2.14", "", { "dependencies": { "bun-types": "1.2.14" } }, "sha512-VsFZKs8oKHzI7zwvECiAJ5oSorWndIWEVhfbYqZd4HI/45kzW7PN2Rr5biAzvGvRuNmYLSANY+H59ubHq8xw7Q=="], + + "@types/node": ["@types/node@22.15.21", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ=="], + + "@vercel/build-utils": ["@vercel/build-utils@10.5.1", "", {}, "sha512-BtqwEmU1AoITpd0KxYrdQOwyKZL8RKba+bWxI8mr3gXPQZWRAE9ok1zF0AXfvMGCstYPHBPNolZGDSfWmY2jqg=="], + + "bun-types": ["bun-types@1.2.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-Kuh4Ub28ucMRWeiUUWMHsT9Wcbr4H3kLIO72RZZElSDxSu7vpetRvxIUDUaW6QtaIeixIpm7OXtNnZPf82EzwA=="], + + "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], + + "immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], + + "jszip": ["jszip@3.10.1", "", { "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", "setimmediate": "^1.0.5" } }, "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g=="], + + "lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="], + + "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], + + "prettier": ["prettier@3.5.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw=="], + + "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], + + "readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], + + "safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + + "setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="], + + "string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + + "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + } +} diff --git a/bun.lockb b/bun.lockb deleted file mode 100755 index 28bae7f..0000000 Binary files a/bun.lockb and /dev/null differ diff --git a/example/.gitignore b/example/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/example/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/example/README.md b/example/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/example/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/example/api/index.ts b/example/api/index.ts new file mode 100644 index 0000000..12806b9 --- /dev/null +++ b/example/api/index.ts @@ -0,0 +1,8 @@ +export default function handler(req: Request) { + return new Response( + JSON.stringify({ message: `Hello from bun@${Bun.version} on Vercel` }), + { + headers: { "Content-Type": "application/json" }, + }, + ) +} diff --git a/example/app/favicon.ico b/example/app/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/example/app/favicon.ico differ diff --git a/example/app/globals.css b/example/app/globals.css new file mode 100644 index 0000000..a2dc41e --- /dev/null +++ b/example/app/globals.css @@ -0,0 +1,26 @@ +@import "tailwindcss"; + +:root { + --background: #ffffff; + --foreground: #171717; +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #0a0a0a; + --foreground: #ededed; + } +} + +body { + background: var(--background); + color: var(--foreground); + font-family: Arial, Helvetica, sans-serif; +} diff --git a/example/app/layout.tsx b/example/app/layout.tsx new file mode 100644 index 0000000..f7fa87e --- /dev/null +++ b/example/app/layout.tsx @@ -0,0 +1,34 @@ +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import "./globals.css"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +export const metadata: Metadata = { + title: "Create Next App", + description: "Generated by create next app", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + ); +} diff --git a/example/app/page.tsx b/example/app/page.tsx new file mode 100644 index 0000000..88f0cc9 --- /dev/null +++ b/example/app/page.tsx @@ -0,0 +1,103 @@ +import Image from "next/image"; + +export default function Home() { + return ( +
+
+ Next.js logo +
    +
  1. + Get started by editing{" "} + + app/page.tsx + + . +
  2. +
  3. + Save and see your changes instantly. +
  4. +
+ + +
+ +
+ ); +} diff --git a/example/bun.lock b/example/bun.lock new file mode 100644 index 0000000..52c4c08 --- /dev/null +++ b/example/bun.lock @@ -0,0 +1,257 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "example", + "dependencies": { + "next": "15.3.2", + "react": "^19.0.0", + "react-dom": "^19.0.0", + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@types/bun": "latest", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "tailwindcss": "^4", + "typescript": "^5", + }, + }, + }, + "packages": { + "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], + + "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="], + + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.1.0" }, "os": "darwin", "cpu": "arm64" }, "sha512-OfXHZPppddivUJnqyKoi5YVeHRkkNE2zUFT2gbpKxp/JZCFYEYubnMg+gOp6lWfasPrTS+KPosKqdI+ELYVDtg=="], + + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.1.0" }, "os": "darwin", "cpu": "x64" }, "sha512-dYvWqmjU9VxqXmjEtjmvHnGqF8GrVjM2Epj9rJ6BUIXvk8slvNDJbhGFvIoXzkDhrJC2jUxNLz/GUjjvSzfw+g=="], + + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.1.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA=="], + + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.1.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ=="], + + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.1.0", "", { "os": "linux", "cpu": "arm" }, "sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA=="], + + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.1.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew=="], + + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.1.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ=="], + + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.1.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA=="], + + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.1.0", "", { "os": "linux", "cpu": "x64" }, "sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q=="], + + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.1.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w=="], + + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.1.0", "", { "os": "linux", "cpu": "x64" }, "sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A=="], + + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.1.0" }, "os": "linux", "cpu": "arm" }, "sha512-0DZzkvuEOqQUP9mo2kjjKNok5AmnOr1jB2XYjkaoNRwpAYMDzRmAqUIa1nRi58S2WswqSfPOWLNOr0FDT3H5RQ=="], + + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.1.0" }, "os": "linux", "cpu": "arm64" }, "sha512-D8n8wgWmPDakc83LORcfJepdOSN6MvWNzzz2ux0MnIbOqdieRZwVYY32zxVx+IFUT8er5KPcyU3XXsn+GzG/0Q=="], + + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.1.0" }, "os": "linux", "cpu": "s390x" }, "sha512-EGZ1xwhBI7dNISwxjChqBGELCWMGDvmxZXKjQRuqMrakhO8QoMgqCrdjnAqJq/CScxfRn+Bb7suXBElKQpPDiw=="], + + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.1.0" }, "os": "linux", "cpu": "x64" }, "sha512-sD7J+h5nFLMMmOXYH4DD9UtSNBD05tWSSdWAcEyzqW8Cn5UxXvsHAxmxSesYUsTOBmUnjtxghKDl15EvfqLFbQ=="], + + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.1.0" }, "os": "linux", "cpu": "arm64" }, "sha512-NEE2vQ6wcxYav1/A22OOxoSOGiKnNmDzCYFOZ949xFmrWZOVII1Bp3NqVVpvj+3UeHMFyN5eP/V5hzViQ5CZNA=="], + + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.1.0" }, "os": "linux", "cpu": "x64" }, "sha512-DOYMrDm5E6/8bm/yQLCWyuDJwUnlevR8xtF8bs+gjZ7cyUNYXiSf/E8Kp0Ss5xasIaXSHzb888V1BE4i1hFhAA=="], + + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.2", "", { "dependencies": { "@emnapi/runtime": "^1.4.3" }, "cpu": "none" }, "sha512-/VI4mdlJ9zkaq53MbIG6rZY+QRN3MLbR6usYlgITEzi4Rpx5S6LFKsycOQjkOGmqTNmkIdLjEvooFKwww6OpdQ=="], + + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-cfP/r9FdS63VA5k0xiqaNaEoGxBg9k7uE+RQGzuK9fHt7jib4zAVVseR9LsE4gJcNWgT6APKMNnCcnyOtmSEUQ=="], + + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLjGGvAbj0X/FXl8n1WbtQ6iVBpWU7JO94u/P2M4a8CFYsvQi4GW2mRy/JqkRx0qpBzaOdKJKw8uc930EX2AHw=="], + + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.2", "", { "os": "win32", "cpu": "x64" }, "sha512-aUdT6zEYtDKCaxkofmmJDJYGCf0+pJg3eU9/oBuqvEeoB9dKI6ZLc/1iLJCTuJQDO4ptntAlkUmHgGjyuobZbw=="], + + "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], + + "@next/env": ["@next/env@15.3.2", "", {}, "sha512-xURk++7P7qR9JG1jJtLzPzf0qEvqCN0A/T3DXf8IPMKo9/6FfjxtEffRJIIew/bIL4T3C2jLLqBor8B/zVlx6g=="], + + "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-2DR6kY/OGcokbnCsjHpNeQblqCZ85/1j6njYSkzRdpLn5At7OkSdmk7WyAmB9G0k25+VgqVZ/u356OSoQZ3z0g=="], + + "@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ro/fdqaZWL6k1S/5CLv1I0DaZfDVJkWNaUU3un8Lg6m0YENWlDulmIWzV96Iou2wEYyEsZq51mwV8+XQXqMp3w=="], + + "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-covwwtZYhlbRWK2HlYX9835qXum4xYZ3E2Mra1mdQ+0ICGoMiw1+nVAn4d9Bo7R3JqSmK1grMq/va+0cdh7bJA=="], + + "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-KQkMEillvlW5Qk5mtGA/3Yz0/tzpNlSw6/3/ttsV1lNtMuOHcGii3zVeXZyi4EJmmLDKYcTcByV2wVsOhDt/zg=="], + + "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uRBo6THWei0chz+Y5j37qzx+BtoDRFIkDzZjlpCItBRXyMPIg079eIkOCl3aqr2tkxL4HFyJ4GHDes7W8HuAUg=="], + + "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-+uxFlPuCNx/T9PdMClOqeE8USKzj8tVz37KflT3Kdbx/LOlZBRI2yxuIcmx1mPNK8DwSOMNCr4ureSet7eyC0w=="], + + "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.3.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-LLTKmaI5cfD8dVzh5Vt7+OMo+AIOClEdIU/TSKbXXT2iScUTSxOGoBhfuv+FU8R9MLmrkIL1e2fBMkEEjYAtPQ=="], + + "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.3.2", "", { "os": "win32", "cpu": "x64" }, "sha512-aW5B8wOPioJ4mBdMDXkt5f3j8pUr9W8AnlX0Df35uRWNT1Y6RIybxjnSUe+PhM+M1bwgyY8PHLmXZC6zT1o5tA=="], + + "@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="], + + "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], + + "@tailwindcss/node": ["@tailwindcss/node@4.1.7", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.7" } }, "sha512-9rsOpdY9idRI2NH6CL4wORFY0+Q6fnx9XP9Ju+iq/0wJwGD5IByIgFmwVbyy4ymuyprj8Qh4ErxMKTUL4uNh3g=="], + + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.7", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.4.3" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.7", "@tailwindcss/oxide-darwin-arm64": "4.1.7", "@tailwindcss/oxide-darwin-x64": "4.1.7", "@tailwindcss/oxide-freebsd-x64": "4.1.7", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.7", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.7", "@tailwindcss/oxide-linux-arm64-musl": "4.1.7", "@tailwindcss/oxide-linux-x64-gnu": "4.1.7", "@tailwindcss/oxide-linux-x64-musl": "4.1.7", "@tailwindcss/oxide-wasm32-wasi": "4.1.7", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.7", "@tailwindcss/oxide-win32-x64-msvc": "4.1.7" } }, "sha512-5SF95Ctm9DFiUyjUPnDGkoKItPX/k+xifcQhcqX5RA85m50jw1pT/KzjdvlqxRja45Y52nR4MR9fD1JYd7f8NQ=="], + + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.7", "", { "os": "android", "cpu": "arm64" }, "sha512-IWA410JZ8fF7kACus6BrUwY2Z1t1hm0+ZWNEzykKmMNM09wQooOcN/VXr0p/WJdtHZ90PvJf2AIBS/Ceqx1emg=="], + + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-81jUw9To7fimGGkuJ2W5h3/oGonTOZKZ8C2ghm/TTxbwvfSiFSDPd6/A/KE2N7Jp4mv3Ps9OFqg2fEKgZFfsvg=="], + + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-q77rWjEyGHV4PdDBtrzO0tgBBPlQWKY7wZK0cUok/HaGgbNKecegNxCGikuPJn5wFAlIywC3v+WMBt0PEBtwGw=="], + + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-RfmdbbK6G6ptgF4qqbzoxmH+PKfP4KSVs7SRlTwcbRgBwezJkAO3Qta/7gDy10Q2DcUVkKxFLXUQO6J3CRvBGw=="], + + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.7", "", { "os": "linux", "cpu": "arm" }, "sha512-OZqsGvpwOa13lVd1z6JVwQXadEobmesxQ4AxhrwRiPuE04quvZHWn/LnihMg7/XkN+dTioXp/VMu/p6A5eZP3g=="], + + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-voMvBTnJSfKecJxGkoeAyW/2XRToLZ227LxswLAwKY7YslG/Xkw9/tJNH+3IVh5bdYzYE7DfiaPbRkSHFxY1xA=="], + + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-PjGuNNmJeKHnP58M7XyjJyla8LPo+RmwHQpBI+W/OxqrwojyuCQ+GUtygu7jUqTEexejZHr/z3nBc/gTiXBj4A=="], + + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.7", "", { "os": "linux", "cpu": "x64" }, "sha512-HMs+Va+ZR3gC3mLZE00gXxtBo3JoSQxtu9lobbZd+DmfkIxR54NO7Z+UQNPsa0P/ITn1TevtFxXTpsRU7qEvWg=="], + + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.7", "", { "os": "linux", "cpu": "x64" }, "sha512-MHZ6jyNlutdHH8rd+YTdr3QbXrHXqwIhHw9e7yXEBcQdluGwhpQY2Eku8UZK6ReLaWtQ4gijIv5QoM5eE+qlsA=="], + + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.7", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@emnapi/wasi-threads": "^1.0.2", "@napi-rs/wasm-runtime": "^0.2.9", "@tybys/wasm-util": "^0.9.0", "tslib": "^2.8.0" }, "cpu": "none" }, "sha512-ANaSKt74ZRzE2TvJmUcbFQ8zS201cIPxUDm5qez5rLEwWkie2SkGtA4P+GPTj+u8N6JbPrC8MtY8RmJA35Oo+A=="], + + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-HUiSiXQ9gLJBAPCMVRk2RT1ZrBjto7WvqsPBwUrNK2BcdSxMnk19h4pjZjI7zgPhDxlAbJSumTC4ljeA9y0tEw=="], + + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.7", "", { "os": "win32", "cpu": "x64" }, "sha512-rYHGmvoHiLJ8hWucSfSOEmdCBIGZIq7SpkPRSqLsH2Ab2YUNgKeAPT1Fi2cx3+hnYOrAb0jp9cRyode3bBW4mQ=="], + + "@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.7", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.7", "@tailwindcss/oxide": "4.1.7", "postcss": "^8.4.41", "tailwindcss": "4.1.7" } }, "sha512-88g3qmNZn7jDgrrcp3ZXEQfp9CVox7xjP1HN2TFKI03CltPVd/c61ydn5qJJL8FYunn0OqBaW5HNUga0kmPVvw=="], + + "@types/bun": ["@types/bun@1.2.14", "", { "dependencies": { "bun-types": "1.2.14" } }, "sha512-VsFZKs8oKHzI7zwvECiAJ5oSorWndIWEVhfbYqZd4HI/45kzW7PN2Rr5biAzvGvRuNmYLSANY+H59ubHq8xw7Q=="], + + "@types/node": ["@types/node@20.17.50", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-Mxiq0ULv/zo1OzOhwPqOA13I81CV/W3nvd3ChtQZRT5Cwz3cr0FKo/wMSsbTqL3EXpaBAEQhva2B8ByRkOIh9A=="], + + "@types/react": ["@types/react@19.1.5", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-piErsCVVbpMMT2r7wbawdZsq4xMvIAhQuac2gedQHysu1TZYEigE6pnFfgZT+/jQnrRuF5r+SHzuehFjfRjr4g=="], + + "@types/react-dom": ["@types/react-dom@19.1.5", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-CMCjrWucUBZvohgZxkjd6S9h0nZxXjzus6yDfUb+xLxYM7VvjKNH1tQrE9GWLql1XoOP4/Ds3bwFqShHUYraGg=="], + + "bun-types": ["bun-types@1.2.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-Kuh4Ub28ucMRWeiUUWMHsT9Wcbr4H3kLIO72RZZElSDxSu7vpetRvxIUDUaW6QtaIeixIpm7OXtNnZPf82EzwA=="], + + "busboy": ["busboy@1.6.0", "", { "dependencies": { "streamsearch": "^1.1.0" } }, "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001718", "", {}, "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw=="], + + "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], + + "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], + + "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], + + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + + "detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="], + + "enhanced-resolve": ["enhanced-resolve@5.18.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], + + "jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="], + + "lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.1", "", { "os": "linux", "cpu": "arm" }, "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="], + + "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], + + "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + + "minizlib": ["minizlib@3.0.2", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA=="], + + "mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "next": ["next@15.3.2", "", { "dependencies": { "@next/env": "15.3.2", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.3.2", "@next/swc-darwin-x64": "15.3.2", "@next/swc-linux-arm64-gnu": "15.3.2", "@next/swc-linux-arm64-musl": "15.3.2", "@next/swc-linux-x64-gnu": "15.3.2", "@next/swc-linux-x64-musl": "15.3.2", "@next/swc-win32-arm64-msvc": "15.3.2", "@next/swc-win32-x64-msvc": "15.3.2", "sharp": "^0.34.1" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-CA3BatMyHkxZ48sgOCLdVHjFU36N7TF1HhqAHLFOkV6buwZnvMI84Cug8xD56B9mCuKrqXnLn94417GrZ/jjCQ=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="], + + "react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="], + + "react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="], + + "scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], + + "semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "sharp": ["sharp@0.34.2", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.4", "semver": "^7.7.2" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.2", "@img/sharp-darwin-x64": "0.34.2", "@img/sharp-libvips-darwin-arm64": "1.1.0", "@img/sharp-libvips-darwin-x64": "1.1.0", "@img/sharp-libvips-linux-arm": "1.1.0", "@img/sharp-libvips-linux-arm64": "1.1.0", "@img/sharp-libvips-linux-ppc64": "1.1.0", "@img/sharp-libvips-linux-s390x": "1.1.0", "@img/sharp-libvips-linux-x64": "1.1.0", "@img/sharp-libvips-linuxmusl-arm64": "1.1.0", "@img/sharp-libvips-linuxmusl-x64": "1.1.0", "@img/sharp-linux-arm": "0.34.2", "@img/sharp-linux-arm64": "0.34.2", "@img/sharp-linux-s390x": "0.34.2", "@img/sharp-linux-x64": "0.34.2", "@img/sharp-linuxmusl-arm64": "0.34.2", "@img/sharp-linuxmusl-x64": "0.34.2", "@img/sharp-wasm32": "0.34.2", "@img/sharp-win32-arm64": "0.34.2", "@img/sharp-win32-ia32": "0.34.2", "@img/sharp-win32-x64": "0.34.2" } }, "sha512-lszvBmB9QURERtyKT2bNmsgxXK0ShJrL/fvqlonCo7e6xBF8nT8xU6pW+PMIbLsz0RxQk3rgH9kd8UmvOzlMJg=="], + + "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "streamsearch": ["streamsearch@1.1.0", "", {}, "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="], + + "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], + + "tailwindcss": ["tailwindcss@4.1.7", "", {}, "sha512-kr1o/ErIdNhTz8uzAYL7TpaUuzKIE6QPQ4qmSdxnoX/lo+5wmUHQA6h3L5yIqEImSRnAAURDirLu/BgiXGPAhg=="], + + "tapable": ["tapable@2.2.2", "", {}, "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg=="], + + "tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + + "undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="], + + "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="], + + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.10", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.9.0" }, "bundled": true }, "sha512-bCsCyeZEwVErsGmyPNSzwfwFn4OdxBj0mmv6hOFucB/k81Ojdu68RbZdxYsRQUPc9l6SU5F/cG+bXgWs3oUgsQ=="], + + "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="], + + "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], + } +} diff --git a/example/main.ts b/example/main.ts deleted file mode 100644 index 1eed4fc..0000000 --- a/example/main.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { Server } from "bun" - -export default { - async fetch(request: Request, server: Server) { - let text = "Hello from Bun on Vercel!\n" - - text += `\nurl: ${request.url}\n` - - for (const [key, value] of request.headers.entries()) { - if (!key.startsWith("x-vercel")) continue - text += `\n${key}: ${value}` - } - - return new Response(text, { - status: 200, - headers: { - "Content-Type": "text/plain;charset=utf-8", - }, - }) - }, -} diff --git a/example/next.config.ts b/example/next.config.ts new file mode 100644 index 0000000..e9ffa30 --- /dev/null +++ b/example/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/example/package.json b/example/package.json new file mode 100644 index 0000000..328026b --- /dev/null +++ b/example/package.json @@ -0,0 +1,26 @@ +{ + "name": "example", + "version": "0.1.0", + "private": true, + "scripts": { + "dev:next": "next dev --port 3001", + "dev:server": "bun run api/index.ts", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "next": "15.3.2", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@types/bun": "latest", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "tailwindcss": "^4", + "typescript": "^5" + } +} diff --git a/example/postcss.config.mjs b/example/postcss.config.mjs new file mode 100644 index 0000000..c7bcb4b --- /dev/null +++ b/example/postcss.config.mjs @@ -0,0 +1,5 @@ +const config = { + plugins: ["@tailwindcss/postcss"], +}; + +export default config; diff --git a/example/public/file.svg b/example/public/file.svg new file mode 100644 index 0000000..004145c --- /dev/null +++ b/example/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/example/public/globe.svg b/example/public/globe.svg new file mode 100644 index 0000000..567f17b --- /dev/null +++ b/example/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/example/public/next.svg b/example/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/example/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/example/public/vercel.svg b/example/public/vercel.svg new file mode 100644 index 0000000..7705396 --- /dev/null +++ b/example/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/example/public/window.svg b/example/public/window.svg new file mode 100644 index 0000000..b2b2a44 --- /dev/null +++ b/example/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/example/tsconfig.json b/example/tsconfig.json new file mode 100644 index 0000000..d8b9323 --- /dev/null +++ b/example/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/example/vercel.json b/example/vercel.json new file mode 100644 index 0000000..821c9e8 --- /dev/null +++ b/example/vercel.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "functions": { + "api/index.ts": { + "runtime": "bun-vercel@1.0.0-alpha.6" + } + }, + "rewrites": [{ "source": "/api/(.*)", "destination": "/api/index.ts" }] +} diff --git a/package.json b/package.json index b751b04..4a79bd4 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,29 @@ { "name": "bun-vercel", - "version": "1.0.0-alpha.5", - "type": "module", - "bin": { - "bun-vercel": "./build.sh" + "description": "🐰 Bun runtime for ▲ Vercel Serverless Functions", + "version": "1.0.0-alpha.6", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "publishConfig": { + "access": "public" + }, + "scripts": { + "build": "rm -rf dist && tsc && cp -r src/bootstrap src/runtime dist", + "release": "bun run --bun build && npm publish", + "release:dry": "bun run release --dry-run" + }, + "dependencies": { + "jszip": "^3.10.1" }, "devDependencies": { - "bun-types": "^1.0.2", - "prettier": "^3.0.3", - "typescript": "^5.2.2" + "@types/bun": "latest", + "prettier": "^3.0.3" }, - "scripts": { - "dev": "bun run src/main.ts" + "peerDependencies": { + "typescript": "^5.2.2", + "@vercel/build-utils": "^10.5.1" } } diff --git a/src/build.ts b/src/build.ts new file mode 100644 index 0000000..deededf --- /dev/null +++ b/src/build.ts @@ -0,0 +1,150 @@ +import { + download, + FileFsRef, + getProvidedRuntime, + Lambda, + type BuildV3, + type Files, +} from "@vercel/build-utils" +import { mkdir, writeFile } from "fs/promises" +import JSZip from "jszip" +import { dirname, resolve } from "path" + +const buildConfig = { + defaultBunVersion: "1.2.13", + defaultArch: "x64", +} as const + +export const build: BuildV3 = async function ({ + files, + config, + entrypoint, + workPath, + repoRootPath, + meta, +}) { + // Determine architecture - Vercel's AWS Lambda runs on x64 by default + const arch = process.arch === "arm64" ? "aarch64" : buildConfig.defaultArch + + // Determine Bun version + const bunVersion = process.env.BUN_VERSION ?? buildConfig.defaultBunVersion + + // Get the Bun binary URL for the right architecture + const { href } = new URL( + `https://bun.sh/download/${bunVersion}/linux/${arch}?avx2=true`, + ) + console.log(`Downloading bun binary: ${href}`) + + // Download the Bun binary + const res = await fetch(href, { + headers: { + "User-Agent": "bun-vercel", + }, + }) + + // Notify if the Bun binary was downloaded from a different URL + if (res.url !== href) { + console.log(`Downloaded bun binary: ${res.url}`) + } + + // Check if res is OK + if (!res.ok) { + const reason = await res.text() + throw new Error(`Failed to download bun binary: ${reason}`) + } + + // Load the archive + let archive = await JSZip.loadAsync(await res.arrayBuffer()).catch((e) => { + console.log(`Failed to load bun binary: ${(e as Error).message}`) + throw e + }) + console.log(`Extracted archive: ${Object.keys(archive.files)}`) + + // Get bun from archive + const bun = archive.filter( + (_, { dir, name }) => !dir && name.endsWith("bun"), + )[0] + if (!bun) { + throw new Error("Failed to find executable in zip") + } + + // Ensure the archive isn't a directory + const cwd = bun.name.split("/")[0] + archive = cwd ? (archive.folder(cwd) ?? archive) : archive + + // Get the directory where this file is located + const currentDir = dirname(__filename) + + // Extract the binary to the workPath + const bunBinaryPath = resolve(currentDir, "bin") + await mkdir(bunBinaryPath, { recursive: true }) + + // Generate the archive and save the Bun binary + const bunExecutable = await bun.async("nodebuffer") + const bunOutputPath = resolve(bunBinaryPath, "bun") + await writeFile(bunOutputPath, bunExecutable, { mode: 0o755 }) + + // Download runtime files containing Bun modules + const runtimeFiles: Files = { + // Save bun binary + "bin/bun": new FileFsRef({ + mode: 0o755, + fsPath: bunOutputPath, + }), + + // Save bootstrap + bootstrap: new FileFsRef({ + mode: 0o755, // Make it executable + fsPath: resolve(currentDir, "bootstrap"), + }), + + // Save runtime files + "runtime/index.ts": new FileFsRef({ + mode: 0o644, + fsPath: resolve(currentDir, "runtime/index.ts"), + }), + "runtime/constants.ts": new FileFsRef({ + mode: 0o644, + fsPath: resolve(currentDir, "runtime/constants.ts"), + }), + "runtime/getHandler.ts": new FileFsRef({ + mode: 0o644, + fsPath: resolve(currentDir, "runtime/getHandler.ts"), + }), + "runtime/http.ts": new FileFsRef({ + mode: 0o644, + fsPath: resolve(currentDir, "runtime/http.ts"), + }), + "runtime/lambda.ts": new FileFsRef({ + mode: 0o644, + fsPath: resolve(currentDir, "runtime/lambda.ts"), + }), + "runtime/transforms.ts": new FileFsRef({ + mode: 0o644, + fsPath: resolve(currentDir, "runtime/transforms.ts"), + }), + "runtime/types.ts": new FileFsRef({ + mode: 0o644, + fsPath: resolve(currentDir, "runtime/types.ts"), + }), + } + + // Download the user files + const userFiles: Files = await download(files, workPath, meta) + + // Create Lambda + const lambda = new Lambda({ + files: { + ...userFiles, + ...runtimeFiles, + }, + handler: entrypoint, + runtime: await getProvidedRuntime(), + }) + console.log(`Created Lambda with bun@${bunVersion} runtime`) + + // Return the Lambda function + return { + output: lambda, + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..1979a36 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,7 @@ +// Exports should satify the Runtime interface defined in DEVELOPING_A_RUNTIME.md +export { shouldServe } from "@vercel/build-utils"; + +export { build } from "./build"; +export { prepareCache } from "./prepare-cache"; +export { startDevServer } from "./start-dev-server"; +export { version } from "./version"; diff --git a/src/prepare-cache.ts b/src/prepare-cache.ts new file mode 100644 index 0000000..d98bfe8 --- /dev/null +++ b/src/prepare-cache.ts @@ -0,0 +1,7 @@ +import { glob, type PrepareCache } from "@vercel/build-utils"; + +export const prepareCache: PrepareCache = async function ({ workPath }) { + return { + ...(await glob("node_modules/**", workPath)), + }; +}; diff --git a/src/runtime/constants.ts b/src/runtime/constants.ts new file mode 100644 index 0000000..7b27f4f --- /dev/null +++ b/src/runtime/constants.ts @@ -0,0 +1 @@ +export const RUNTIME_PATH = "2018-06-01/runtime"; diff --git a/src/runtime/getHandler.ts b/src/runtime/getHandler.ts new file mode 100644 index 0000000..6b71af7 --- /dev/null +++ b/src/runtime/getHandler.ts @@ -0,0 +1,26 @@ +import { resolve } from "path"; +import type { Handler } from "./types"; + +let handler: Handler | null = null; + +const HANDLER_PATH = resolve(process.cwd(), process.env._HANDLER ?? ""); + +export async function getHandler(): Promise { + if (handler) return handler; + + try { + const mod = await import(HANDLER_PATH); + handler = mod.default; + + if (typeof handler !== "function") { + throw new Error( + `Handler function not found in "${HANDLER_PATH}". Make sure it exports a default function.` + ); + } + + return handler; + } catch (e: any) { + console.error("getHandler Error:", e.message); + throw e; + } +} diff --git a/src/runtime/http.ts b/src/runtime/http.ts new file mode 100644 index 0000000..e8b1608 --- /dev/null +++ b/src/runtime/http.ts @@ -0,0 +1,43 @@ +import { request as httpRequest } from "node:http"; + +import { RUNTIME_PATH } from "./constants"; + +export function httpRequest2Promise( + url: string, + options: any = {}, + responseHandler: (res: any) => Promise +): Promise { + return new Promise((resolve, reject) => { + const req = httpRequest(url, options, (res) => { + responseHandler(res).then(resolve).catch(reject); + }); + + req.on("error", reject); + + if (options.body) req.write(options.body); + + req.end(); + }); +} + +export function performRequest(path: string, options: any = {}) { + return httpRequest2Promise<{ status: number; headers: any; body: string }>( + `http://${process.env.AWS_LAMBDA_RUNTIME_API}/${RUNTIME_PATH}/${path}`, + options, + async (res) => { + let body = ""; + + return new Promise((resolve) => { + res.on("data", (chunk: string) => (body += chunk)); + + res.on("end", () => { + resolve({ + status: res.statusCode || 200, + headers: res.headers, + body, + }); + }); + }); + } + ); +} diff --git a/src/runtime/index.ts b/src/runtime/index.ts new file mode 100644 index 0000000..b20716e --- /dev/null +++ b/src/runtime/index.ts @@ -0,0 +1,71 @@ +import { getHandler } from "./getHandler"; +import { invokeError, invokeResponse, nextInvocation } from "./lambda"; +import { fromVercelRequest, toVercelResponse } from "./transforms"; +import type { + Handler, + VercelRequestPayload, + VercelResponsePayload, +} from "./types"; + +async function processEvents() { + let event: any; + let awsRequestId: string; + + let handler: Handler; + + let payload: VercelRequestPayload; + let req: Request; + + let res: Response; + + let vercelRes: VercelResponsePayload; + + while (true) { + try { + // Get the next event + ({ event, awsRequestId } = await nextInvocation()); + + try { + // Get the handler + handler = await getHandler(); + + // Parse the request + payload = JSON.parse(event.body); + req = fromVercelRequest(payload); + + // Run user code and send response + res = await handler(req); + + // Parse the response + vercelRes = await toVercelResponse(res); + await invokeResponse(vercelRes, awsRequestId); + } catch (e: any) { + // Log the error + console.error("User code error:", e.message); + + // Invoke the error + await invokeError(e, awsRequestId); + } + } catch (e: any) { + // Log the error + console.error("Lambda runtime error:", e.message); + + // Wait a bit before retrying + await new Promise((resolve) => setTimeout(resolve, 100)); + } + } +} + +if (process.env._HANDLER) { + // Runtime - execute the runtime loop + processEvents().catch((e) => { + console.error("processEvents error:", e.message); + process.exit(1); + }); +} else if (process.env.ENTRYPOINT) { + // Build - import the entrypoint so that it gets cached + import(process.env.ENTRYPOINT).catch((e) => { + console.error("Failed to import entrypoint:", e.message); + process.exit(1); + }); +} diff --git a/src/runtime/lambda.ts b/src/runtime/lambda.ts new file mode 100644 index 0000000..c574f36 --- /dev/null +++ b/src/runtime/lambda.ts @@ -0,0 +1,85 @@ +import { RUNTIME_PATH } from "./constants"; +import { httpRequest2Promise, performRequest } from "./http"; +import type { VercelResponsePayload } from "./types"; + +export async function nextInvocation() { + return httpRequest2Promise<{ event: any; awsRequestId: string }>( + `http://${process.env.AWS_LAMBDA_RUNTIME_API}/${RUNTIME_PATH}/invocation/next`, + {}, + async (res) => { + // Set trace ID if available + const traceId = res.headers["lambda-runtime-trace-id"]; + if (typeof traceId === "string") { + process.env._X_AMZN_TRACE_ID = traceId; + } else { + delete process.env._X_AMZN_TRACE_ID; + } + + // Get request ID + const awsRequestId = res.headers["lambda-runtime-aws-request-id"]; + if (typeof awsRequestId !== "string") { + throw new Error( + 'Did not receive "lambda-runtime-aws-request-id" header' + ); + } + + // Get response body + let body = ""; + return new Promise<{ event: any; awsRequestId: string }>( + (resolve, reject) => { + res.on("data", (chunk: string) => (body += chunk)); + + res.on("end", () => { + try { + resolve({ event: JSON.parse(body), awsRequestId }); + } catch (e) { + reject(e); + } + }); + } + ); + } + ); +} + +export async function invokeResponse( + result: VercelResponsePayload, + awsRequestId: string +) { + const res = await performRequest(`invocation/${awsRequestId}/response`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(result), + }); + + if (res.status !== 202) { + throw new Error( + `Unexpected "/invocation/response" response: ${JSON.stringify(res)}` + ); + } +} + +export function invokeError(err: Error, awsRequestId: string) { + return postError(`invocation/${awsRequestId}/error`, err); +} + +export async function postError(path: string, err: Error): Promise { + const lambdaErr = { + errorType: err.name, + errorMessage: err.message, + stackTrace: (err.stack || "").split("\n").slice(1), + }; + + const res = await performRequest(path, { + method: "POST", + headers: { + "Content-Type": "application/json", + "Lambda-Runtime-Function-Error-Type": "Unhandled", + }, + body: JSON.stringify(lambdaErr), + }); + + if (res.status !== 202) { + throw new Error(`Unexpected "${path}" response: ${JSON.stringify(res)}`); + } +} diff --git a/src/runtime/transforms.ts b/src/runtime/transforms.ts new file mode 100644 index 0000000..da0138d --- /dev/null +++ b/src/runtime/transforms.ts @@ -0,0 +1,54 @@ +import { Buffer } from "node:buffer"; + +import type { VercelRequestPayload, VercelResponsePayload } from "./types"; + +export function fromVercelRequest(payload: VercelRequestPayload): Request { + const headers = new Headers(payload.headers); + const base = `${headers.get("x-forwarded-proto")}://${headers.get( + "x-forwarded-host" + )}`; + const url = new URL(payload.path, base); + const body = payload.body ? Buffer.from(payload.body, "base64") : undefined; + + return new Request(url.href, { method: payload.method, headers, body }); +} + +export function headersToVercelHeaders( + headers: Headers +): Record { + const result: Record = {}; + + for (const [name, value] of headers) { + const current = result[name]; + + if (typeof current === "string") { + result[name] = [current, value]; + continue; + } + + if (Array.isArray(current)) { + current.push(value); + continue; + } + + result[name] = value; + } + + return result; +} + +export async function toVercelResponse( + res: Response +): Promise { + const bodyBuffer = await res.arrayBuffer(); + + const body = + bodyBuffer.byteLength > 0 ? Buffer.from(bodyBuffer).toString("base64") : ""; + + return { + statusCode: res.status, + headers: headersToVercelHeaders(res.headers), + encoding: "base64", + body, + }; +} diff --git a/src/runtime/types.ts b/src/runtime/types.ts new file mode 100644 index 0000000..d11374f --- /dev/null +++ b/src/runtime/types.ts @@ -0,0 +1,15 @@ +export type Handler = (req: Request) => Promise; + +export interface VercelRequestPayload { + method: string; + path: string; + headers: Record; + body: string; +} + +export interface VercelResponsePayload { + statusCode: number; + headers: Record; + encoding: "base64"; + body: string; +} diff --git a/src/start-dev-server.ts b/src/start-dev-server.ts new file mode 100644 index 0000000..6fa0850 --- /dev/null +++ b/src/start-dev-server.ts @@ -0,0 +1,3 @@ +import type { StartDevServer } from "@vercel/build-utils"; + +export const startDevServer: StartDevServer | undefined = undefined; diff --git a/src/version.ts b/src/version.ts new file mode 100644 index 0000000..8efabc6 --- /dev/null +++ b/src/version.ts @@ -0,0 +1 @@ +export const version = 3; diff --git a/tsconfig.json b/tsconfig.json index 8d1637c..36c778e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,12 +1,31 @@ { "compilerOptions": { + // Environment setup & latest features "lib": ["ESNext"], - "module": "ESNext", "target": "ESNext", + "module": "CommonJS", + "moduleDetection": "force", + "allowJs": true, + + // Bundler mode "moduleResolution": "node", - "types": ["bun-types"], + "allowImportingTsExtensions": false, + "noEmit": false, + "outDir": "dist", + "declaration": true, "esModuleInterop": true, - "allowJs": true, - "strict": true - } + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist", "src/runtime/**/*.ts"] }