| import { F as prettyPath, ct as join, dt as resolve, m as getBuildInfo } from "../_build/common.mjs"; | ||
| import { n as proxyFetch, r as proxyUpgrade } from "../_libs/httpxy.mjs"; | ||
| import consola from "consola"; | ||
| import { spawn } from "node:child_process"; | ||
| //#region src/preview.ts | ||
| async function startPreview(opts) { | ||
| const { outputDir, buildInfo } = await getBuildInfo(opts.rootDir); | ||
| if (!buildInfo) throw new Error("Cannot load nitro build info. Make sure to build first."); | ||
| const info = [ | ||
| ["Build Directory:", prettyPath(outputDir)], | ||
| ["Date:", buildInfo.date && new Date(buildInfo.date).toLocaleString()], | ||
| ["Nitro Version:", buildInfo.versions.nitro], | ||
| ["Nitro Preset:", buildInfo.preset], | ||
| buildInfo.framework?.name !== "nitro" && ["Framework:", buildInfo.framework?.name + (buildInfo.framework?.version ? ` (v${buildInfo.framework.version})` : "")] | ||
| ].filter((i) => i && i[1]); | ||
| consola.box({ | ||
| title: " [Build Info] ", | ||
| message: info.map((i) => `- ${i[0]} ${i[1]}`).join("\n") | ||
| }); | ||
| const dotEnvEntries = await loadPreviewDotEnv(opts.rootDir); | ||
| if (dotEnvEntries.length > 0) consola.box({ | ||
| title: " [Environment Variables] ", | ||
| message: [ | ||
| "Loaded variables from .env files (preview mode only).", | ||
| "Set platform environment variables for production:", | ||
| ...dotEnvEntries.map(([key, val]) => ` - ${key}`) | ||
| ].join("\n") | ||
| }); | ||
| if (buildInfo.preset.includes("cloudflare")) { | ||
| if (!buildInfo.commands?.preview) throw new Error(`No nitro build preview command found for the "${buildInfo.preset}" preset.`); | ||
| return await runPreviewCommand({ | ||
| command: buildInfo.commands.preview, | ||
| rootDir: opts.rootDir, | ||
| env: dotEnvEntries | ||
| }); | ||
| } | ||
| let fetchHandler = () => Promise.resolve(new Response("Not Found", { status: 404 })); | ||
| if (buildInfo.serverEntry) { | ||
| for (const [key, val] of dotEnvEntries) if (!process.env[key]) process.env[key] = val; | ||
| const { loadServerEntry } = await import("srvx/loader"); | ||
| const entry = await loadServerEntry({ | ||
| entry: resolve(outputDir, buildInfo.serverEntry), | ||
| ...opts.loader | ||
| }); | ||
| if (entry.fetch) fetchHandler = entry.fetch; | ||
| } | ||
| if (buildInfo.publicDir) { | ||
| const { serveStatic } = await import("srvx/static"); | ||
| const staticHandler = serveStatic({ dir: join(outputDir, buildInfo.publicDir) }); | ||
| const originalFetchHandler = fetchHandler; | ||
| fetchHandler = async (req) => { | ||
| const staticRes = await staticHandler(req, () => void 0); | ||
| if (staticRes) return staticRes; | ||
| return originalFetchHandler(req); | ||
| }; | ||
| } | ||
| return { | ||
| fetch: fetchHandler, | ||
| async close() {} | ||
| }; | ||
| } | ||
| async function loadPreviewDotEnv(root) { | ||
| const { loadDotenv } = await import("../_libs/_.mjs"); | ||
| const env = await loadDotenv({ | ||
| cwd: root, | ||
| fileName: [ | ||
| ".env.preview", | ||
| ".env.production", | ||
| ".env" | ||
| ] | ||
| }); | ||
| return Object.entries(env).filter(([_key, val]) => val); | ||
| } | ||
| async function runPreviewCommand(opts) { | ||
| const [arg0, ...args] = opts.command.split(" "); | ||
| consola.info(`Spawning preview server...`); | ||
| consola.info(opts.command); | ||
| console.log(""); | ||
| const { getRandomPort, waitForPort } = await import("get-port-please"); | ||
| const randomPort = await getRandomPort(); | ||
| const child = spawn(arg0, [ | ||
| ...args, | ||
| "--port", | ||
| String(randomPort), | ||
| "--host", | ||
| "localhost" | ||
| ], { | ||
| stdio: "inherit", | ||
| cwd: opts.rootDir, | ||
| env: { | ||
| ...process.env, | ||
| ...Object.fromEntries(opts.env ?? []), | ||
| PORT: String(randomPort) | ||
| } | ||
| }); | ||
| const killChild = (signal) => { | ||
| if (child && !child.killed) child.kill(signal); | ||
| }; | ||
| for (const sig of ["SIGINT", "SIGHUP"]) process.once(sig, () => { | ||
| killChild(sig); | ||
| }); | ||
| child.once("exit", (code) => { | ||
| if (code && code !== 0) consola.error(`[nitro] Preview server exited with code ${code}`); | ||
| }); | ||
| await waitForPort(randomPort, { | ||
| retries: 20, | ||
| delay: 500, | ||
| host: "localhost" | ||
| }); | ||
| return { | ||
| fetch(req) { | ||
| return proxyFetch({ | ||
| port: randomPort, | ||
| host: "localhost" | ||
| }, req); | ||
| }, | ||
| async upgrade(req, socket, head, opts) { | ||
| await proxyUpgrade({ | ||
| port: randomPort, | ||
| host: "localhost" | ||
| }, req, socket, head, opts); | ||
| }, | ||
| async close() { | ||
| killChild("SIGTERM"); | ||
| } | ||
| }; | ||
| } | ||
| //#endregion | ||
| export { startPreview as t }; |
+519
| import { A as src_default, H as v, V as m, ct as join$1, dt as resolve$1, g as writeDevBuildInfo, ot as extname$1 } from "./_build/common.mjs"; | ||
| import { n as watch$1 } from "./_libs/readdirp+chokidar.mjs"; | ||
| import { t as debounce } from "./_libs/perfect-debounce.mjs"; | ||
| import { t as createProxyServer } from "./_libs/httpxy.mjs"; | ||
| import consola from "consola"; | ||
| import { createReadStream } from "node:fs"; | ||
| import { readFile, stat as stat$1 } from "node:fs/promises"; | ||
| import { dirname, resolve } from "node:path"; | ||
| import { joinURL } from "ufo"; | ||
| import { createBrotliCompress, createGzip } from "node:zlib"; | ||
| import { RunnerManager, loadRunner } from "env-runner"; | ||
| import { H3, HTTPError, defineHandler, fromNodeHandler, getRequestIP, getRequestURL, serveStatic, toEventHandler } from "h3"; | ||
| import { serve } from "srvx/node"; | ||
| import { FastResponse } from "srvx"; | ||
| //#region src/dev/vfs.ts | ||
| function createVFSHandler(nitro) { | ||
| return defineHandler(async (event) => { | ||
| const { socket } = event.runtime?.node?.req || {}; | ||
| const ip = getRequestIP(event, { xForwardedFor: !socket?.remoteAddress && !socket?.localAddress && Object.keys(socket?.address?.() || {}).length === 0 && socket?.readable && socket?.writable && !socket?.remotePort }); | ||
| if (!(ip && /^::1$|^127\.\d+\.\d+\.\d+$/.test(ip))) throw new HTTPError({ | ||
| statusText: `Forbidden IP: "${ip || "?"}"`, | ||
| status: 403 | ||
| }); | ||
| const url = event.context.params?._ || ""; | ||
| const isJson = url.endsWith(".json") || event.req.headers.get("accept")?.includes("application/json"); | ||
| const id = decodeURIComponent(url.replace(/^(\.json)?\/?/, "") || ""); | ||
| if (id && !nitro.vfs.has(id)) throw new HTTPError({ | ||
| message: "File not found", | ||
| status: 404 | ||
| }); | ||
| const content = id ? await nitro.vfs.get(id)?.render() : void 0; | ||
| if (isJson) return { | ||
| rootDir: nitro.options.rootDir, | ||
| entries: [...nitro.vfs.keys()].map((id) => ({ | ||
| id, | ||
| path: "/_vfs.json/" + encodeURIComponent(id) | ||
| })), | ||
| current: id ? { | ||
| id, | ||
| content | ||
| } : null | ||
| }; | ||
| const directories = { [nitro.options.rootDir]: {} }; | ||
| const fpaths = [...nitro.vfs.keys()]; | ||
| for (const item of fpaths) { | ||
| const segments = item.replace(nitro.options.rootDir, "").split("/").filter(Boolean); | ||
| let currentDir = item.startsWith(nitro.options.rootDir) ? directories[nitro.options.rootDir] : directories; | ||
| for (const segment of segments) { | ||
| if (!currentDir[segment]) currentDir[segment] = {}; | ||
| currentDir = currentDir[segment]; | ||
| } | ||
| } | ||
| const generateHTML = (directory, path = []) => Object.entries(directory).map(([fname, value = {}]) => { | ||
| const subpath = [...path, fname]; | ||
| const key = subpath.join("/"); | ||
| const encodedUrl = encodeURIComponent(key); | ||
| const linkClass = url === `/${encodedUrl}` ? "bg-gray-700 text-white" : "hover:bg-gray-800 text-gray-200"; | ||
| return Object.keys(value).length === 0 ? ` | ||
| <li class="flex flex-nowrap"> | ||
| <a href="/_vfs/${encodedUrl}" class="w-full text-sm px-2 py-1 border-b border-gray-10 ${linkClass}"> | ||
| ${fname} | ||
| </a> | ||
| </li> | ||
| ` : ` | ||
| <li> | ||
| <details ${url.startsWith(`/${encodedUrl}`) ? "open" : ""}> | ||
| <summary class="w-full text-sm px-2 py-1 border-b border-gray-10 hover:bg-gray-800 text-gray-200"> | ||
| ${fname} | ||
| </summary> | ||
| <ul class="ml-4"> | ||
| ${generateHTML(value, subpath)} | ||
| </ul> | ||
| </details> | ||
| </li> | ||
| `; | ||
| }).join(""); | ||
| const rootDirectory = directories[nitro.options.rootDir]; | ||
| delete directories[nitro.options.rootDir]; | ||
| const files = ` | ||
| <div class="h-full overflow-auto border-r border-gray:10"> | ||
| <p class="text-white text-bold text-center py-1 opacity-50">Virtual Files</p> | ||
| <ul class="flex flex-col">${generateHTML(rootDirectory, [nitro.options.rootDir]) + generateHTML(directories)}</ul> | ||
| </div> | ||
| `; | ||
| const file = id ? editorTemplate({ | ||
| readOnly: true, | ||
| language: id.endsWith("html") ? "html" : "javascript", | ||
| theme: "vs-dark", | ||
| value: content, | ||
| wordWrap: "wordWrapColumn", | ||
| wordWrapColumn: 80 | ||
| }) : ` | ||
| <div class="w-full h-full flex opacity-50"> | ||
| <h1 class="text-white m-auto">Select a virtual file to inspect</h1> | ||
| </div> | ||
| `; | ||
| event.res.headers.set("Content-Type", "text/html; charset=utf-8"); | ||
| return ` | ||
| <!doctype html> | ||
| <html> | ||
| <head> | ||
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@unocss/reset/tailwind.min.css" /> | ||
| <link rel="stylesheet" data-name="vs/editor/editor.main" href="${vsUrl}/editor/editor.main.min.css"> | ||
| <script src="https://cdn.jsdelivr.net/npm/@unocss/runtime"><\/script> | ||
| <style> | ||
| html { | ||
| background: #1E1E1E; | ||
| color: white; | ||
| } | ||
| [un-cloak] { | ||
| display: none; | ||
| } | ||
| </style> | ||
| </head> | ||
| <body class="bg-[#1E1E1E]"> | ||
| <div un-cloak class="h-screen grid grid-cols-[300px_1fr]"> | ||
| ${files} | ||
| ${file} | ||
| </div> | ||
| </body> | ||
| </html>`; | ||
| }); | ||
| } | ||
| const monacoUrl = `https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.30.0/min`; | ||
| const vsUrl = `${monacoUrl}/vs`; | ||
| const editorTemplate = (options) => ` | ||
| <div id="editor" class="min-h-screen w-full h-full"></div> | ||
| <script src="${vsUrl}/loader.min.js"><\/script> | ||
| <script> | ||
| require.config({ paths: { vs: '${vsUrl}' } }) | ||
| const proxy = URL.createObjectURL(new Blob([\` | ||
| self.MonacoEnvironment = { baseUrl: '${monacoUrl}' } | ||
| importScripts('${vsUrl}/base/worker/workerMain.min.js') | ||
| \`], { type: 'text/javascript' })) | ||
| window.MonacoEnvironment = { getWorkerUrl: () => proxy } | ||
| setTimeout(() => { | ||
| require(['vs/editor/editor.main'], function () { | ||
| monaco.editor.create(document.getElementById('editor'), ${JSON.stringify(options)}) | ||
| }) | ||
| }, 0); | ||
| <\/script> | ||
| `; | ||
| //#endregion | ||
| //#region src/runtime/internal/error/utils.ts | ||
| function defineNitroErrorHandler(handler) { | ||
| return handler; | ||
| } | ||
| //#endregion | ||
| //#region src/runtime/internal/error/dev.ts | ||
| const errorHandler = defineNitroErrorHandler(async function defaultNitroErrorHandler(error, event) { | ||
| const res = await defaultHandler(error, event); | ||
| return new FastResponse(typeof res.body === "string" ? res.body : JSON.stringify(res.body, null, 2), res); | ||
| }); | ||
| async function defaultHandler(error, event, opts) { | ||
| const unhandled = error.unhandled ?? !HTTPError.isError(error); | ||
| const { status = 500, statusText = "" } = unhandled ? {} : error; | ||
| const url = getRequestURL(event, { | ||
| xForwardedHost: true, | ||
| xForwardedProto: true | ||
| }); | ||
| if (status === 404) { | ||
| const baseURL = import.meta.baseURL || "/"; | ||
| if (/^\/[^/]/.test(baseURL) && !url.pathname.startsWith(baseURL)) return { | ||
| status: 302, | ||
| statusText: "Found", | ||
| headers: new Headers({ location: `${baseURL}${url.pathname.slice(1)}${url.search}` }), | ||
| body: `Redirecting...` | ||
| }; | ||
| } | ||
| await loadStackTrace(error).catch(consola.error); | ||
| const { Youch } = await import("youch"); | ||
| const youch = new Youch(); | ||
| if (unhandled && !opts?.silent) { | ||
| const ansiError = (await youch.toANSI(error)).replaceAll(process.cwd(), "."); | ||
| consola.error(`[request error] [${event.req.method}] ${url}\n\n`, ansiError); | ||
| } | ||
| const useJSON = opts?.json ?? !event.req.headers.get("accept")?.includes("text/html"); | ||
| const headers = new Headers(unhandled ? {} : error.headers); | ||
| if (useJSON) { | ||
| headers.set("Content-Type", "application/json; charset=utf-8"); | ||
| const jsonBody = typeof error.toJSON === "function" ? error.toJSON() : { | ||
| status, | ||
| statusText, | ||
| message: error.message | ||
| }; | ||
| return { | ||
| status, | ||
| statusText, | ||
| headers, | ||
| body: { | ||
| error: true, | ||
| stack: error.stack?.split("\n").map((line) => line.trim()), | ||
| ...jsonBody | ||
| } | ||
| }; | ||
| } | ||
| headers.set("Content-Type", "text/html; charset=utf-8"); | ||
| return { | ||
| status, | ||
| statusText: unhandled ? "" : error.statusText, | ||
| headers, | ||
| body: await youch.toHTML(error, { request: { | ||
| url: url.href, | ||
| method: event.req.method, | ||
| headers: Object.fromEntries(event.req.headers.entries()) | ||
| } }) | ||
| }; | ||
| } | ||
| async function loadStackTrace(error) { | ||
| if (!(error instanceof Error)) return; | ||
| const { ErrorParser } = await import("youch-core"); | ||
| const parsed = await new ErrorParser().defineSourceLoader(sourceLoader).parse(error); | ||
| const stack = error.message + "\n" + parsed.frames.map((frame) => fmtFrame(frame)).join("\n"); | ||
| Object.defineProperty(error, "stack", { value: stack }); | ||
| if (error.cause) await loadStackTrace(error.cause).catch(consola.error); | ||
| } | ||
| async function sourceLoader(frame) { | ||
| if (!frame.fileName || frame.fileType !== "fs" || frame.type === "native") return; | ||
| if (frame.type === "app") { | ||
| const rawSourceMap = await readFile(`${frame.fileName}.map`, "utf8").catch(() => {}); | ||
| if (rawSourceMap) { | ||
| const { SourceMapConsumer } = await import("source-map"); | ||
| const originalPosition = (await new SourceMapConsumer(rawSourceMap)).originalPositionFor({ | ||
| line: frame.lineNumber, | ||
| column: frame.columnNumber | ||
| }); | ||
| if (originalPosition.source && originalPosition.line) { | ||
| frame.fileName = resolve(dirname(frame.fileName), originalPosition.source); | ||
| frame.lineNumber = originalPosition.line; | ||
| frame.columnNumber = originalPosition.column || 0; | ||
| } | ||
| } | ||
| } | ||
| const contents = await readFile(frame.fileName, "utf8").catch(() => {}); | ||
| return contents ? { contents } : void 0; | ||
| } | ||
| function fmtFrame(frame) { | ||
| if (frame.type === "native") return frame.raw; | ||
| const src = `${frame.fileName || ""}:${frame.lineNumber}:${frame.columnNumber})`; | ||
| return frame.functionName ? `at ${frame.functionName} (${src}` : `at ${src}`; | ||
| } | ||
| //#endregion | ||
| //#region src/dev/app.ts | ||
| var NitroDevApp = class { | ||
| nitro; | ||
| fetch; | ||
| constructor(nitro, catchAllHandler) { | ||
| this.nitro = nitro; | ||
| const app = this.#createApp(catchAllHandler); | ||
| this.fetch = app.fetch.bind(app); | ||
| } | ||
| #createApp(catchAllHandler) { | ||
| const app = new H3({ | ||
| debug: true, | ||
| onError: async (error, event) => { | ||
| const errorHandler$1 = this.nitro.options.devErrorHandler || errorHandler; | ||
| await loadStackTrace(error).catch(() => {}); | ||
| return errorHandler$1(error, event, { defaultHandler }); | ||
| } | ||
| }); | ||
| for (const h of this.nitro.options.devHandlers) { | ||
| const handler = toEventHandler(h.handler); | ||
| if (!handler) { | ||
| this.nitro.logger.warn("Invalid dev handler:", h); | ||
| continue; | ||
| } | ||
| if (h.middleware || !h.route) if (h.route) app.use(h.route, handler, { method: h.method }); | ||
| else app.use(handler, { method: h.method }); | ||
| else app.on(h.method || "", h.route, handler, { meta: h.meta }); | ||
| } | ||
| app.get("/_vfs/**", createVFSHandler(this.nitro)); | ||
| for (const asset of this.nitro.options.publicAssets) { | ||
| const assetBase = joinURL(this.nitro.options.baseURL, asset.baseURL || "/"); | ||
| app.use(joinURL(assetBase, "**"), (event) => serveStaticDir(event, { | ||
| dir: asset.dir, | ||
| base: assetBase, | ||
| fallthrough: asset.fallthrough | ||
| })); | ||
| } | ||
| const routes = Object.keys(this.nitro.options.devProxy).sort().reverse(); | ||
| for (const route of routes) { | ||
| let opts = this.nitro.options.devProxy[route]; | ||
| if (typeof opts === "string") opts = { target: opts }; | ||
| const proxy = createHTTPProxy(opts); | ||
| app.all(route, proxy.handleEvent); | ||
| } | ||
| if (catchAllHandler) app.all("/**", catchAllHandler); | ||
| return app; | ||
| } | ||
| }; | ||
| function serveStaticDir(event, opts) { | ||
| const dir = resolve$1(opts.dir) + "/"; | ||
| const r = (id) => { | ||
| if (!id.startsWith(opts.base) || !extname$1(id)) return; | ||
| const resolved = join$1(dir, id.slice(opts.base.length)); | ||
| if (resolved.startsWith(dir)) return resolved; | ||
| }; | ||
| return serveStatic(event, { | ||
| fallthrough: opts.fallthrough, | ||
| getMeta: async (id) => { | ||
| const path = r(id); | ||
| if (!path) return; | ||
| const s = await stat$1(path).catch(() => null); | ||
| if (!s?.isFile()) return; | ||
| const ext = extname$1(path); | ||
| return { | ||
| size: s.size, | ||
| mtime: s.mtime, | ||
| type: src_default.getType(ext) || "application/octet-stream" | ||
| }; | ||
| }, | ||
| getContents(id) { | ||
| const path = r(id); | ||
| if (!path) return; | ||
| const stream = createReadStream(path); | ||
| const acceptEncoding = event.req.headers.get("accept-encoding") || ""; | ||
| if (acceptEncoding.includes("br")) { | ||
| event.res.headers.set("Content-Encoding", "br"); | ||
| event.res.headers.delete("Content-Length"); | ||
| event.res.headers.set("Vary", "Accept-Encoding"); | ||
| return stream.pipe(createBrotliCompress()); | ||
| } else if (acceptEncoding.includes("gzip")) { | ||
| event.res.headers.set("Content-Encoding", "gzip"); | ||
| event.res.headers.delete("Content-Length"); | ||
| event.res.headers.set("Vary", "Accept-Encoding"); | ||
| return stream.pipe(createGzip()); | ||
| } | ||
| return stream; | ||
| } | ||
| }); | ||
| } | ||
| function createHTTPProxy(defaults = {}) { | ||
| const proxy = createProxyServer({ | ||
| xfwd: true, | ||
| ...defaults | ||
| }); | ||
| return { | ||
| proxy, | ||
| async handleEvent(event, opts) { | ||
| try { | ||
| return await fromNodeHandler((req, res) => { | ||
| return proxy.web(req, res, opts); | ||
| })(event); | ||
| } catch (error) { | ||
| event.res.headers.set("refresh", "3"); | ||
| throw new HTTPError({ | ||
| status: 503, | ||
| message: "Dev server is unavailable.", | ||
| cause: error | ||
| }); | ||
| } | ||
| } | ||
| }; | ||
| } | ||
| //#endregion | ||
| //#region src/dev/server.ts | ||
| function createDevServer(nitro) { | ||
| return new NitroDevServer(nitro); | ||
| } | ||
| var NitroDevServer = class NitroDevServer extends NitroDevApp { | ||
| #entry; | ||
| #workerData = {}; | ||
| #listeners = []; | ||
| #watcher; | ||
| #manager; | ||
| #workerIdCtr = 0; | ||
| #workerError; | ||
| #workerRetries = 0; | ||
| #building = true; | ||
| #buildError; | ||
| #reloadPromise; | ||
| constructor(nitro) { | ||
| super(nitro, async (event) => { | ||
| if (this.#building) await this.#waitForBuild(); | ||
| if (this.#reloadPromise) await this.#reloadPromise; | ||
| if (this.#buildError) return this.#generateError(); | ||
| const response = await this.#manager.fetch(event.req); | ||
| if (response.status === 503 && !this.#manager.ready) return this.#generateError(); | ||
| return response; | ||
| }); | ||
| for (const key of Object.getOwnPropertyNames(NitroDevServer.prototype)) { | ||
| const value = this[key]; | ||
| if (typeof value === "function" && key !== "constructor") this[key] = value.bind(this); | ||
| } | ||
| nitro.fetch = this.fetch.bind(this); | ||
| this.#entry = resolve$1(nitro.options.output.dir, nitro.options.output.serverDir, "index.mjs"); | ||
| this.#manager = new RunnerManager(); | ||
| this.#manager.onReady(async (_runner, addr) => { | ||
| this.#workerRetries = 0; | ||
| writeDevBuildInfo(this.nitro, addr).catch((error) => { | ||
| this.nitro.logger.warn(`Failed to write dev build info: ${error instanceof Error ? error.message : String(error)}`); | ||
| }); | ||
| }); | ||
| this.#manager.onClose((_runner, cause) => { | ||
| this.#workerError = cause; | ||
| if (this.#workerRetries++ < 3) { | ||
| this.nitro.logger.info("Restarting dev worker...", cause ? `Cause: ${cause}` : ""); | ||
| this.reload(); | ||
| } else this.nitro.logger.error("Dev worker failed after 3 retries.", cause ? `Last cause: ${cause}` : ""); | ||
| }); | ||
| nitro.hooks.hook("close", () => this.close()); | ||
| nitro.hooks.hook("dev:start", () => { | ||
| this.#building = true; | ||
| this.#buildError = void 0; | ||
| }); | ||
| nitro.hooks.hook("dev:reload", (payload) => { | ||
| this.#buildError = void 0; | ||
| this.#building = false; | ||
| if (payload?.entry) this.#entry = payload.entry; | ||
| if (payload?.workerData) this.#workerData = payload.workerData; | ||
| this.reload(); | ||
| }); | ||
| nitro.hooks.hook("dev:error", (cause) => { | ||
| this.#buildError = cause; | ||
| this.#building = false; | ||
| }); | ||
| const devWatch = nitro.options.devServer.watch; | ||
| if (devWatch && devWatch.length > 0) { | ||
| const debouncedReload = debounce(() => this.reload()); | ||
| this.#watcher = watch$1(devWatch, nitro.options.watchOptions); | ||
| this.#watcher.on("add", debouncedReload).on("change", debouncedReload); | ||
| } | ||
| } | ||
| async upgrade(req, socket, head) { | ||
| if (!this.#manager.upgrade) throw new HTTPError({ | ||
| status: 501, | ||
| statusText: "Worker does not support upgrades." | ||
| }); | ||
| return this.#manager.upgrade({ node: { | ||
| req, | ||
| socket, | ||
| head | ||
| } }); | ||
| } | ||
| listen(opts) { | ||
| const server = serve({ | ||
| ...opts, | ||
| fetch: this.fetch, | ||
| gracefulShutdown: false | ||
| }); | ||
| this.#listeners.push(server); | ||
| if (server.node?.server) server.node.server.on("upgrade", (req, sock, head) => this.upgrade(req, sock, head)); | ||
| return server; | ||
| } | ||
| async close() { | ||
| await Promise.all([ | ||
| Promise.all(this.#listeners.map((l) => l.close())).then(() => { | ||
| this.#listeners = []; | ||
| }), | ||
| this.#manager.close(), | ||
| Promise.resolve(this.#watcher?.close()).then(() => { | ||
| this.#watcher = void 0; | ||
| }) | ||
| ].map((p) => p.catch((error) => { | ||
| consola.error(error); | ||
| }))); | ||
| } | ||
| reload() { | ||
| const nextReload = (this.#reloadPromise ?? Promise.resolve()).catch(() => {}).then(() => this.#reload()); | ||
| this.#reloadPromise = nextReload.finally(() => { | ||
| if (this.#reloadPromise === nextReload) this.#reloadPromise = void 0; | ||
| }); | ||
| } | ||
| async #reload() { | ||
| const runner = await loadRunner(this.nitro.options.devServer.runner || process.env.NITRO_DEV_RUNNER || "node-worker", { | ||
| name: `Nitro_${this.#workerIdCtr++}`, | ||
| data: { | ||
| entry: this.#entry, | ||
| ...this.#workerData | ||
| } | ||
| }); | ||
| await this.#manager.reload(runner); | ||
| } | ||
| sendMessage(message) { | ||
| this.#manager.sendMessage(message); | ||
| } | ||
| onMessage(listener) { | ||
| this.#manager.onMessage(listener); | ||
| } | ||
| offMessage(listener) { | ||
| this.#manager.offMessage(listener); | ||
| } | ||
| async #waitForBuild() { | ||
| const timeout = v || m ? 6e4 : 6e3; | ||
| await this.#manager.waitForReady(timeout); | ||
| } | ||
| #generateError() { | ||
| const error = this.#buildError || this.#workerError; | ||
| if (error) { | ||
| try { | ||
| error.unhandled = false; | ||
| let id = error.id || error.path; | ||
| if (id) { | ||
| const cause = error.errors?.[0]; | ||
| const loc = error.location || error.loc || cause?.location || cause?.loc; | ||
| if (loc) id += `:${loc.line}:${loc.column}`; | ||
| error.stack = (error.stack || "").replace(/(^\s*at\s+.+)/m, ` at ${id}\n$1`); | ||
| } | ||
| } catch {} | ||
| return new HTTPError(error); | ||
| } | ||
| return new Response(JSON.stringify({ | ||
| error: "Dev server is unavailable.", | ||
| hint: "Please reload the page and check the console for errors if the issue persists." | ||
| }, null, 2), { | ||
| status: 503, | ||
| statusText: "Dev server is unavailable", | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| "Cache-Control": "no-store", | ||
| Refresh: "3" | ||
| } | ||
| }); | ||
| } | ||
| }; | ||
| //#endregion | ||
| export { createDevServer as n, NitroDevApp as r, NitroDevServer as t }; |
| import { a as loadDotenv } from "./c12+rc9.mjs"; | ||
| import "../_build/common.mjs"; | ||
| export { loadDotenv }; |
| import { t as mergeAssets } from "./pluginutils.mjs"; | ||
| export { mergeAssets }; |
| import "../_build/common.mjs"; | ||
| import { t as inject } from "./plugin-inject.mjs"; | ||
| export { inject as default }; |
| import { n as utils_exports, t as dist_exports } from "./c12+rc9.mjs"; | ||
| export { dist_exports as n, utils_exports as t }; |
| import "../_build/common.mjs"; | ||
| import { n as addDevDependency } from "./nypm+tinyexec.mjs"; | ||
| export { addDevDependency }; |
| import "../_build/common.mjs"; | ||
| import "./estree-walker.mjs"; | ||
| import { a as detectImportsAcorn } from "./unimport+unplugin.mjs"; | ||
| export { detectImportsAcorn }; |
| import "../_build/common.mjs"; | ||
| import { r as unplugin } from "./unimport+unplugin.mjs"; | ||
| import "./resolve-uri+gen-mapping.mjs"; | ||
| import "./remapping.mjs"; | ||
| export { unplugin as default }; |
| import { J as readPackageJSON, K as findWorkspaceDir, at as dirname$1, ct as join$1, dt as resolve$1, it as basename$1, lt as normalize$1, nt as resolveModulePath, ot as extname$1 } from "../_build/common.mjs"; | ||
| import { existsSync, readFileSync, statSync } from "node:fs"; | ||
| import * as nodeUtil from "node:util"; | ||
| import { readFile, rm } from "node:fs/promises"; | ||
| import { pathToFileURL } from "node:url"; | ||
| import { homedir } from "node:os"; | ||
| import { resolve } from "node:path"; | ||
| import destr from "destr"; | ||
| import { defu } from "defu"; | ||
| import { createHash } from "node:crypto"; | ||
| //#region node_modules/.pnpm/c12@4.0.0-beta.3_chokidar@5.0.0_dotenv@17.3.1_giget@3.1.2_jiti@2.6.1_magicast@0.5.2/node_modules/c12/dist/_chunks/libs/perfect-debounce.mjs | ||
| const DEBOUNCE_DEFAULTS = { trailing: true }; | ||
| function debounce(fn, wait = 25, options = {}) { | ||
| options = { | ||
| ...DEBOUNCE_DEFAULTS, | ||
| ...options | ||
| }; | ||
| if (!Number.isFinite(wait)) throw new TypeError("Expected `wait` to be a finite number"); | ||
| let leadingValue; | ||
| let timeout; | ||
| let resolveList = []; | ||
| let currentPromise; | ||
| let trailingArgs; | ||
| const applyFn = (_this, args) => { | ||
| currentPromise = _applyPromised(fn, _this, args); | ||
| currentPromise.finally(() => { | ||
| currentPromise = null; | ||
| if (options.trailing && trailingArgs && !timeout) { | ||
| const promise = applyFn(_this, trailingArgs); | ||
| trailingArgs = null; | ||
| return promise; | ||
| } | ||
| }); | ||
| return currentPromise; | ||
| }; | ||
| const debounced = function(...args) { | ||
| if (options.trailing) trailingArgs = args; | ||
| if (currentPromise) return currentPromise; | ||
| return new Promise((resolve) => { | ||
| const shouldCallNow = !timeout && options.leading; | ||
| clearTimeout(timeout); | ||
| timeout = setTimeout(() => { | ||
| timeout = null; | ||
| const promise = options.leading ? leadingValue : applyFn(this, args); | ||
| trailingArgs = null; | ||
| for (const _resolve of resolveList) _resolve(promise); | ||
| resolveList = []; | ||
| }, wait); | ||
| if (shouldCallNow) { | ||
| leadingValue = applyFn(this, args); | ||
| resolve(leadingValue); | ||
| } else resolveList.push(resolve); | ||
| }); | ||
| }; | ||
| const _clearTimeout = (timer) => { | ||
| if (timer) { | ||
| clearTimeout(timer); | ||
| timeout = null; | ||
| } | ||
| }; | ||
| debounced.isPending = () => !!timeout; | ||
| debounced.cancel = () => { | ||
| _clearTimeout(timeout); | ||
| resolveList = []; | ||
| trailingArgs = null; | ||
| }; | ||
| debounced.flush = () => { | ||
| _clearTimeout(timeout); | ||
| if (!trailingArgs || currentPromise) return; | ||
| const args = trailingArgs; | ||
| trailingArgs = null; | ||
| return applyFn(this, args); | ||
| }; | ||
| return debounced; | ||
| } | ||
| async function _applyPromised(fn, _this, args) { | ||
| return await fn.apply(_this, args); | ||
| } | ||
| //#endregion | ||
| //#region node_modules/.pnpm/rc9@3.0.0/node_modules/rc9/dist/_chunks/libs/flat.mjs | ||
| function isBuffer(obj) { | ||
| return obj && obj.constructor && typeof obj.constructor.isBuffer === "function" && obj.constructor.isBuffer(obj); | ||
| } | ||
| function keyIdentity(key) { | ||
| return key; | ||
| } | ||
| function flatten(target, opts) { | ||
| opts = opts || {}; | ||
| const delimiter = opts.delimiter || "."; | ||
| const maxDepth = opts.maxDepth; | ||
| const transformKey = opts.transformKey || keyIdentity; | ||
| const output = {}; | ||
| function step(object, prev, currentDepth) { | ||
| currentDepth = currentDepth || 1; | ||
| Object.keys(object).forEach(function(key) { | ||
| const value = object[key]; | ||
| const isarray = opts.safe && Array.isArray(value); | ||
| const type = Object.prototype.toString.call(value); | ||
| const isbuffer = isBuffer(value); | ||
| const isobject = type === "[object Object]" || type === "[object Array]"; | ||
| const newKey = prev ? prev + delimiter + transformKey(key) : transformKey(key); | ||
| if (!isarray && !isbuffer && isobject && Object.keys(value).length && (!opts.maxDepth || currentDepth < maxDepth)) return step(value, newKey, currentDepth + 1); | ||
| output[newKey] = value; | ||
| }); | ||
| } | ||
| step(target); | ||
| return output; | ||
| } | ||
| function unflatten(target, opts) { | ||
| opts = opts || {}; | ||
| const delimiter = opts.delimiter || "."; | ||
| const overwrite = opts.overwrite || false; | ||
| const transformKey = opts.transformKey || keyIdentity; | ||
| const result = {}; | ||
| if (isBuffer(target) || Object.prototype.toString.call(target) !== "[object Object]") return target; | ||
| function getkey(key) { | ||
| const parsedKey = Number(key); | ||
| return isNaN(parsedKey) || key.indexOf(".") !== -1 || opts.object ? key : parsedKey; | ||
| } | ||
| function addKeys(keyPrefix, recipient, target) { | ||
| return Object.keys(target).reduce(function(result, key) { | ||
| result[keyPrefix + delimiter + key] = target[key]; | ||
| return result; | ||
| }, recipient); | ||
| } | ||
| function isEmpty(val) { | ||
| const type = Object.prototype.toString.call(val); | ||
| const isArray = type === "[object Array]"; | ||
| const isObject = type === "[object Object]"; | ||
| if (!val) return true; | ||
| else if (isArray) return !val.length; | ||
| else if (isObject) return !Object.keys(val).length; | ||
| } | ||
| target = Object.keys(target).reduce(function(result, key) { | ||
| const type = Object.prototype.toString.call(target[key]); | ||
| if (!(type === "[object Object]" || type === "[object Array]") || isEmpty(target[key])) { | ||
| result[key] = target[key]; | ||
| return result; | ||
| } else return addKeys(key, result, flatten(target[key], opts)); | ||
| }, {}); | ||
| Object.keys(target).forEach(function(key) { | ||
| const split = key.split(delimiter).map(transformKey); | ||
| let key1 = getkey(split.shift()); | ||
| let key2 = getkey(split[0]); | ||
| let recipient = result; | ||
| while (key2 !== void 0) { | ||
| if (key1 === "__proto__") return; | ||
| const type = Object.prototype.toString.call(recipient[key1]); | ||
| const isobject = type === "[object Object]" || type === "[object Array]"; | ||
| if (!overwrite && !isobject && typeof recipient[key1] !== "undefined") return; | ||
| if (overwrite && !isobject || !overwrite && recipient[key1] == null) recipient[key1] = typeof key2 === "number" && !opts.object ? [] : {}; | ||
| recipient = recipient[key1]; | ||
| if (split.length > 0) { | ||
| key1 = getkey(split.shift()); | ||
| key2 = getkey(split[0]); | ||
| } | ||
| } | ||
| recipient[key1] = unflatten(target[key], opts); | ||
| }); | ||
| return result; | ||
| } | ||
| //#endregion | ||
| //#region node_modules/.pnpm/rc9@3.0.0/node_modules/rc9/dist/index.mjs | ||
| const RE_KEY_VAL = /^\s*([^\s=]+)\s*=\s*(.*)?\s*$/; | ||
| const RE_LINES = /\n|\r|\r\n/; | ||
| const defaults = { | ||
| name: ".conf", | ||
| dir: process.cwd(), | ||
| flat: false | ||
| }; | ||
| function withDefaults(options) { | ||
| if (typeof options === "string") options = { name: options }; | ||
| return { | ||
| ...defaults, | ||
| ...options | ||
| }; | ||
| } | ||
| function parse(contents, options = {}) { | ||
| const config = {}; | ||
| const lines = contents.split(RE_LINES); | ||
| for (const line of lines) { | ||
| const match = line.match(RE_KEY_VAL); | ||
| if (!match) continue; | ||
| const key = match[1]; | ||
| if (!key || key === "__proto__" || key === "constructor") continue; | ||
| const value = destr((match[2] || "").trim()); | ||
| if (key.endsWith("[]")) { | ||
| const nkey = key.slice(0, Math.max(0, key.length - 2)); | ||
| config[nkey] = (config[nkey] || []).concat(value); | ||
| continue; | ||
| } | ||
| config[key] = value; | ||
| } | ||
| return options.flat ? config : unflatten(config, { overwrite: true }); | ||
| } | ||
| function parseFile(path, options) { | ||
| if (!existsSync(path)) return {}; | ||
| return parse(readFileSync(path, "utf8"), options); | ||
| } | ||
| function read(options) { | ||
| options = withDefaults(options); | ||
| return parseFile(resolve(options.dir, options.name), options); | ||
| } | ||
| function readUser(options) { | ||
| options = withDefaults(options); | ||
| options.dir = process.env.XDG_CONFIG_HOME || homedir(); | ||
| return read(options); | ||
| } | ||
| //#endregion | ||
| //#region node_modules/.pnpm/c12@4.0.0-beta.3_chokidar@5.0.0_dotenv@17.3.1_giget@3.1.2_jiti@2.6.1_magicast@0.5.2/node_modules/c12/dist/index.mjs | ||
| async function setupDotenv(options) { | ||
| const targetEnvironment = options.env ?? process.env; | ||
| const environment = await loadDotenv({ | ||
| cwd: options.cwd, | ||
| fileName: options.fileName ?? ".env", | ||
| env: targetEnvironment, | ||
| interpolate: options.interpolate ?? true, | ||
| expandFileReferences: options.expandFileReferences ?? false | ||
| }); | ||
| const dotenvVars = getDotEnvVars(targetEnvironment); | ||
| for (const key in environment) { | ||
| if (key.startsWith("_")) continue; | ||
| if (targetEnvironment[key] === void 0 || dotenvVars.has(key)) targetEnvironment[key] = environment[key]; | ||
| } | ||
| return environment; | ||
| } | ||
| async function loadDotenv(options) { | ||
| const environment = Object.create(null); | ||
| const cwd = resolve$1(options.cwd || "."); | ||
| const _fileName = options.fileName || ".env"; | ||
| const dotenvFiles = typeof _fileName === "string" ? [_fileName] : _fileName; | ||
| const dotenvVars = getDotEnvVars(options.env || {}); | ||
| Object.assign(environment, options.env); | ||
| for (const file of dotenvFiles) { | ||
| const dotenvFile = resolve$1(cwd, file); | ||
| if (!statSync(dotenvFile, { throwIfNoEntry: false })?.isFile()) continue; | ||
| const parsed = await readEnvFile(dotenvFile); | ||
| for (const key in parsed) { | ||
| if (key in environment && !dotenvVars.has(key)) continue; | ||
| environment[key] = parsed[key]; | ||
| dotenvVars.add(key); | ||
| } | ||
| } | ||
| if (options.expandFileReferences) { | ||
| for (const key in environment) if (key.endsWith("_FILE")) { | ||
| const targetKey = key.slice(0, -5); | ||
| if (environment[targetKey] === void 0) { | ||
| const filePath = environment[key]; | ||
| if (filePath && statSync(filePath, { throwIfNoEntry: false })?.isFile()) { | ||
| environment[targetKey] = readFileSync(filePath, "utf8").trim(); | ||
| dotenvVars.add(targetKey); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| if (options.interpolate) interpolate(environment); | ||
| return environment; | ||
| } | ||
| let _parseEnv = nodeUtil.parseEnv; | ||
| async function readEnvFile(path) { | ||
| const src = readFileSync(path, "utf8"); | ||
| if (!_parseEnv) try { | ||
| const dotenv = await import("dotenv"); | ||
| _parseEnv = (src) => dotenv.parse(src); | ||
| } catch { | ||
| throw new Error("Failed to parse .env file: `node:util.parseEnv` is not available and `dotenv` package is not installed. Please upgrade your runtime or install `dotenv` as a dependency."); | ||
| } | ||
| return _parseEnv(src); | ||
| } | ||
| function interpolate(target, source = {}, parse = (v) => v) { | ||
| function getValue(key) { | ||
| return source[key] === void 0 ? target[key] : source[key]; | ||
| } | ||
| function interpolate(value, parents = []) { | ||
| if (typeof value !== "string") return value; | ||
| return parse((value.match(/(.?\${?(?:[\w:]+)?}?)/g) || []).reduce((newValue, match) => { | ||
| const parts = /(.?)\${?([\w:]+)?}?/g.exec(match) || []; | ||
| const prefix = parts[1]; | ||
| let value, replacePart; | ||
| if (prefix === "\\") { | ||
| replacePart = parts[0] || ""; | ||
| value = replacePart.replace(String.raw`\$`, "$"); | ||
| } else { | ||
| const key = parts[2]; | ||
| replacePart = (parts[0] || "").slice(prefix.length); | ||
| if (parents.includes(key)) { | ||
| console.warn(`Please avoid recursive environment variables ( loop: ${parents.join(" > ")} > ${key} )`); | ||
| return ""; | ||
| } | ||
| value = getValue(key); | ||
| value = interpolate(value, [...parents, key]); | ||
| } | ||
| return value === void 0 ? newValue : newValue.replace(replacePart, value); | ||
| }, value)); | ||
| } | ||
| for (const key in target) target[key] = interpolate(getValue(key)); | ||
| } | ||
| function getDotEnvVars(targetEnvironment) { | ||
| const globalRegistry = globalThis.__c12_dotenv_vars__ ||= /* @__PURE__ */ new Map(); | ||
| if (!globalRegistry.has(targetEnvironment)) globalRegistry.set(targetEnvironment, /* @__PURE__ */ new Set()); | ||
| return globalRegistry.get(targetEnvironment); | ||
| } | ||
| const _normalize = (p) => p?.replace(/\\/g, "/"); | ||
| const ASYNC_LOADERS = { | ||
| ".yaml": () => import("../_build/common.mjs").then((n) => n.Q).then((r) => r.parseYAML), | ||
| ".yml": () => import("../_build/common.mjs").then((n) => n.Q).then((r) => r.parseYAML), | ||
| ".jsonc": () => import("../_build/common.mjs").then((n) => n.$).then((r) => r.parseJSONC), | ||
| ".json5": () => import("../_build/common.mjs").then((n) => n.tt).then((r) => r.parseJSON5), | ||
| ".toml": () => import("../_build/common.mjs").then((n) => n.X).then((r) => r.parseTOML) | ||
| }; | ||
| const SUPPORTED_EXTENSIONS = Object.freeze([ | ||
| ".js", | ||
| ".ts", | ||
| ".mjs", | ||
| ".cjs", | ||
| ".mts", | ||
| ".cts", | ||
| ".json", | ||
| ".jsonc", | ||
| ".json5", | ||
| ".yaml", | ||
| ".yml", | ||
| ".toml" | ||
| ]); | ||
| async function loadConfig(options) { | ||
| options.cwd = resolve$1(process.cwd(), options.cwd || "."); | ||
| options.name = options.name || "config"; | ||
| options.envName = options.envName ?? process.env.NODE_ENV; | ||
| options.configFile = options.configFile ?? (options.name === "config" ? "config" : `${options.name}.config`); | ||
| options.rcFile = options.rcFile ?? `.${options.name}rc`; | ||
| if (options.extend !== false) options.extend = { | ||
| extendKey: "extends", | ||
| ...options.extend | ||
| }; | ||
| const _merger = options.merger || defu; | ||
| const r = { | ||
| config: {}, | ||
| cwd: options.cwd, | ||
| configFile: resolve$1(options.cwd, options.configFile), | ||
| layers: [], | ||
| _configFile: void 0 | ||
| }; | ||
| const rawConfigs = { | ||
| overrides: options.overrides, | ||
| main: void 0, | ||
| rc: void 0, | ||
| packageJson: void 0, | ||
| defaultConfig: options.defaultConfig | ||
| }; | ||
| if (options.dotenv) await setupDotenv({ | ||
| cwd: options.cwd, | ||
| ...options.dotenv === true ? {} : options.dotenv | ||
| }); | ||
| const _mainConfig = await resolveConfig(".", options); | ||
| if (_mainConfig.configFile) { | ||
| rawConfigs.main = _mainConfig.config; | ||
| r.configFile = _mainConfig.configFile; | ||
| r._configFile = _mainConfig._configFile; | ||
| } | ||
| if (_mainConfig.meta) r.meta = _mainConfig.meta; | ||
| if (options.rcFile) { | ||
| const rcSources = []; | ||
| rcSources.push(read({ | ||
| name: options.rcFile, | ||
| dir: options.cwd | ||
| })); | ||
| if (options.globalRc) { | ||
| const workspaceDir = await findWorkspaceDir(options.cwd).catch(() => {}); | ||
| if (workspaceDir) rcSources.push(read({ | ||
| name: options.rcFile, | ||
| dir: workspaceDir | ||
| })); | ||
| rcSources.push(readUser({ | ||
| name: options.rcFile, | ||
| dir: options.cwd | ||
| })); | ||
| } | ||
| rawConfigs.rc = _merger({}, ...rcSources); | ||
| } | ||
| if (options.packageJson) { | ||
| const keys = (Array.isArray(options.packageJson) ? options.packageJson : [typeof options.packageJson === "string" ? options.packageJson : options.name]).filter((t) => t && typeof t === "string"); | ||
| const pkgJsonFile = await readPackageJSON(options.cwd).catch(() => {}); | ||
| rawConfigs.packageJson = _merger({}, ...keys.map((key) => pkgJsonFile?.[key])); | ||
| } | ||
| const configs = {}; | ||
| for (const key in rawConfigs) { | ||
| const value = rawConfigs[key]; | ||
| configs[key] = await (typeof value === "function" ? value({ | ||
| configs, | ||
| rawConfigs | ||
| }) : value); | ||
| } | ||
| if (Array.isArray(configs.main)) r.config = configs.main; | ||
| else { | ||
| r.config = _merger(configs.overrides, configs.main, configs.rc, configs.packageJson, configs.defaultConfig); | ||
| if (options.extend) { | ||
| await extendConfig(r.config, options); | ||
| r.layers = r.config._layers; | ||
| delete r.config._layers; | ||
| r.config = _merger(r.config, ...r.layers.map((e) => e.config)); | ||
| } | ||
| } | ||
| r.layers = [...[ | ||
| configs.overrides && { | ||
| config: configs.overrides, | ||
| configFile: void 0, | ||
| cwd: void 0 | ||
| }, | ||
| { | ||
| config: configs.main, | ||
| configFile: options.configFile, | ||
| cwd: options.cwd | ||
| }, | ||
| configs.rc && { | ||
| config: configs.rc, | ||
| configFile: options.rcFile | ||
| }, | ||
| configs.packageJson && { | ||
| config: configs.packageJson, | ||
| configFile: "package.json" | ||
| } | ||
| ].filter((l) => l && l.config), ...r.layers]; | ||
| if (options.defaults) r.config = _merger(r.config, options.defaults); | ||
| if (options.omit$Keys) { | ||
| for (const key in r.config) if (key.startsWith("$")) delete r.config[key]; | ||
| } | ||
| if (options.configFileRequired && !r._configFile) throw new Error(`Required config (${r.configFile}) cannot be resolved.`); | ||
| return r; | ||
| } | ||
| async function extendConfig(config, options) { | ||
| config._layers = config._layers || []; | ||
| if (!options.extend) return; | ||
| let keys = options.extend.extendKey; | ||
| if (typeof keys === "string") keys = [keys]; | ||
| const extendSources = []; | ||
| for (const key of keys) { | ||
| extendSources.push(...(Array.isArray(config[key]) ? config[key] : [config[key]]).filter(Boolean)); | ||
| delete config[key]; | ||
| } | ||
| for (let extendSource of extendSources) { | ||
| const originalExtendSource = extendSource; | ||
| let sourceOptions = {}; | ||
| if (extendSource.source) { | ||
| sourceOptions = extendSource.options || {}; | ||
| extendSource = extendSource.source; | ||
| } | ||
| if (Array.isArray(extendSource)) { | ||
| sourceOptions = extendSource[1] || {}; | ||
| extendSource = extendSource[0]; | ||
| } | ||
| if (typeof extendSource !== "string") { | ||
| console.warn(`Cannot extend config from \`${JSON.stringify(originalExtendSource)}\` in ${options.cwd}`); | ||
| continue; | ||
| } | ||
| const _config = await resolveConfig(extendSource, options, sourceOptions); | ||
| if (!_config.config) { | ||
| console.warn(`Cannot extend config from \`${extendSource}\` in ${options.cwd}`); | ||
| continue; | ||
| } | ||
| await extendConfig(_config.config, { | ||
| ...options, | ||
| cwd: _config.cwd | ||
| }); | ||
| config._layers.push(_config); | ||
| if (_config.config._layers) { | ||
| config._layers.push(..._config.config._layers); | ||
| delete _config.config._layers; | ||
| } | ||
| } | ||
| } | ||
| const GIGET_PREFIXES = [ | ||
| "gh:", | ||
| "github:", | ||
| "gitlab:", | ||
| "bitbucket:", | ||
| "https://", | ||
| "http://" | ||
| ]; | ||
| const NPM_PACKAGE_RE = /^(@[\da-z~-][\d._a-z~-]*\/)?[\da-z~-][\d._a-z~-]*($|\/.*)/; | ||
| async function resolveConfig(source, options, sourceOptions = {}) { | ||
| if (options.resolve) { | ||
| const res = await options.resolve(source, options); | ||
| if (res) return res; | ||
| } | ||
| const _merger = options.merger || defu; | ||
| const customProviderKeys = Object.keys(sourceOptions.giget?.providers || {}).map((key) => `${key}:`); | ||
| const gigetPrefixes = customProviderKeys.length > 0 ? [...new Set([...customProviderKeys, ...GIGET_PREFIXES])] : GIGET_PREFIXES; | ||
| if (options.giget !== false && gigetPrefixes.some((prefix) => source.startsWith(prefix))) { | ||
| const { downloadTemplate } = await import("giget").catch((error) => { | ||
| throw new Error(`Extending config from \`${source}\` requires \`giget\` peer dependency to be installed.\n\nInstall it with: \`npx nypm i giget\``, { cause: error }); | ||
| }); | ||
| const { digest } = await import("./_4.mjs").then((n) => n.n); | ||
| const cloneName = source.replace(/\W+/g, "_").split("_").splice(0, 3).join("_") + "_" + digest(source).slice(0, 10).replace(/[-_]/g, ""); | ||
| let cloneDir; | ||
| const localNodeModules = resolve$1(options.cwd, "node_modules"); | ||
| const parentDir = dirname$1(options.cwd); | ||
| if (basename$1(parentDir) === ".c12") cloneDir = join$1(parentDir, cloneName); | ||
| else if (existsSync(localNodeModules)) cloneDir = join$1(localNodeModules, ".c12", cloneName); | ||
| else cloneDir = process.env.XDG_CACHE_HOME ? resolve$1(process.env.XDG_CACHE_HOME, "c12", cloneName) : resolve$1(homedir(), ".cache/c12", cloneName); | ||
| if (existsSync(cloneDir) && !sourceOptions.install) await rm(cloneDir, { recursive: true }); | ||
| source = (await downloadTemplate(source, { | ||
| dir: cloneDir, | ||
| install: sourceOptions.install, | ||
| force: sourceOptions.install, | ||
| auth: sourceOptions.auth, | ||
| ...options.giget, | ||
| ...sourceOptions.giget | ||
| })).dir; | ||
| } | ||
| if (NPM_PACKAGE_RE.test(source)) source = tryResolve(source, options) || source; | ||
| const ext = extname$1(source); | ||
| const isDir = !ext || ext === basename$1(source); | ||
| const cwd = resolve$1(options.cwd, isDir ? source : dirname$1(source)); | ||
| if (isDir) source = options.configFile; | ||
| const res = { | ||
| config: void 0, | ||
| configFile: void 0, | ||
| cwd, | ||
| source, | ||
| sourceOptions | ||
| }; | ||
| res.configFile = tryResolve(resolve$1(cwd, source), options) || tryResolve(resolve$1(cwd, ".config", source.replace(/\.config$/, "")), options) || tryResolve(resolve$1(cwd, ".config", source), options) || source; | ||
| if (!existsSync(res.configFile)) return res; | ||
| res._configFile = res.configFile; | ||
| const configFileExt = extname$1(res.configFile) || ""; | ||
| if (configFileExt in ASYNC_LOADERS) res.config = (await ASYNC_LOADERS[configFileExt]())(await readFile(res.configFile, "utf8")); | ||
| else { | ||
| const _resolveModule = options.resolveModule || ((mod) => mod.default || mod); | ||
| if (options.import) res.config = _resolveModule(await options.import(res.configFile)); | ||
| else res.config = await import(res.configFile).then(_resolveModule, async (error) => { | ||
| const { createJiti } = await import("jiti").catch(() => { | ||
| throw new Error(`Failed to load config file \`${res.configFile}\`: ${error?.message}. Hint install \`jiti\` for compatibility.`, { cause: error }); | ||
| }); | ||
| const jiti = createJiti(join$1(options.cwd || ".", options.configFile || "/"), { | ||
| interopDefault: true, | ||
| moduleCache: false, | ||
| extensions: [...SUPPORTED_EXTENSIONS] | ||
| }); | ||
| options.import = (id) => jiti.import(id); | ||
| return _resolveModule(await options.import(res.configFile)); | ||
| }); | ||
| } | ||
| if (typeof res.config === "function") res.config = await res.config(options.context); | ||
| if (options.envName) { | ||
| const envConfig = { | ||
| ...res.config["$" + options.envName], | ||
| ...res.config.$env?.[options.envName] | ||
| }; | ||
| if (Object.keys(envConfig).length > 0) res.config = _merger(envConfig, res.config); | ||
| } | ||
| res.meta = defu(res.sourceOptions.meta, res.config.$meta); | ||
| delete res.config.$meta; | ||
| if (res.sourceOptions.overrides) res.config = _merger(res.sourceOptions.overrides, res.config); | ||
| res.configFile = _normalize(res.configFile); | ||
| res.source = _normalize(res.source); | ||
| return res; | ||
| } | ||
| function tryResolve(id, options) { | ||
| const res = resolveModulePath(id, { | ||
| try: true, | ||
| from: pathToFileURL(join$1(options.cwd || ".", options.configFile || "/")), | ||
| suffixes: ["", "/index"], | ||
| extensions: SUPPORTED_EXTENSIONS, | ||
| cache: false | ||
| }); | ||
| return res ? normalize$1(res) : void 0; | ||
| } | ||
| const eventMap = { | ||
| add: "created", | ||
| change: "updated", | ||
| unlink: "removed" | ||
| }; | ||
| async function watchConfig(options) { | ||
| let config = await loadConfig(options); | ||
| const configName = options.name || "config"; | ||
| const configFileName = options.configFile ?? (options.name === "config" ? "config" : `${options.name}.config`); | ||
| const watchingFiles = [...new Set((config.layers || []).filter((l) => l.cwd).flatMap((l) => [ | ||
| ...SUPPORTED_EXTENSIONS.flatMap((ext) => [ | ||
| resolve$1(l.cwd, configFileName + ext), | ||
| resolve$1(l.cwd, ".config", configFileName + ext), | ||
| resolve$1(l.cwd, ".config", configFileName.replace(/\.config$/, "") + ext) | ||
| ]), | ||
| l.source && resolve$1(l.cwd, l.source), | ||
| options.rcFile && resolve$1(l.cwd, typeof options.rcFile === "string" ? options.rcFile : `.${configName}rc`), | ||
| options.packageJson && resolve$1(l.cwd, "package.json") | ||
| ]).filter(Boolean))]; | ||
| const watch = await import("./readdirp+chokidar.mjs").then((n) => n.t).then((r) => r.watch || r.default || r); | ||
| const { diff } = await import("./_4.mjs").then((n) => n.t); | ||
| const _fswatcher = watch(watchingFiles, { | ||
| ignoreInitial: true, | ||
| ...options.chokidarOptions | ||
| }); | ||
| const onChange = async (event, path) => { | ||
| const type = eventMap[event]; | ||
| if (!type) return; | ||
| if (options.onWatch) await options.onWatch({ | ||
| type, | ||
| path | ||
| }); | ||
| const oldConfig = config; | ||
| try { | ||
| config = await loadConfig(options); | ||
| } catch (error) { | ||
| console.warn(`Failed to load config ${path}\n${error}`); | ||
| return; | ||
| } | ||
| const changeCtx = { | ||
| newConfig: config, | ||
| oldConfig, | ||
| getDiff: () => diff(oldConfig.config, config.config) | ||
| }; | ||
| if (options.acceptHMR) { | ||
| if (await options.acceptHMR(changeCtx)) return; | ||
| } | ||
| if (options.onUpdate) await options.onUpdate(changeCtx); | ||
| }; | ||
| if (options.debounce === false) _fswatcher.on("all", onChange); | ||
| else _fswatcher.on("all", debounce(onChange, options.debounce ?? 100)); | ||
| const utils = { | ||
| watchingFiles, | ||
| unwatch: async () => { | ||
| await _fswatcher.close(); | ||
| } | ||
| }; | ||
| return new Proxy(utils, { get(_, prop) { | ||
| if (prop in utils) return utils[prop]; | ||
| return config[prop]; | ||
| } }); | ||
| } | ||
| //#endregion | ||
| //#region node_modules/.pnpm/c12@4.0.0-beta.3_chokidar@5.0.0_dotenv@17.3.1_giget@3.1.2_jiti@2.6.1_magicast@0.5.2/node_modules/c12/dist/_chunks/libs/ohash.mjs | ||
| var __defProp = Object.defineProperty; | ||
| var __exportAll = (all, no_symbols) => { | ||
| let target = {}; | ||
| for (var name in all) __defProp(target, name, { | ||
| get: all[name], | ||
| enumerable: true | ||
| }); | ||
| if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" }); | ||
| return target; | ||
| }; | ||
| function serialize(o) { | ||
| return typeof o == "string" ? `'${o}'` : new c().serialize(o); | ||
| } | ||
| const c = /* @__PURE__ */ function() { | ||
| class o { | ||
| #t = /* @__PURE__ */ new Map(); | ||
| compare(t, r) { | ||
| const e = typeof t, n = typeof r; | ||
| return e === "string" && n === "string" ? t.localeCompare(r) : e === "number" && n === "number" ? t - r : String.prototype.localeCompare.call(this.serialize(t, true), this.serialize(r, true)); | ||
| } | ||
| serialize(t, r) { | ||
| if (t === null) return "null"; | ||
| switch (typeof t) { | ||
| case "string": return r ? t : `'${t}'`; | ||
| case "bigint": return `${t}n`; | ||
| case "object": return this.$object(t); | ||
| case "function": return this.$function(t); | ||
| } | ||
| return String(t); | ||
| } | ||
| serializeObject(t) { | ||
| const r = Object.prototype.toString.call(t); | ||
| if (r !== "[object Object]") return this.serializeBuiltInType(r.length < 10 ? `unknown:${r}` : r.slice(8, -1), t); | ||
| const e = t.constructor, n = e === Object || e === void 0 ? "" : e.name; | ||
| if (n !== "" && globalThis[n] === e) return this.serializeBuiltInType(n, t); | ||
| if (typeof t.toJSON == "function") { | ||
| const i = t.toJSON(); | ||
| return n + (i !== null && typeof i == "object" ? this.$object(i) : `(${this.serialize(i)})`); | ||
| } | ||
| return this.serializeObjectEntries(n, Object.entries(t)); | ||
| } | ||
| serializeBuiltInType(t, r) { | ||
| const e = this["$" + t]; | ||
| if (e) return e.call(this, r); | ||
| if (typeof r?.entries == "function") return this.serializeObjectEntries(t, r.entries()); | ||
| throw new Error(`Cannot serialize ${t}`); | ||
| } | ||
| serializeObjectEntries(t, r) { | ||
| const e = Array.from(r).sort((i, a) => this.compare(i[0], a[0])); | ||
| let n = `${t}{`; | ||
| for (let i = 0; i < e.length; i++) { | ||
| const [a, l] = e[i]; | ||
| n += `${this.serialize(a, true)}:${this.serialize(l)}`, i < e.length - 1 && (n += ","); | ||
| } | ||
| return n + "}"; | ||
| } | ||
| $object(t) { | ||
| let r = this.#t.get(t); | ||
| return r === void 0 && (this.#t.set(t, `#${this.#t.size}`), r = this.serializeObject(t), this.#t.set(t, r)), r; | ||
| } | ||
| $function(t) { | ||
| const r = Function.prototype.toString.call(t); | ||
| return r.slice(-15) === "[native code] }" ? `${t.name || ""}()[native]` : `${t.name}(${t.length})${r.replace(/\s*\n\s*/g, "")}`; | ||
| } | ||
| $Array(t) { | ||
| let r = "["; | ||
| for (let e = 0; e < t.length; e++) r += this.serialize(t[e]), e < t.length - 1 && (r += ","); | ||
| return r + "]"; | ||
| } | ||
| $Date(t) { | ||
| try { | ||
| return `Date(${t.toISOString()})`; | ||
| } catch { | ||
| return "Date(null)"; | ||
| } | ||
| } | ||
| $ArrayBuffer(t) { | ||
| return `ArrayBuffer[${new Uint8Array(t).join(",")}]`; | ||
| } | ||
| $Set(t) { | ||
| return `Set${this.$Array(Array.from(t).sort((r, e) => this.compare(r, e)))}`; | ||
| } | ||
| $Map(t) { | ||
| return this.serializeObjectEntries("Map", t.entries()); | ||
| } | ||
| } | ||
| for (const s of [ | ||
| "Error", | ||
| "RegExp", | ||
| "URL" | ||
| ]) o.prototype["$" + s] = function(t) { | ||
| return `${s}(${t})`; | ||
| }; | ||
| for (const s of [ | ||
| "Int8Array", | ||
| "Uint8Array", | ||
| "Uint8ClampedArray", | ||
| "Int16Array", | ||
| "Uint16Array", | ||
| "Int32Array", | ||
| "Uint32Array", | ||
| "Float32Array", | ||
| "Float64Array" | ||
| ]) o.prototype["$" + s] = function(t) { | ||
| return `${s}[${t.join(",")}]`; | ||
| }; | ||
| for (const s of ["BigInt64Array", "BigUint64Array"]) o.prototype["$" + s] = function(t) { | ||
| return `${s}[${t.join("n,")}${t.length > 0 ? "n" : ""}]`; | ||
| }; | ||
| return o; | ||
| }(); | ||
| const e = globalThis.process?.getBuiltinModule?.("crypto")?.hash, r = "sha256", s = "base64url"; | ||
| function digest(t) { | ||
| if (e) return e(r, t, s); | ||
| const o = createHash(r).update(t); | ||
| return globalThis.process?.versions?.webcontainer ? o.digest().toString(s) : o.digest(s); | ||
| } | ||
| var dist_exports = /* @__PURE__ */ __exportAll({ digest: () => digest }); | ||
| var utils_exports = /* @__PURE__ */ __exportAll({ diff: () => diff }); | ||
| function diff(obj1, obj2) { | ||
| return _diff(_toHashedObject(obj1), _toHashedObject(obj2)); | ||
| } | ||
| function _diff(h1, h2) { | ||
| const diffs = []; | ||
| const allProps = /* @__PURE__ */ new Set([...Object.keys(h1.props || {}), ...Object.keys(h2.props || {})]); | ||
| if (h1.props && h2.props) for (const prop of allProps) { | ||
| const p1 = h1.props[prop]; | ||
| const p2 = h2.props[prop]; | ||
| if (p1 && p2) diffs.push(..._diff(h1.props?.[prop], h2.props?.[prop])); | ||
| else if (p1 || p2) diffs.push(new DiffEntry((p2 || p1).key, p1 ? "removed" : "added", p2, p1)); | ||
| } | ||
| if (allProps.size === 0 && h1.hash !== h2.hash) diffs.push(new DiffEntry((h2 || h1).key, "changed", h2, h1)); | ||
| return diffs; | ||
| } | ||
| function _toHashedObject(obj, key = "") { | ||
| if (obj && typeof obj !== "object") return new DiffHashedObject(key, obj, serialize(obj)); | ||
| const props = {}; | ||
| const hashes = []; | ||
| for (const _key in obj) { | ||
| props[_key] = _toHashedObject(obj[_key], key ? `${key}.${_key}` : _key); | ||
| hashes.push(props[_key].hash); | ||
| } | ||
| return new DiffHashedObject(key, obj, `{${hashes.join(":")}}`, props); | ||
| } | ||
| var DiffEntry = class { | ||
| constructor(key, type, newValue, oldValue) { | ||
| this.key = key; | ||
| this.type = type; | ||
| this.newValue = newValue; | ||
| this.oldValue = oldValue; | ||
| } | ||
| toString() { | ||
| return this.toJSON(); | ||
| } | ||
| toJSON() { | ||
| switch (this.type) { | ||
| case "added": return `Added \`${this.key}\``; | ||
| case "removed": return `Removed \`${this.key}\``; | ||
| case "changed": return `Changed \`${this.key}\` from \`${this.oldValue?.toString() || "-"}\` to \`${this.newValue.toString()}\``; | ||
| } | ||
| } | ||
| }; | ||
| var DiffHashedObject = class { | ||
| constructor(key, value, hash, props) { | ||
| this.key = key; | ||
| this.value = value; | ||
| this.hash = hash; | ||
| this.props = props; | ||
| } | ||
| toString() { | ||
| if (this.props) return `{${Object.keys(this.props).join(",")}}`; | ||
| else return JSON.stringify(this.value); | ||
| } | ||
| toJSON() { | ||
| const k = this.key || "."; | ||
| if (this.props) return `${k}({${Object.keys(this.props).join(",")}})`; | ||
| return `${k}(${this.value})`; | ||
| } | ||
| }; | ||
| //#endregion | ||
| export { loadDotenv as a, loadConfig as i, utils_exports as n, setupDotenv as o, SUPPORTED_EXTENSIONS as r, watchConfig as s, dist_exports as t }; |
| import { Stats } from "node:fs"; | ||
| import { EventEmitter } from "node:events"; | ||
| import { Readable } from "node:stream"; | ||
| import { DownloadTemplateOptions } from "giget"; | ||
| //#region node_modules/.pnpm/c12@4.0.0-beta.3_chokidar@5.0.0_dotenv@17.3.1_giget@3.1.2_jiti@2.6.1_magicast@0.5.2/node_modules/c12/dist/_chunks/libs/ohash.d.mts | ||
| //#region node_modules/.pnpm/ohash@2.0.11/node_modules/ohash/dist/utils/index.d.mts | ||
| /** | ||
| * Calculates the difference between two objects and returns a list of differences. | ||
| * | ||
| * @param {any} obj1 - The first object to compare. | ||
| * @param {any} obj2 - The second object to compare. | ||
| * @param {HashOptions} [opts={}] - Configuration options for hashing the objects. See {@link HashOptions}. | ||
| * @returns {DiffEntry[]} An array with the differences between the two objects. | ||
| */ | ||
| declare function diff(obj1: any, obj2: any): DiffEntry[]; | ||
| declare class DiffEntry { | ||
| key: string; | ||
| type: "changed" | "added" | "removed"; | ||
| newValue: DiffHashedObject; | ||
| oldValue?: DiffHashedObject | undefined; | ||
| constructor(key: string, type: "changed" | "added" | "removed", newValue: DiffHashedObject, oldValue?: DiffHashedObject | undefined); | ||
| toString(): string; | ||
| toJSON(): string; | ||
| } | ||
| declare class DiffHashedObject { | ||
| key: string; | ||
| value: any; | ||
| hash?: string | undefined; | ||
| props?: Record<string, DiffHashedObject> | undefined; | ||
| constructor(key: string, value: any, hash?: string | undefined, props?: Record<string, DiffHashedObject> | undefined); | ||
| toString(): string; | ||
| toJSON(): string; | ||
| } //#endregion | ||
| //#endregion | ||
| //#region node_modules/.pnpm/chokidar@5.0.0/node_modules/chokidar/index.d.ts | ||
| type AWF = { | ||
| stabilityThreshold: number; | ||
| pollInterval: number; | ||
| }; | ||
| type BasicOpts = { | ||
| persistent: boolean; | ||
| ignoreInitial: boolean; | ||
| followSymlinks: boolean; | ||
| cwd?: string; | ||
| usePolling: boolean; | ||
| interval: number; | ||
| binaryInterval: number; | ||
| alwaysStat?: boolean; | ||
| depth?: number; | ||
| ignorePermissionErrors: boolean; | ||
| atomic: boolean | number; | ||
| }; | ||
| type ChokidarOptions = Partial<BasicOpts & { | ||
| ignored: Matcher | Matcher[]; | ||
| awaitWriteFinish: boolean | Partial<AWF>; | ||
| }>; | ||
| type MatchFunction = (val: string, stats?: Stats) => boolean; | ||
| interface MatcherObject { | ||
| path: string; | ||
| recursive?: boolean; | ||
| } | ||
| type Matcher = string | RegExp | MatchFunction | MatcherObject; | ||
| //#endregion | ||
| //#region node_modules/.pnpm/c12@4.0.0-beta.3_chokidar@5.0.0_dotenv@17.3.1_giget@3.1.2_jiti@2.6.1_magicast@0.5.2/node_modules/c12/dist/index.d.mts | ||
| //#region src/dotenv.d.ts | ||
| interface DotenvOptions { | ||
| /** | ||
| * The project root directory (either absolute or relative to the current working directory). | ||
| * | ||
| * Defaults to `options.cwd` in `loadConfig` context, or `process.cwd()` when used as standalone. | ||
| */ | ||
| cwd?: string; | ||
| /** | ||
| * What file or files to look in for environment variables (either absolute or relative | ||
| * to the current working directory). For example, `.env`. | ||
| * With the array type, the order enforce the env loading priority (last one overrides). | ||
| */ | ||
| fileName?: string | string[]; | ||
| /** | ||
| * Whether to interpolate variables within .env. | ||
| * | ||
| * @example | ||
| * ```env | ||
| * BASE_DIR="/test" | ||
| * # resolves to "/test/further" | ||
| * ANOTHER_DIR="${BASE_DIR}/further" | ||
| * ``` | ||
| */ | ||
| interpolate?: boolean; | ||
| /** | ||
| * An object describing environment variables (key, value pairs). | ||
| */ | ||
| env?: NodeJS.ProcessEnv; | ||
| /** | ||
| * Resolve `_FILE` suffixed environment variables by reading the file at the | ||
| * specified path and assigning its trimmed content to the base key. | ||
| * | ||
| * This is useful for container secrets (e.g. Docker, Kubernetes) where | ||
| * sensitive values are mounted as files. | ||
| * | ||
| * @default false | ||
| * | ||
| * @example | ||
| * ```env | ||
| * DATABASE_PASSWORD_FILE="/run/secrets/db_password" | ||
| * # resolves to DATABASE_PASSWORD=<contents of /run/secrets/db_password> | ||
| * ``` | ||
| */ | ||
| expandFileReferences?: boolean; | ||
| } | ||
| declare global { | ||
| var __c12_dotenv_vars__: Map<Record<string, any>, Set<string>>; | ||
| } //#endregion | ||
| //#region src/types.d.ts | ||
| interface ConfigLayerMeta { | ||
| name?: string; | ||
| [key: string]: any; | ||
| } | ||
| type UserInputConfig = Record<string, any>; | ||
| interface C12InputConfig<T extends UserInputConfig = UserInputConfig, MT extends ConfigLayerMeta = ConfigLayerMeta> { | ||
| $test?: T; | ||
| $development?: T; | ||
| $production?: T; | ||
| $env?: Record<string, T>; | ||
| $meta?: MT; | ||
| } | ||
| interface SourceOptions<T extends UserInputConfig = UserInputConfig, MT extends ConfigLayerMeta = ConfigLayerMeta> { | ||
| /** Custom meta for layer */ | ||
| meta?: MT; | ||
| /** Layer config overrides */ | ||
| overrides?: T; | ||
| [key: string]: any; | ||
| /** | ||
| * Options for cloning remote sources | ||
| * | ||
| * @see https://giget.unjs.io | ||
| */ | ||
| giget?: DownloadTemplateOptions; | ||
| /** | ||
| * Install dependencies after cloning | ||
| * | ||
| * @see https://nypm.unjs.io | ||
| */ | ||
| install?: boolean; | ||
| /** | ||
| * Token for cloning private sources | ||
| * | ||
| * @see https://giget.unjs.io#providing-token-for-private-repositories | ||
| */ | ||
| auth?: string; | ||
| } | ||
| interface ConfigLayer<T extends UserInputConfig = UserInputConfig, MT extends ConfigLayerMeta = ConfigLayerMeta> { | ||
| config: T | null; | ||
| source?: string; | ||
| sourceOptions?: SourceOptions<T, MT>; | ||
| meta?: MT; | ||
| cwd?: string; | ||
| configFile?: string; | ||
| } | ||
| interface ResolvedConfig<T extends UserInputConfig = UserInputConfig, MT extends ConfigLayerMeta = ConfigLayerMeta> extends ConfigLayer<T, MT> { | ||
| config: T; | ||
| layers?: ConfigLayer<T, MT>[]; | ||
| cwd?: string; | ||
| _configFile?: string; | ||
| } | ||
| type ConfigSource = "overrides" | "main" | "rc" | "packageJson" | "defaultConfig"; | ||
| interface ConfigFunctionContext { | ||
| [key: string]: any; | ||
| } | ||
| interface ResolvableConfigContext<T extends UserInputConfig = UserInputConfig> { | ||
| configs: Record<ConfigSource, T | null | undefined>; | ||
| rawConfigs: Record<ConfigSource, ResolvableConfig<T> | null | undefined>; | ||
| } | ||
| type MaybePromise<T> = T | Promise<T>; | ||
| type ResolvableConfig<T extends UserInputConfig = UserInputConfig> = MaybePromise<T | null | undefined> | ((ctx: ResolvableConfigContext<T>) => MaybePromise<T | null | undefined>); | ||
| interface LoadConfigOptions<T extends UserInputConfig = UserInputConfig, MT extends ConfigLayerMeta = ConfigLayerMeta> { | ||
| name?: string; | ||
| cwd?: string; | ||
| configFile?: string; | ||
| rcFile?: false | string; | ||
| globalRc?: boolean; | ||
| dotenv?: boolean | DotenvOptions; | ||
| envName?: string | false; | ||
| packageJson?: boolean | string | string[]; | ||
| defaults?: T; | ||
| defaultConfig?: ResolvableConfig<T>; | ||
| overrides?: ResolvableConfig<T>; | ||
| omit$Keys?: boolean; | ||
| /** Context passed to config functions */ | ||
| context?: ConfigFunctionContext; | ||
| resolve?: (id: string, options: LoadConfigOptions<T, MT>) => null | undefined | ResolvedConfig<T, MT> | Promise<ResolvedConfig<T, MT> | undefined | null>; | ||
| /** Custom import function used to load configuration files */ | ||
| import?: (id: string) => Promise<unknown>; | ||
| /** Custom resolver for picking which export to use from the loaded module. Default: `(mod) => mod.default || mod` */ | ||
| resolveModule?: (mod: any) => any; | ||
| giget?: false | DownloadTemplateOptions; | ||
| merger?: (...sources: Array<T | null | undefined>) => T; | ||
| extend?: false | { | ||
| extendKey?: string | string[]; | ||
| }; | ||
| configFileRequired?: boolean; | ||
| } | ||
| //#endregion | ||
| //#region src/watch.d.ts | ||
| type DiffEntries = ReturnType<typeof diff>; | ||
| type ConfigWatcher<T extends UserInputConfig = UserInputConfig, MT extends ConfigLayerMeta = ConfigLayerMeta> = ResolvedConfig<T, MT> & { | ||
| watchingFiles: string[]; | ||
| unwatch: () => Promise<void>; | ||
| }; | ||
| interface WatchConfigOptions<T extends UserInputConfig = UserInputConfig, MT extends ConfigLayerMeta = ConfigLayerMeta> extends LoadConfigOptions<T, MT> { | ||
| chokidarOptions?: ChokidarOptions; | ||
| debounce?: false | number; | ||
| onWatch?: (event: { | ||
| type: "created" | "updated" | "removed"; | ||
| path: string; | ||
| }) => void | Promise<void>; | ||
| acceptHMR?: (context: { | ||
| getDiff: () => DiffEntries; | ||
| newConfig: ResolvedConfig<T, MT>; | ||
| oldConfig: ResolvedConfig<T, MT>; | ||
| }) => void | boolean | Promise<void | boolean>; | ||
| onUpdate?: (context: { | ||
| getDiff: () => ReturnType<typeof diff>; | ||
| newConfig: ResolvedConfig<T, MT>; | ||
| oldConfig: ResolvedConfig<T, MT>; | ||
| }) => void | Promise<void>; | ||
| } | ||
| //#endregion | ||
| export { WatchConfigOptions as a, ResolvedConfig as i, ConfigWatcher as n, ChokidarOptions as o, DotenvOptions as r, C12InputConfig as t }; |
| import { Plugin } from "rollup"; | ||
| //#region node_modules/.pnpm/@rollup+pluginutils@5.3.0_rollup@4.59.0/node_modules/@rollup/pluginutils/types/index.d.ts | ||
| /** | ||
| * A valid `picomatch` glob pattern, or array of patterns. | ||
| */ | ||
| type FilterPattern = ReadonlyArray<string | RegExp> | string | RegExp | null; | ||
| //#endregion | ||
| //#region node_modules/.pnpm/@rollup+plugin-commonjs@29.0.2_rollup@4.59.0/node_modules/@rollup/plugin-commonjs/types/index.d.ts | ||
| type RequireReturnsDefaultOption = boolean | 'auto' | 'preferred' | 'namespace'; | ||
| type DefaultIsModuleExportsOption = boolean | 'auto'; | ||
| interface RollupCommonJSOptions { | ||
| /** | ||
| * A picomatch pattern, or array of patterns, which specifies the files in | ||
| * the build the plugin should operate on. By default, all files with | ||
| * extension `".cjs"` or those in `extensions` are included, but you can | ||
| * narrow this list by only including specific files. These files will be | ||
| * analyzed and transpiled if either the analysis does not find ES module | ||
| * specific statements or `transformMixedEsModules` is `true`. | ||
| * @default undefined | ||
| */ | ||
| include?: FilterPattern; | ||
| /** | ||
| * A picomatch pattern, or array of patterns, which specifies the files in | ||
| * the build the plugin should _ignore_. By default, all files with | ||
| * extensions other than those in `extensions` or `".cjs"` are ignored, but you | ||
| * can exclude additional files. See also the `include` option. | ||
| * @default undefined | ||
| */ | ||
| exclude?: FilterPattern; | ||
| /** | ||
| * For extensionless imports, search for extensions other than .js in the | ||
| * order specified. Note that you need to make sure that non-JavaScript files | ||
| * are transpiled by another plugin first. | ||
| * @default [ '.js' ] | ||
| */ | ||
| extensions?: ReadonlyArray<string>; | ||
| /** | ||
| * If true then uses of `global` won't be dealt with by this plugin | ||
| * @default false | ||
| */ | ||
| ignoreGlobal?: boolean; | ||
| /** | ||
| * If false, skips source map generation for CommonJS modules. This will | ||
| * improve performance. | ||
| * @default true | ||
| */ | ||
| sourceMap?: boolean; | ||
| /** | ||
| * Some `require` calls cannot be resolved statically to be translated to | ||
| * imports. | ||
| * When this option is set to `false`, the generated code will either | ||
| * directly throw an error when such a call is encountered or, when | ||
| * `dynamicRequireTargets` is used, when such a call cannot be resolved with a | ||
| * configured dynamic require target. | ||
| * Setting this option to `true` will instead leave the `require` call in the | ||
| * code or use it as a fallback for `dynamicRequireTargets`. | ||
| * @default false | ||
| */ | ||
| ignoreDynamicRequires?: boolean; | ||
| /** | ||
| * Instructs the plugin whether to enable mixed module transformations. This | ||
| * is useful in scenarios with modules that contain a mix of ES `import` | ||
| * statements and CommonJS `require` expressions. Set to `true` if `require` | ||
| * calls should be transformed to imports in mixed modules, or `false` if the | ||
| * `require` expressions should survive the transformation. The latter can be | ||
| * important if the code contains environment detection, or you are coding | ||
| * for an environment with special treatment for `require` calls such as | ||
| * ElectronJS. See also the `ignore` option. | ||
| * @default false | ||
| */ | ||
| transformMixedEsModules?: boolean; | ||
| /** | ||
| * By default, this plugin will try to hoist `require` statements as imports | ||
| * to the top of each file. While this works well for many code bases and | ||
| * allows for very efficient ESM output, it does not perfectly capture | ||
| * CommonJS semantics as the order of side effects like log statements may | ||
| * change. But it is especially problematic when there are circular `require` | ||
| * calls between CommonJS modules as those often rely on the lazy execution of | ||
| * nested `require` calls. | ||
| * | ||
| * Setting this option to `true` will wrap all CommonJS files in functions | ||
| * which are executed when they are required for the first time, preserving | ||
| * NodeJS semantics. Note that this can have an impact on the size and | ||
| * performance of the generated code. | ||
| * | ||
| * The default value of `"auto"` will only wrap CommonJS files when they are | ||
| * part of a CommonJS dependency cycle, e.g. an index file that is required by | ||
| * many of its dependencies. All other CommonJS files are hoisted. This is the | ||
| * recommended setting for most code bases. | ||
| * | ||
| * `false` will entirely prevent wrapping and hoist all files. This may still | ||
| * work depending on the nature of cyclic dependencies but will often cause | ||
| * problems. | ||
| * | ||
| * You can also provide a picomatch pattern, or array of patterns, to only | ||
| * specify a subset of files which should be wrapped in functions for proper | ||
| * `require` semantics. | ||
| * | ||
| * `"debug"` works like `"auto"` but after bundling, it will display a warning | ||
| * containing a list of ids that have been wrapped which can be used as | ||
| * picomatch pattern for fine-tuning. | ||
| * @default "auto" | ||
| */ | ||
| strictRequires?: boolean | FilterPattern; | ||
| /** | ||
| * Sometimes you have to leave require statements unconverted. Pass an array | ||
| * containing the IDs or a `id => boolean` function. | ||
| * @default [] | ||
| */ | ||
| ignore?: ReadonlyArray<string> | ((id: string) => boolean); | ||
| /** | ||
| * In most cases, where `require` calls are inside a `try-catch` clause, | ||
| * they should be left unconverted as it requires an optional dependency | ||
| * that may or may not be installed beside the rolled up package. | ||
| * Due to the conversion of `require` to a static `import` - the call is | ||
| * hoisted to the top of the file, outside the `try-catch` clause. | ||
| * | ||
| * - `true`: Default. All `require` calls inside a `try` will be left unconverted. | ||
| * - `false`: All `require` calls inside a `try` will be converted as if the | ||
| * `try-catch` clause is not there. | ||
| * - `remove`: Remove all `require` calls from inside any `try` block. | ||
| * - `string[]`: Pass an array containing the IDs to left unconverted. | ||
| * - `((id: string) => boolean|'remove')`: Pass a function that controls | ||
| * individual IDs. | ||
| * | ||
| * @default true | ||
| */ | ||
| ignoreTryCatch?: boolean | 'remove' | ReadonlyArray<string> | ((id: string) => boolean | 'remove'); | ||
| /** | ||
| * Controls how to render imports from external dependencies. By default, | ||
| * this plugin assumes that all external dependencies are CommonJS. This | ||
| * means they are rendered as default imports to be compatible with e.g. | ||
| * NodeJS where ES modules can only import a default export from a CommonJS | ||
| * dependency. | ||
| * | ||
| * If you set `esmExternals` to `true`, this plugin assumes that all | ||
| * external dependencies are ES modules and respect the | ||
| * `requireReturnsDefault` option. If that option is not set, they will be | ||
| * rendered as namespace imports. | ||
| * | ||
| * You can also supply an array of ids to be treated as ES modules, or a | ||
| * function that will be passed each external id to determine whether it is | ||
| * an ES module. | ||
| * @default false | ||
| */ | ||
| esmExternals?: boolean | ReadonlyArray<string> | ((id: string) => boolean); | ||
| /** | ||
| * Controls what is returned when requiring an ES module from a CommonJS file. | ||
| * When using the `esmExternals` option, this will also apply to external | ||
| * modules. By default, this plugin will render those imports as namespace | ||
| * imports i.e. | ||
| * | ||
| * ```js | ||
| * // input | ||
| * const foo = require('foo'); | ||
| * | ||
| * // output | ||
| * import * as foo from 'foo'; | ||
| * ``` | ||
| * | ||
| * However, there are some situations where this may not be desired. | ||
| * For these situations, you can change Rollup's behaviour either globally or | ||
| * per module. To change it globally, set the `requireReturnsDefault` option | ||
| * to one of the following values: | ||
| * | ||
| * - `false`: This is the default, requiring an ES module returns its | ||
| * namespace. This is the only option that will also add a marker | ||
| * `__esModule: true` to the namespace to support interop patterns in | ||
| * CommonJS modules that are transpiled ES modules. | ||
| * - `"namespace"`: Like `false`, requiring an ES module returns its | ||
| * namespace, but the plugin does not add the `__esModule` marker and thus | ||
| * creates more efficient code. For external dependencies when using | ||
| * `esmExternals: true`, no additional interop code is generated. | ||
| * - `"auto"`: This is complementary to how `output.exports: "auto"` works in | ||
| * Rollup: If a module has a default export and no named exports, requiring | ||
| * that module returns the default export. In all other cases, the namespace | ||
| * is returned. For external dependencies when using `esmExternals: true`, a | ||
| * corresponding interop helper is added. | ||
| * - `"preferred"`: If a module has a default export, requiring that module | ||
| * always returns the default export, no matter whether additional named | ||
| * exports exist. This is similar to how previous versions of this plugin | ||
| * worked. Again for external dependencies when using `esmExternals: true`, | ||
| * an interop helper is added. | ||
| * - `true`: This will always try to return the default export on require | ||
| * without checking if it actually exists. This can throw at build time if | ||
| * there is no default export. This is how external dependencies are handled | ||
| * when `esmExternals` is not used. The advantage over the other options is | ||
| * that, like `false`, this does not add an interop helper for external | ||
| * dependencies, keeping the code lean. | ||
| * | ||
| * To change this for individual modules, you can supply a function for | ||
| * `requireReturnsDefault` instead. This function will then be called once for | ||
| * each required ES module or external dependency with the corresponding id | ||
| * and allows you to return different values for different modules. | ||
| * @default false | ||
| */ | ||
| requireReturnsDefault?: RequireReturnsDefaultOption | ((id: string) => RequireReturnsDefaultOption); | ||
| /** | ||
| * @default "auto" | ||
| */ | ||
| defaultIsModuleExports?: DefaultIsModuleExportsOption | ((id: string) => DefaultIsModuleExportsOption); | ||
| /** | ||
| * Some modules contain dynamic `require` calls, or require modules that | ||
| * contain circular dependencies, which are not handled well by static | ||
| * imports. Including those modules as `dynamicRequireTargets` will simulate a | ||
| * CommonJS (NodeJS-like) environment for them with support for dynamic | ||
| * dependencies. It also enables `strictRequires` for those modules. | ||
| * | ||
| * Note: In extreme cases, this feature may result in some paths being | ||
| * rendered as absolute in the final bundle. The plugin tries to avoid | ||
| * exposing paths from the local machine, but if you are `dynamicRequirePaths` | ||
| * with paths that are far away from your project's folder, that may require | ||
| * replacing strings like `"/Users/John/Desktop/foo-project/"` -> `"/"`. | ||
| */ | ||
| dynamicRequireTargets?: string | ReadonlyArray<string>; | ||
| /** | ||
| * To avoid long paths when using the `dynamicRequireTargets` option, you can use this option to specify a directory | ||
| * that is a common parent for all files that use dynamic require statements. Using a directory higher up such as `/` | ||
| * may lead to unnecessarily long paths in the generated code and may expose directory names on your machine like your | ||
| * home directory name. By default, it uses the current working directory. | ||
| */ | ||
| dynamicRequireRoot?: string; | ||
| /** | ||
| * When enabled, external Node built-ins (e.g., `node:fs`) required from wrapped CommonJS modules | ||
| * will use `createRequire(import.meta.url)` instead of being hoisted as ESM imports. This prevents | ||
| * eager loading of Node built-ins at module initialization time. | ||
| * | ||
| * Note: This option adds a dependency on `node:module` in the output, which may not be available | ||
| * in some environments like edge runtimes (Cloudflare Workers, Vercel Edge Runtime). | ||
| * | ||
| * @default false | ||
| */ | ||
| requireNodeBuiltins?: boolean; | ||
| } | ||
| /** | ||
| * Convert CommonJS modules to ES6, so they can be included in a Rollup bundle | ||
| */ | ||
| declare function commonjs(options?: RollupCommonJSOptions): Plugin; | ||
| //#endregion | ||
| export { commonjs as t }; |
| export { }; |
| export { }; |
| import { ct as join$1, dt as resolve$1, lt as normalize$1 } from "../_build/common.mjs"; | ||
| import { createRequire } from "node:module"; | ||
| import { existsSync } from "node:fs"; | ||
| import { readFile } from "node:fs/promises"; | ||
| import { delimiter, dirname, join, normalize, resolve } from "node:path"; | ||
| import { cwd } from "node:process"; | ||
| import { createRequire as createRequire$1 } from "module"; | ||
| import { PassThrough } from "node:stream"; | ||
| import { spawn } from "node:child_process"; | ||
| import c from "node:readline"; | ||
| //#region node_modules/.pnpm/tinyexec@1.0.2/node_modules/tinyexec/dist/main.js | ||
| var l = Object.create; | ||
| var u = Object.defineProperty; | ||
| var d = Object.getOwnPropertyDescriptor; | ||
| var f = Object.getOwnPropertyNames; | ||
| var p = Object.getPrototypeOf; | ||
| var m = Object.prototype.hasOwnProperty; | ||
| var h = (e, t) => () => (t || e((t = { exports: {} }).exports, t), t.exports); | ||
| var g = (e, t, n, r) => { | ||
| if (t && typeof t === "object" || typeof t === "function") for (var i = f(t), a = 0, o = i.length, s; a < o; a++) { | ||
| s = i[a]; | ||
| if (!m.call(e, s) && s !== n) u(e, s, { | ||
| get: ((e) => t[e]).bind(null, s), | ||
| enumerable: !(r = d(t, s)) || r.enumerable | ||
| }); | ||
| } | ||
| return e; | ||
| }; | ||
| var _ = (e, t, n) => (n = e != null ? l(p(e)) : {}, g(t || !e || !e.__esModule ? u(n, "default", { | ||
| value: e, | ||
| enumerable: true | ||
| }) : n, e)); | ||
| var v = /* @__PURE__ */ createRequire$1(import.meta.url); | ||
| const y = /^path$/i; | ||
| const b = { | ||
| key: "PATH", | ||
| value: "" | ||
| }; | ||
| function x(e) { | ||
| for (const t in e) { | ||
| if (!Object.prototype.hasOwnProperty.call(e, t) || !y.test(t)) continue; | ||
| const n = e[t]; | ||
| if (!n) return b; | ||
| return { | ||
| key: t, | ||
| value: n | ||
| }; | ||
| } | ||
| return b; | ||
| } | ||
| function S(e, t) { | ||
| const i = t.value.split(delimiter); | ||
| let o = e; | ||
| let s; | ||
| do { | ||
| i.push(resolve(o, "node_modules", ".bin")); | ||
| s = o; | ||
| o = dirname(o); | ||
| } while (o !== s); | ||
| return { | ||
| key: t.key, | ||
| value: i.join(delimiter) | ||
| }; | ||
| } | ||
| function C(e, t) { | ||
| const n = { | ||
| ...process.env, | ||
| ...t | ||
| }; | ||
| const r = S(e, x(n)); | ||
| n[r.key] = r.value; | ||
| return n; | ||
| } | ||
| const w = (e) => { | ||
| let t = e.length; | ||
| const n = new PassThrough(); | ||
| const r = () => { | ||
| if (--t === 0) n.emit("end"); | ||
| }; | ||
| for (const t of e) { | ||
| t.pipe(n, { end: false }); | ||
| t.on("end", r); | ||
| } | ||
| return n; | ||
| }; | ||
| var T = h((exports, t) => { | ||
| t.exports = a; | ||
| a.sync = o; | ||
| var n = v("fs"); | ||
| function r(e, t) { | ||
| var n = t.pathExt !== void 0 ? t.pathExt : process.env.PATHEXT; | ||
| if (!n) return true; | ||
| n = n.split(";"); | ||
| if (n.indexOf("") !== -1) return true; | ||
| for (var r = 0; r < n.length; r++) { | ||
| var i = n[r].toLowerCase(); | ||
| if (i && e.substr(-i.length).toLowerCase() === i) return true; | ||
| } | ||
| return false; | ||
| } | ||
| function i(e, t, n) { | ||
| if (!e.isSymbolicLink() && !e.isFile()) return false; | ||
| return r(t, n); | ||
| } | ||
| function a(e, t, r) { | ||
| n.stat(e, function(n, a) { | ||
| r(n, n ? false : i(a, e, t)); | ||
| }); | ||
| } | ||
| function o(e, t) { | ||
| return i(n.statSync(e), e, t); | ||
| } | ||
| }); | ||
| var E = h((exports, t) => { | ||
| t.exports = r; | ||
| r.sync = i; | ||
| var n = v("fs"); | ||
| function r(e, t, r) { | ||
| n.stat(e, function(e, n) { | ||
| r(e, e ? false : a(n, t)); | ||
| }); | ||
| } | ||
| function i(e, t) { | ||
| return a(n.statSync(e), t); | ||
| } | ||
| function a(e, t) { | ||
| return e.isFile() && o(e, t); | ||
| } | ||
| function o(e, t) { | ||
| var n = e.mode; | ||
| var r = e.uid; | ||
| var i = e.gid; | ||
| var a = t.uid !== void 0 ? t.uid : process.getuid && process.getuid(); | ||
| var o = t.gid !== void 0 ? t.gid : process.getgid && process.getgid(); | ||
| var s = parseInt("100", 8); | ||
| var c = parseInt("010", 8); | ||
| var l = parseInt("001", 8); | ||
| var u = s | c; | ||
| return n & l || n & c && i === o || n & s && r === a || n & u && a === 0; | ||
| } | ||
| }); | ||
| var D = h((exports, t) => { | ||
| v("fs"); | ||
| var r; | ||
| if (process.platform === "win32" || global.TESTING_WINDOWS) r = T(); | ||
| else r = E(); | ||
| t.exports = i; | ||
| i.sync = a; | ||
| function i(e, t, n) { | ||
| if (typeof t === "function") { | ||
| n = t; | ||
| t = {}; | ||
| } | ||
| if (!n) { | ||
| if (typeof Promise !== "function") throw new TypeError("callback not provided"); | ||
| return new Promise(function(n, r) { | ||
| i(e, t || {}, function(e, t) { | ||
| if (e) r(e); | ||
| else n(t); | ||
| }); | ||
| }); | ||
| } | ||
| r(e, t || {}, function(e, r) { | ||
| if (e) { | ||
| if (e.code === "EACCES" || t && t.ignoreErrors) { | ||
| e = null; | ||
| r = false; | ||
| } | ||
| } | ||
| n(e, r); | ||
| }); | ||
| } | ||
| function a(e, t) { | ||
| try { | ||
| return r.sync(e, t || {}); | ||
| } catch (e) { | ||
| if (t && t.ignoreErrors || e.code === "EACCES") return false; | ||
| else throw e; | ||
| } | ||
| } | ||
| }); | ||
| var O = h((exports, t) => { | ||
| const n = process.platform === "win32" || process.env.OSTYPE === "cygwin" || process.env.OSTYPE === "msys"; | ||
| const r = v("path"); | ||
| const i = n ? ";" : ":"; | ||
| const a = D(); | ||
| const o = (e) => Object.assign(/* @__PURE__ */ new Error(`not found: ${e}`), { code: "ENOENT" }); | ||
| const s = (e, t) => { | ||
| const r = t.colon || i; | ||
| const a = e.match(/\//) || n && e.match(/\\/) ? [""] : [...n ? [process.cwd()] : [], ...(t.path || process.env.PATH || "").split(r)]; | ||
| const o = n ? t.pathExt || process.env.PATHEXT || ".EXE;.CMD;.BAT;.COM" : ""; | ||
| const s = n ? o.split(r) : [""]; | ||
| if (n) { | ||
| if (e.indexOf(".") !== -1 && s[0] !== "") s.unshift(""); | ||
| } | ||
| return { | ||
| pathEnv: a, | ||
| pathExt: s, | ||
| pathExtExe: o | ||
| }; | ||
| }; | ||
| const c = (e, t, n) => { | ||
| if (typeof t === "function") { | ||
| n = t; | ||
| t = {}; | ||
| } | ||
| if (!t) t = {}; | ||
| const { pathEnv: i, pathExt: c, pathExtExe: l } = s(e, t); | ||
| const u = []; | ||
| const d = (n) => new Promise((a, s) => { | ||
| if (n === i.length) return t.all && u.length ? a(u) : s(o(e)); | ||
| const c = i[n]; | ||
| const l = /^".*"$/.test(c) ? c.slice(1, -1) : c; | ||
| const d = r.join(l, e); | ||
| a(f(!l && /^\.[\\\/]/.test(e) ? e.slice(0, 2) + d : d, n, 0)); | ||
| }); | ||
| const f = (e, n, r) => new Promise((i, o) => { | ||
| if (r === c.length) return i(d(n + 1)); | ||
| const s = c[r]; | ||
| a(e + s, { pathExt: l }, (a, o) => { | ||
| if (!a && o) if (t.all) u.push(e + s); | ||
| else return i(e + s); | ||
| return i(f(e, n, r + 1)); | ||
| }); | ||
| }); | ||
| return n ? d(0).then((e) => n(null, e), n) : d(0); | ||
| }; | ||
| const l = (e, t) => { | ||
| t = t || {}; | ||
| const { pathEnv: n, pathExt: i, pathExtExe: c } = s(e, t); | ||
| const l = []; | ||
| for (let o = 0; o < n.length; o++) { | ||
| const s = n[o]; | ||
| const u = /^".*"$/.test(s) ? s.slice(1, -1) : s; | ||
| const d = r.join(u, e); | ||
| const f = !u && /^\.[\\\/]/.test(e) ? e.slice(0, 2) + d : d; | ||
| for (let e = 0; e < i.length; e++) { | ||
| const n = f + i[e]; | ||
| try { | ||
| if (a.sync(n, { pathExt: c })) if (t.all) l.push(n); | ||
| else return n; | ||
| } catch (e) {} | ||
| } | ||
| } | ||
| if (t.all && l.length) return l; | ||
| if (t.nothrow) return null; | ||
| throw o(e); | ||
| }; | ||
| t.exports = c; | ||
| c.sync = l; | ||
| }); | ||
| var k = h((exports, t) => { | ||
| const n = (e = {}) => { | ||
| const t = e.env || process.env; | ||
| if ((e.platform || process.platform) !== "win32") return "PATH"; | ||
| return Object.keys(t).reverse().find((e) => e.toUpperCase() === "PATH") || "Path"; | ||
| }; | ||
| t.exports = n; | ||
| t.exports.default = n; | ||
| }); | ||
| var A = h((exports, t) => { | ||
| const n = v("path"); | ||
| const r = O(); | ||
| const i = k(); | ||
| function a(e, t) { | ||
| const a = e.options.env || process.env; | ||
| const o = process.cwd(); | ||
| const s = e.options.cwd != null; | ||
| const c = s && process.chdir !== void 0 && !process.chdir.disabled; | ||
| if (c) try { | ||
| process.chdir(e.options.cwd); | ||
| } catch (e) {} | ||
| let l; | ||
| try { | ||
| l = r.sync(e.command, { | ||
| path: a[i({ env: a })], | ||
| pathExt: t ? n.delimiter : void 0 | ||
| }); | ||
| } catch (e) {} finally { | ||
| if (c) process.chdir(o); | ||
| } | ||
| if (l) l = n.resolve(s ? e.options.cwd : "", l); | ||
| return l; | ||
| } | ||
| function o(e) { | ||
| return a(e) || a(e, true); | ||
| } | ||
| t.exports = o; | ||
| }); | ||
| var j = h((exports, t) => { | ||
| const n = /([()\][%!^"`<>&|;, *?])/g; | ||
| function r(e) { | ||
| e = e.replace(n, "^$1"); | ||
| return e; | ||
| } | ||
| function i(e, t) { | ||
| e = `${e}`; | ||
| e = e.replace(/(\\*)"/g, "$1$1\\\""); | ||
| e = e.replace(/(\\*)$/, "$1$1"); | ||
| e = `"${e}"`; | ||
| e = e.replace(n, "^$1"); | ||
| if (t) e = e.replace(n, "^$1"); | ||
| return e; | ||
| } | ||
| t.exports.command = r; | ||
| t.exports.argument = i; | ||
| }); | ||
| var M = h((exports, t) => { | ||
| t.exports = /^#!(.*)/; | ||
| }); | ||
| var N = h((exports, t) => { | ||
| const n = M(); | ||
| t.exports = (e = "") => { | ||
| const t = e.match(n); | ||
| if (!t) return null; | ||
| const [r, i] = t[0].replace(/#! ?/, "").split(" "); | ||
| const a = r.split("/").pop(); | ||
| if (a === "env") return i; | ||
| return i ? `${a} ${i}` : a; | ||
| }; | ||
| }); | ||
| var P = h((exports, t) => { | ||
| const n = v("fs"); | ||
| const r = N(); | ||
| function i(e) { | ||
| const t = 150; | ||
| const i = Buffer.alloc(t); | ||
| let a; | ||
| try { | ||
| a = n.openSync(e, "r"); | ||
| n.readSync(a, i, 0, t, 0); | ||
| n.closeSync(a); | ||
| } catch (e) {} | ||
| return r(i.toString()); | ||
| } | ||
| t.exports = i; | ||
| }); | ||
| var F = h((exports, t) => { | ||
| const n = v("path"); | ||
| const r = A(); | ||
| const i = j(); | ||
| const a = P(); | ||
| const o = process.platform === "win32"; | ||
| const s = /\.(?:com|exe)$/i; | ||
| const c = /node_modules[\\/].bin[\\/][^\\/]+\.cmd$/i; | ||
| function l(e) { | ||
| e.file = r(e); | ||
| const t = e.file && a(e.file); | ||
| if (t) { | ||
| e.args.unshift(e.file); | ||
| e.command = t; | ||
| return r(e); | ||
| } | ||
| return e.file; | ||
| } | ||
| function u(e) { | ||
| if (!o) return e; | ||
| const t = l(e); | ||
| const r = !s.test(t); | ||
| if (e.options.forceShell || r) { | ||
| const r = c.test(t); | ||
| e.command = n.normalize(e.command); | ||
| e.command = i.command(e.command); | ||
| e.args = e.args.map((e) => i.argument(e, r)); | ||
| e.args = [ | ||
| "/d", | ||
| "/s", | ||
| "/c", | ||
| `"${[e.command].concat(e.args).join(" ")}"` | ||
| ]; | ||
| e.command = process.env.comspec || "cmd.exe"; | ||
| e.options.windowsVerbatimArguments = true; | ||
| } | ||
| return e; | ||
| } | ||
| function d(e, t, n) { | ||
| if (t && !Array.isArray(t)) { | ||
| n = t; | ||
| t = null; | ||
| } | ||
| t = t ? t.slice(0) : []; | ||
| n = Object.assign({}, n); | ||
| const r = { | ||
| command: e, | ||
| args: t, | ||
| options: n, | ||
| file: void 0, | ||
| original: { | ||
| command: e, | ||
| args: t | ||
| } | ||
| }; | ||
| return n.shell ? r : u(r); | ||
| } | ||
| t.exports = d; | ||
| }); | ||
| var I = h((exports, t) => { | ||
| const n = process.platform === "win32"; | ||
| function r(e, t) { | ||
| return Object.assign(/* @__PURE__ */ new Error(`${t} ${e.command} ENOENT`), { | ||
| code: "ENOENT", | ||
| errno: "ENOENT", | ||
| syscall: `${t} ${e.command}`, | ||
| path: e.command, | ||
| spawnargs: e.args | ||
| }); | ||
| } | ||
| function i(e, t) { | ||
| if (!n) return; | ||
| const r = e.emit; | ||
| e.emit = function(n, i) { | ||
| if (n === "exit") { | ||
| const n = a(i, t, "spawn"); | ||
| if (n) return r.call(e, "error", n); | ||
| } | ||
| return r.apply(e, arguments); | ||
| }; | ||
| } | ||
| function a(e, t) { | ||
| if (n && e === 1 && !t.file) return r(t.original, "spawn"); | ||
| return null; | ||
| } | ||
| function o(e, t) { | ||
| if (n && e === 1 && !t.file) return r(t.original, "spawnSync"); | ||
| return null; | ||
| } | ||
| t.exports = { | ||
| hookChildProcess: i, | ||
| verifyENOENT: a, | ||
| verifyENOENTSync: o, | ||
| notFoundError: r | ||
| }; | ||
| }); | ||
| var R = _(h((exports, t) => { | ||
| const n = v("child_process"); | ||
| const r = F(); | ||
| const i = I(); | ||
| function a(e, t, a) { | ||
| const o = r(e, t, a); | ||
| const s = n.spawn(o.command, o.args, o.options); | ||
| i.hookChildProcess(s, o); | ||
| return s; | ||
| } | ||
| function o(e, t, a) { | ||
| const o = r(e, t, a); | ||
| const s = n.spawnSync(o.command, o.args, o.options); | ||
| s.error = s.error || i.verifyENOENTSync(s.status, o); | ||
| return s; | ||
| } | ||
| t.exports = a; | ||
| t.exports.spawn = a; | ||
| t.exports.sync = o; | ||
| t.exports._parse = r; | ||
| t.exports._enoent = i; | ||
| })(), 1); | ||
| var z = class extends Error { | ||
| result; | ||
| output; | ||
| get exitCode() { | ||
| if (this.result.exitCode !== null) return this.result.exitCode; | ||
| } | ||
| constructor(e, t) { | ||
| super(`Process exited with non-zero status (${e.exitCode})`); | ||
| this.result = e; | ||
| this.output = t; | ||
| } | ||
| }; | ||
| const B = { | ||
| timeout: void 0, | ||
| persist: false | ||
| }; | ||
| const V = { windowsHide: true }; | ||
| function H(e, t) { | ||
| return { | ||
| command: normalize(e), | ||
| args: t ?? [] | ||
| }; | ||
| } | ||
| function U(e) { | ||
| const t = new AbortController(); | ||
| for (const n of e) { | ||
| if (n.aborted) { | ||
| t.abort(); | ||
| return n; | ||
| } | ||
| const e = () => { | ||
| t.abort(n.reason); | ||
| }; | ||
| n.addEventListener("abort", e, { signal: t.signal }); | ||
| } | ||
| return t.signal; | ||
| } | ||
| async function W(e) { | ||
| let t = ""; | ||
| for await (const n of e) t += n.toString(); | ||
| return t; | ||
| } | ||
| var G = class { | ||
| _process; | ||
| _aborted = false; | ||
| _options; | ||
| _command; | ||
| _args; | ||
| _resolveClose; | ||
| _processClosed; | ||
| _thrownError; | ||
| get process() { | ||
| return this._process; | ||
| } | ||
| get pid() { | ||
| return this._process?.pid; | ||
| } | ||
| get exitCode() { | ||
| if (this._process && this._process.exitCode !== null) return this._process.exitCode; | ||
| } | ||
| constructor(e, t, n) { | ||
| this._options = { | ||
| ...B, | ||
| ...n | ||
| }; | ||
| this._command = e; | ||
| this._args = t ?? []; | ||
| this._processClosed = new Promise((e) => { | ||
| this._resolveClose = e; | ||
| }); | ||
| } | ||
| kill(e) { | ||
| return this._process?.kill(e) === true; | ||
| } | ||
| get aborted() { | ||
| return this._aborted; | ||
| } | ||
| get killed() { | ||
| return this._process?.killed === true; | ||
| } | ||
| pipe(e, t, n) { | ||
| return q(e, t, { | ||
| ...n, | ||
| stdin: this | ||
| }); | ||
| } | ||
| async *[Symbol.asyncIterator]() { | ||
| const e = this._process; | ||
| if (!e) return; | ||
| const t = []; | ||
| if (this._streamErr) t.push(this._streamErr); | ||
| if (this._streamOut) t.push(this._streamOut); | ||
| const n = w(t); | ||
| const r = c.createInterface({ input: n }); | ||
| for await (const e of r) yield e.toString(); | ||
| await this._processClosed; | ||
| e.removeAllListeners(); | ||
| if (this._thrownError) throw this._thrownError; | ||
| if (this._options?.throwOnError && this.exitCode !== 0 && this.exitCode !== void 0) throw new z(this); | ||
| } | ||
| async _waitForOutput() { | ||
| const e = this._process; | ||
| if (!e) throw new Error("No process was started"); | ||
| const [t, n] = await Promise.all([this._streamOut ? W(this._streamOut) : "", this._streamErr ? W(this._streamErr) : ""]); | ||
| await this._processClosed; | ||
| if (this._options?.stdin) await this._options.stdin; | ||
| e.removeAllListeners(); | ||
| if (this._thrownError) throw this._thrownError; | ||
| const r = { | ||
| stderr: n, | ||
| stdout: t, | ||
| exitCode: this.exitCode | ||
| }; | ||
| if (this._options.throwOnError && this.exitCode !== 0 && this.exitCode !== void 0) throw new z(this, r); | ||
| return r; | ||
| } | ||
| then(e, t) { | ||
| return this._waitForOutput().then(e, t); | ||
| } | ||
| _streamOut; | ||
| _streamErr; | ||
| spawn() { | ||
| const e = cwd(); | ||
| const n = this._options; | ||
| const r = { | ||
| ...V, | ||
| ...n.nodeOptions | ||
| }; | ||
| const i = []; | ||
| this._resetState(); | ||
| if (n.timeout !== void 0) i.push(AbortSignal.timeout(n.timeout)); | ||
| if (n.signal !== void 0) i.push(n.signal); | ||
| if (n.persist === true) r.detached = true; | ||
| if (i.length > 0) r.signal = U(i); | ||
| r.env = C(e, r.env); | ||
| const { command: a, args: s } = H(this._command, this._args); | ||
| const c = (0, R._parse)(a, s, r); | ||
| const l = spawn(c.command, c.args, c.options); | ||
| if (l.stderr) this._streamErr = l.stderr; | ||
| if (l.stdout) this._streamOut = l.stdout; | ||
| this._process = l; | ||
| l.once("error", this._onError); | ||
| l.once("close", this._onClose); | ||
| if (n.stdin !== void 0 && l.stdin && n.stdin.process) { | ||
| const { stdout: e } = n.stdin.process; | ||
| if (e) e.pipe(l.stdin); | ||
| } | ||
| } | ||
| _resetState() { | ||
| this._aborted = false; | ||
| this._processClosed = new Promise((e) => { | ||
| this._resolveClose = e; | ||
| }); | ||
| this._thrownError = void 0; | ||
| } | ||
| _onError = (e) => { | ||
| if (e.name === "AbortError" && (!(e.cause instanceof Error) || e.cause.name !== "TimeoutError")) { | ||
| this._aborted = true; | ||
| return; | ||
| } | ||
| this._thrownError = e; | ||
| }; | ||
| _onClose = () => { | ||
| if (this._resolveClose) this._resolveClose(); | ||
| }; | ||
| }; | ||
| const K = (e, t, n) => { | ||
| const r = new G(e, t, n); | ||
| r.spawn(); | ||
| return r; | ||
| }; | ||
| const q = K; | ||
| //#endregion | ||
| //#region node_modules/.pnpm/nypm@0.6.5/node_modules/nypm/dist/index.mjs | ||
| async function findup(cwd, match, options = {}) { | ||
| const segments = normalize$1(cwd).split("/"); | ||
| while (segments.length > 0) { | ||
| const result = await match(segments.join("/") || "/"); | ||
| if (result || !options.includeParentDirs) return result; | ||
| segments.pop(); | ||
| } | ||
| } | ||
| async function readPackageJSON(cwd) { | ||
| return findup(cwd, (p) => { | ||
| const pkgPath = join(p, "package.json"); | ||
| if (existsSync(pkgPath)) return readFile(pkgPath, "utf8").then((data) => JSON.parse(data)); | ||
| }); | ||
| } | ||
| function cached(fn) { | ||
| let v; | ||
| return () => { | ||
| if (v === void 0) v = fn().then((r) => { | ||
| v = r; | ||
| return v; | ||
| }); | ||
| return v; | ||
| }; | ||
| } | ||
| const hasCorepack = cached(async () => { | ||
| if (globalThis.process?.versions?.webcontainer) return false; | ||
| try { | ||
| const { exitCode } = await K("corepack", ["--version"]); | ||
| return exitCode === 0; | ||
| } catch { | ||
| return false; | ||
| } | ||
| }); | ||
| async function executeCommand(command, args, options = {}) { | ||
| const xArgs = command !== "npm" && command !== "bun" && command !== "deno" && options.corepack !== false && await hasCorepack() ? ["corepack", [command, ...args]] : [command, args]; | ||
| const { exitCode, stdout, stderr } = await K(xArgs[0], xArgs[1], { nodeOptions: { | ||
| cwd: resolve$1(options.cwd || process.cwd()), | ||
| env: options.env, | ||
| stdio: options.silent ? "pipe" : "inherit" | ||
| } }); | ||
| if (exitCode !== 0) throw new Error(`\`${xArgs.flat().join(" ")}\` failed.${options.silent ? [ | ||
| "", | ||
| stdout, | ||
| stderr | ||
| ].join("\n") : ""}`); | ||
| } | ||
| const NO_PACKAGE_MANAGER_DETECTED_ERROR_MSG = "No package manager auto-detected."; | ||
| async function resolveOperationOptions(options = {}) { | ||
| const cwd = options.cwd || process.cwd(); | ||
| const env = { | ||
| ...process.env, | ||
| ...options.env | ||
| }; | ||
| const packageManager = (typeof options.packageManager === "string" ? packageManagers.find((pm) => pm.name === options.packageManager) : options.packageManager) || await detectPackageManager(options.cwd || process.cwd()); | ||
| if (!packageManager) throw new Error(NO_PACKAGE_MANAGER_DETECTED_ERROR_MSG); | ||
| return { | ||
| cwd, | ||
| env, | ||
| silent: options.silent ?? false, | ||
| packageManager, | ||
| dev: options.dev ?? false, | ||
| workspace: options.workspace, | ||
| global: options.global ?? false, | ||
| dry: options.dry ?? false, | ||
| corepack: options.corepack ?? true | ||
| }; | ||
| } | ||
| function getWorkspaceArgs(options) { | ||
| if (!options.workspace) return []; | ||
| const workspacePkg = typeof options.workspace === "string" && options.workspace !== "" ? options.workspace : void 0; | ||
| if (options.packageManager.name === "pnpm") return workspacePkg ? ["--filter", workspacePkg] : ["--workspace-root"]; | ||
| if (options.packageManager.name === "npm") return workspacePkg ? ["-w", workspacePkg] : ["--workspaces"]; | ||
| if (options.packageManager.name === "yarn") if (!options.packageManager.majorVersion || options.packageManager.majorVersion === "1") return workspacePkg ? ["--cwd", workspacePkg] : ["-W"]; | ||
| else return workspacePkg ? ["workspace", workspacePkg] : []; | ||
| return []; | ||
| } | ||
| function parsePackageManagerField(packageManager) { | ||
| const [name, _version] = (packageManager || "").split("@"); | ||
| const [version, buildMeta] = _version?.split("+") || []; | ||
| if (name && name !== "-" && /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(name)) return { | ||
| name, | ||
| version, | ||
| buildMeta | ||
| }; | ||
| const sanitized = (name || "").replace(/\W+/g, ""); | ||
| return { | ||
| name: sanitized, | ||
| version, | ||
| buildMeta, | ||
| warnings: [`Abnormal characters found in \`packageManager\` field, sanitizing from \`${name}\` to \`${sanitized}\``] | ||
| }; | ||
| } | ||
| const packageManagers = [ | ||
| { | ||
| name: "npm", | ||
| command: "npm", | ||
| lockFile: "package-lock.json" | ||
| }, | ||
| { | ||
| name: "pnpm", | ||
| command: "pnpm", | ||
| lockFile: "pnpm-lock.yaml", | ||
| files: ["pnpm-workspace.yaml"] | ||
| }, | ||
| { | ||
| name: "bun", | ||
| command: "bun", | ||
| lockFile: ["bun.lockb", "bun.lock"] | ||
| }, | ||
| { | ||
| name: "yarn", | ||
| command: "yarn", | ||
| lockFile: "yarn.lock", | ||
| files: [".yarnrc.yml"] | ||
| }, | ||
| { | ||
| name: "deno", | ||
| command: "deno", | ||
| lockFile: "deno.lock", | ||
| files: ["deno.json"] | ||
| } | ||
| ]; | ||
| async function detectPackageManager(cwd, options = {}) { | ||
| const detected = await findup(resolve$1(cwd || "."), async (path) => { | ||
| if (!options.ignorePackageJSON) { | ||
| const packageJSONPath = join$1(path, "package.json"); | ||
| if (existsSync(packageJSONPath)) { | ||
| const packageJSON = JSON.parse(await readFile(packageJSONPath, "utf8")); | ||
| if (packageJSON?.packageManager) { | ||
| const { name, version = "0.0.0", buildMeta, warnings } = parsePackageManagerField(packageJSON.packageManager); | ||
| if (name) { | ||
| const majorVersion = version.split(".")[0]; | ||
| const packageManager = packageManagers.find((pm) => pm.name === name && pm.majorVersion === majorVersion) || packageManagers.find((pm) => pm.name === name); | ||
| return { | ||
| name, | ||
| command: name, | ||
| version, | ||
| majorVersion, | ||
| buildMeta, | ||
| warnings, | ||
| files: packageManager?.files, | ||
| lockFile: packageManager?.lockFile | ||
| }; | ||
| } | ||
| } | ||
| } | ||
| if (existsSync(join$1(path, "deno.json"))) return packageManagers.find((pm) => pm.name === "deno"); | ||
| } | ||
| if (!options.ignoreLockFile) { | ||
| for (const packageManager of packageManagers) if ([packageManager.lockFile, packageManager.files].flat().filter(Boolean).some((file) => existsSync(resolve$1(path, file)))) return { ...packageManager }; | ||
| } | ||
| }, { includeParentDirs: options.includeParentDirs ?? true }); | ||
| if (!detected && !options.ignoreArgv) { | ||
| const scriptArg = process.argv[1]; | ||
| if (scriptArg) { | ||
| for (const packageManager of packageManagers) if (new RegExp(`[/\\\\]\\.?${packageManager.command}`).test(scriptArg)) return packageManager; | ||
| } | ||
| } | ||
| return detected; | ||
| } | ||
| async function addDependency(name, options = {}) { | ||
| const resolvedOptions = await resolveOperationOptions(options); | ||
| const names = Array.isArray(name) ? name : [name]; | ||
| if (resolvedOptions.packageManager.name === "deno") { | ||
| for (let i = 0; i < names.length; i++) if (!/^(npm|jsr|file):.+$/.test(names[i] || "")) names[i] = `npm:${names[i]}`; | ||
| } | ||
| if (names.length === 0) return {}; | ||
| const args = (resolvedOptions.packageManager.name === "yarn" ? [ | ||
| ...getWorkspaceArgs(resolvedOptions), | ||
| resolvedOptions.global && resolvedOptions.packageManager.majorVersion === "1" ? "global" : "", | ||
| "add", | ||
| resolvedOptions.dev ? "-D" : "", | ||
| ...names | ||
| ] : [ | ||
| resolvedOptions.packageManager.name === "npm" ? "install" : "add", | ||
| ...getWorkspaceArgs(resolvedOptions), | ||
| resolvedOptions.dev ? "-D" : "", | ||
| resolvedOptions.global ? "-g" : "", | ||
| ...names | ||
| ]).filter(Boolean); | ||
| if (!resolvedOptions.dry) await executeCommand(resolvedOptions.packageManager.command, args, { | ||
| cwd: resolvedOptions.cwd, | ||
| silent: resolvedOptions.silent, | ||
| corepack: resolvedOptions.corepack | ||
| }); | ||
| if (!resolvedOptions.dry && options.installPeerDependencies) { | ||
| const existingPkg = await readPackageJSON(resolvedOptions.cwd); | ||
| const peerDeps = []; | ||
| const peerDevDeps = []; | ||
| const _require = createRequire(join(resolvedOptions.cwd, "/_.js")); | ||
| for (const _name of names) { | ||
| const pkgName = _name.match(/^(.[^@]+)/)?.[0]; | ||
| const pkg = await readPackageJSON(_require.resolve(pkgName)); | ||
| if (!pkg?.peerDependencies || pkg?.name !== pkgName) continue; | ||
| for (const [peerDependency, version] of Object.entries(pkg.peerDependencies)) { | ||
| if (pkg.peerDependenciesMeta?.[peerDependency]?.optional) continue; | ||
| if (existingPkg?.dependencies?.[peerDependency] || existingPkg?.devDependencies?.[peerDependency]) continue; | ||
| (pkg.peerDependenciesMeta?.[peerDependency]?.dev ? peerDevDeps : peerDeps).push(`${peerDependency}@${version}`); | ||
| } | ||
| } | ||
| if (peerDeps.length > 0) await addDependency(peerDeps, { ...resolvedOptions }); | ||
| if (peerDevDeps.length > 0) await addDevDependency(peerDevDeps, { ...resolvedOptions }); | ||
| } | ||
| return { exec: { | ||
| command: resolvedOptions.packageManager.command, | ||
| args | ||
| } }; | ||
| } | ||
| async function addDevDependency(name, options = {}) { | ||
| return await addDependency(name, { | ||
| ...options, | ||
| dev: true | ||
| }); | ||
| } | ||
| //#endregion | ||
| export { packageManagers as i, addDevDependency as n, detectPackageManager as r, addDependency as t }; |
| //#region node_modules/.pnpm/perfect-debounce@2.1.0/node_modules/perfect-debounce/dist/index.mjs | ||
| const DEBOUNCE_DEFAULTS = { trailing: true }; | ||
| /** | ||
| Debounce functions | ||
| @param fn - Promise-returning/async function to debounce. | ||
| @param wait - Milliseconds to wait before calling `fn`. Default value is 25ms | ||
| @returns A function that delays calling `fn` until after `wait` milliseconds have elapsed since the last time it was called. | ||
| @example | ||
| ``` | ||
| import { debounce } from 'perfect-debounce'; | ||
| const expensiveCall = async input => input; | ||
| const debouncedFn = debounce(expensiveCall, 200); | ||
| for (const number of [1, 2, 3]) { | ||
| console.log(await debouncedFn(number)); | ||
| } | ||
| //=> 1 | ||
| //=> 2 | ||
| //=> 3 | ||
| ``` | ||
| */ | ||
| function debounce(fn, wait = 25, options = {}) { | ||
| options = { | ||
| ...DEBOUNCE_DEFAULTS, | ||
| ...options | ||
| }; | ||
| if (!Number.isFinite(wait)) throw new TypeError("Expected `wait` to be a finite number"); | ||
| let leadingValue; | ||
| let timeout; | ||
| let resolveList = []; | ||
| let currentPromise; | ||
| let trailingArgs; | ||
| const applyFn = (_this, args) => { | ||
| currentPromise = _applyPromised(fn, _this, args); | ||
| currentPromise.finally(() => { | ||
| currentPromise = null; | ||
| if (options.trailing && trailingArgs && !timeout) { | ||
| const promise = applyFn(_this, trailingArgs); | ||
| trailingArgs = null; | ||
| return promise; | ||
| } | ||
| }); | ||
| return currentPromise; | ||
| }; | ||
| const debounced = function(...args) { | ||
| if (options.trailing) trailingArgs = args; | ||
| if (currentPromise) return currentPromise; | ||
| return new Promise((resolve) => { | ||
| const shouldCallNow = !timeout && options.leading; | ||
| clearTimeout(timeout); | ||
| timeout = setTimeout(() => { | ||
| timeout = null; | ||
| const promise = options.leading ? leadingValue : applyFn(this, args); | ||
| trailingArgs = null; | ||
| for (const _resolve of resolveList) _resolve(promise); | ||
| resolveList = []; | ||
| }, wait); | ||
| if (shouldCallNow) { | ||
| leadingValue = applyFn(this, args); | ||
| resolve(leadingValue); | ||
| } else resolveList.push(resolve); | ||
| }); | ||
| }; | ||
| const _clearTimeout = (timer) => { | ||
| if (timer) { | ||
| clearTimeout(timer); | ||
| timeout = null; | ||
| } | ||
| }; | ||
| debounced.isPending = () => !!timeout; | ||
| debounced.cancel = () => { | ||
| _clearTimeout(timeout); | ||
| resolveList = []; | ||
| trailingArgs = null; | ||
| }; | ||
| debounced.flush = () => { | ||
| _clearTimeout(timeout); | ||
| if (!trailingArgs || currentPromise) return; | ||
| const args = trailingArgs; | ||
| trailingArgs = null; | ||
| return applyFn(this, args); | ||
| }; | ||
| return debounced; | ||
| } | ||
| async function _applyPromised(fn, _this, args) { | ||
| return await fn.apply(_this, args); | ||
| } | ||
| //#endregion | ||
| export { debounce as t }; |
| export { }; |
| import { dt as resolve, m as getBuildInfo, ut as relative } from "../../_build/common.mjs"; | ||
| import { t as defineCommand } from "../../_libs/citty.mjs"; | ||
| import "./common.mjs"; | ||
| import build_default, { buildArgs } from "./build.mjs"; | ||
| import consola from "consola"; | ||
| import { execSync } from "node:child_process"; | ||
| //#region src/cli/commands/deploy.ts | ||
| var deploy_default = defineCommand({ | ||
| meta: { | ||
| name: "deploy", | ||
| description: "Build and deploy nitro project for production" | ||
| }, | ||
| args: { | ||
| ...buildArgs, | ||
| prebuilt: { | ||
| type: "boolean", | ||
| description: "Skip the build step and deploy the existing build" | ||
| } | ||
| }, | ||
| async run(ctx) { | ||
| globalThis.__nitroDeploying__ = true; | ||
| if (!ctx.args.prebuilt) await build_default.run(ctx); | ||
| if (globalThis.__nitroDeployed__) return; | ||
| const { buildInfo, outputDir } = await getBuildInfo(resolve(ctx.args.dir || ctx.args._dir || ".")); | ||
| if (!buildInfo) { | ||
| consola.error("No build info found, cannot deploy."); | ||
| process.exit(1); | ||
| } | ||
| if (!buildInfo.commands?.deploy) { | ||
| consola.error(`The \`${buildInfo.preset}\` preset does not have a default deploy command.\n\nTry using a different preset with the \`--preset\` option, or configure a deploy command in the Nitro config, or deploy manually.`); | ||
| process.exit(1); | ||
| } | ||
| const extraArgs = ctx.rawArgs.indexOf("--") !== -1 ? ctx.rawArgs.slice(ctx.rawArgs.indexOf("--") + 1).join(" ") : ""; | ||
| const deployCommand = buildInfo.commands.deploy.replace(/([\s:])\.\/(\S*)/g, `$1${relative(process.cwd(), outputDir)}/$2`) + (extraArgs ? ` ${extraArgs}` : ""); | ||
| consola.info(`$ ${deployCommand}`); | ||
| execSync(deployCommand, { stdio: "inherit" }); | ||
| } | ||
| }); | ||
| //#endregion | ||
| export { deploy_default as default }; |
| import { t as defineCommand } from "../../_libs/citty.mjs"; | ||
| import { execSync } from "node:child_process"; | ||
| //#region src/cli/commands/docs.ts | ||
| var docs_default = defineCommand({ | ||
| meta: { | ||
| name: "docs", | ||
| description: "Explore Nitro documentation" | ||
| }, | ||
| args: { page: { | ||
| type: "string", | ||
| description: "Page path to open" | ||
| } }, | ||
| run({ rawArgs }) { | ||
| const runnerCmd = ([ | ||
| ["bun", "x"], | ||
| ["pnpm", "dlx"], | ||
| ["npm", "x"] | ||
| ].find(([pkg]) => { | ||
| try { | ||
| execSync(`${pkg} -v`, { stdio: "ignore" }); | ||
| return true; | ||
| } catch {} | ||
| }) || ["npm", "x"]).join(" "); | ||
| const docsDir = new URL("../../../skills/nitro/docs", import.meta.url).pathname; | ||
| const args = rawArgs?.join(" ") || ""; | ||
| execSync(`${runnerCmd} mdzilla ${docsDir}${args ? ` ${args}` : ""}`, { stdio: "inherit" }); | ||
| } | ||
| }); | ||
| //#endregion | ||
| export { docs_default as default }; |
| import { dt as resolve } from "../../_build/common.mjs"; | ||
| import "../../_libs/httpxy.mjs"; | ||
| import { t as defineCommand } from "../../_libs/citty.mjs"; | ||
| import { t as startPreview } from "../../_chunks/nitro3.mjs"; | ||
| import { t as commonArgs } from "./common.mjs"; | ||
| import { serve } from "srvx"; | ||
| import { log } from "srvx/log"; | ||
| //#region src/cli/commands/preview.ts | ||
| var preview_default = defineCommand({ | ||
| meta: { | ||
| name: "preview", | ||
| description: "Start a local server to preview the built server" | ||
| }, | ||
| args: { | ||
| ...commonArgs, | ||
| port: { | ||
| type: "string", | ||
| description: "specify port" | ||
| }, | ||
| host: { | ||
| type: "string", | ||
| description: "specify hostname" | ||
| } | ||
| }, | ||
| async run({ args }) { | ||
| const rootDir = resolve(args.dir || args._dir || "."); | ||
| const server = serve({ | ||
| fetch(req) { | ||
| return preview.fetch(req); | ||
| }, | ||
| middleware: [log()], | ||
| gracefulShutdown: false, | ||
| port: args.port, | ||
| hostname: args.host | ||
| }); | ||
| const preview = await startPreview({ | ||
| rootDir, | ||
| loader: { srvxServer: server } | ||
| }); | ||
| if (preview.upgrade) server.node?.server?.on("upgrade", (req, socket, head) => { | ||
| preview.upgrade(req, socket, head); | ||
| }); | ||
| process.on("SIGINT", async () => { | ||
| await server.close(); | ||
| await preview.close(); | ||
| process.exit(0); | ||
| }); | ||
| } | ||
| }); | ||
| //#endregion | ||
| export { preview_default as default }; |
| import useColors from "@poppinss/colors"; | ||
| const ANSI_REGEX = new RegExp([`[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?(?:\\u0007|\\u001B\\u005C|\\u009C))`, "(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))"].join("|"), "g"); | ||
| function htmlEscape(value) { | ||
| return value.replace(/&/g, "&").replace(/\\"/g, "\"").replace(/</g, "<").replace(/>/g, ">"); | ||
| } | ||
| function wordWrap(value, options) { | ||
| const width = options.width; | ||
| const indent = options.indent; | ||
| const newLine = `${options.newLine}${indent}`; | ||
| if (!width) return options.escape ? options.escape(value) : htmlEscape(value); | ||
| let regexString = ".{1," + width + "}"; | ||
| regexString += "([\\s]+|$)|[^\\s]+?([\\s]+|$)"; | ||
| const re = new RegExp(regexString, "g"); | ||
| return (value.match(re) || []).map(function(line) { | ||
| if (line.slice(-1) === "\n") line = line.slice(0, line.length - 1); | ||
| return options.escape ? options.escape(line) : htmlEscape(line); | ||
| }).join(newLine); | ||
| } | ||
| function stripAnsi(value) { | ||
| return value.replace(ANSI_REGEX, ""); | ||
| } | ||
| const colors = useColors.ansi(); | ||
| export { wordWrap as i, htmlEscape as n, stripAnsi as r, colors as t }; |
| import { readFile } from "node:fs/promises"; | ||
| var BaseComponent = class { | ||
| #cachedStyles; | ||
| #cachedScript; | ||
| #inDevMode; | ||
| scriptFile; | ||
| cssFile; | ||
| constructor(devMode) { | ||
| this.#inDevMode = devMode; | ||
| } | ||
| async getStyles() { | ||
| if (!this.cssFile) return null; | ||
| if (this.#inDevMode) return await readFile(this.cssFile, "utf-8"); | ||
| this.#cachedStyles = this.#cachedStyles ?? await readFile(this.cssFile, "utf-8"); | ||
| return this.#cachedStyles; | ||
| } | ||
| async getScript() { | ||
| if (!this.scriptFile) return null; | ||
| if (this.#inDevMode) return await readFile(this.scriptFile, "utf-8"); | ||
| this.#cachedScript = this.#cachedScript ?? await readFile(this.scriptFile, "utf-8"); | ||
| return this.#cachedScript; | ||
| } | ||
| }; | ||
| const publicDirURL = new URL("./public/", import.meta.url); | ||
| export { BaseComponent as n, publicDirURL as t }; |
| import { n as BaseComponent, t as publicDirURL } from "../../../public_dir-C5bujZKB.js"; | ||
| import { t as colors } from "../../../helpers-B9BQYaS6.js"; | ||
| import { dump, themes } from "@poppinss/dumper/html"; | ||
| import { dump as dump$1 } from "@poppinss/dumper/console"; | ||
| var ErrorCause = class extends BaseComponent { | ||
| cssFile = new URL("./error_cause/style.css", publicDirURL); | ||
| async toHTML(props) { | ||
| if (!props.error.cause) return ""; | ||
| return `<section> | ||
| <div class="card"> | ||
| <div class="card-heading"> | ||
| <div> | ||
| <h3 class="card-title"> | ||
| Error Cause | ||
| </h3> | ||
| </div> | ||
| </div> | ||
| <div class="card-body"> | ||
| <div id="error-cause"> | ||
| ${dump(props.error.cause, { | ||
| cspNonce: props.cspNonce, | ||
| styles: themes.cssVariables, | ||
| inspectObjectPrototype: false, | ||
| inspectStaticMembers: false, | ||
| inspectArrayPrototype: false | ||
| })} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </section>`; | ||
| } | ||
| async toANSI(props) { | ||
| if (!props.error.cause) return ""; | ||
| let depth = process.env.YOUCH_CAUSE ? Number(process.env.YOUCH_CAUSE) : 2; | ||
| if (Number.isNaN(depth)) depth = 2; | ||
| return `\n\n${colors.red("[CAUSE]")}\n${dump$1(props.error.cause, { | ||
| depth, | ||
| inspectObjectPrototype: false, | ||
| inspectStaticMembers: false, | ||
| inspectArrayPrototype: false | ||
| })}`; | ||
| } | ||
| }; | ||
| export { ErrorCause }; |
| import { n as BaseComponent, t as publicDirURL } from "../../../public_dir-C5bujZKB.js"; | ||
| import { i as wordWrap, n as htmlEscape, t as colors } from "../../../helpers-B9BQYaS6.js"; | ||
| const ERROR_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="24" height="24" fill="none"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 7v6m0 4.01.01-.011M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10Z"/></svg>`; | ||
| const HINT_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="24" height="24" fill="none"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m21 2-1 1M3 2l1 1m17 13-1-1M3 16l1-1m5 3h6m-5 3h4M12 3C8 3 5.952 4.95 6 8c.023 1.487.5 2.5 1.5 3.5S9 13 9 15h6c0-2 .5-2.5 1.5-3.5h0c1-1 1.477-2.013 1.5-3.5.048-3.05-2-5-6-5Z"/></svg>`; | ||
| const COPY_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M7 7m0 2.667a2.667 2.667 0 0 1 2.667 -2.667h8.666a2.667 2.667 0 0 1 2.667 2.667v8.666a2.667 2.667 0 0 1 -2.667 2.667h-8.666a2.667 2.667 0 0 1 -2.667 -2.667z" /><path d="M4.012 16.737a2.005 2.005 0 0 1 -1.012 -1.737v-10c0 -1.1 .9 -2 2 -2h10c.75 0 1.158 .385 1.5 1" /></svg>`; | ||
| function buildErrorWithStacktrace(error) { | ||
| const cwd = process.cwd(); | ||
| const lines = []; | ||
| lines.push(`${error.name}: ${error.message}`); | ||
| if (error.hint) lines.push(`Hint: ${error.hint.replace(/(<([^>]+)>)/gi, "")}`); | ||
| if (error.frames.length > 0) { | ||
| lines.push(""); | ||
| lines.push("Stack trace:"); | ||
| for (const frame of error.frames) { | ||
| const fileName = frame.fileName?.replace(`${cwd}/`, "") || "<anonymous>"; | ||
| const func = frame.functionName ? `at ${frame.functionName} ` : "at "; | ||
| lines.push(` ${func}(${fileName}:${frame.lineNumber}:${frame.columnNumber})`); | ||
| } | ||
| } | ||
| return lines.join("\n"); | ||
| } | ||
| function htmlAttributeEscape(value) { | ||
| return value.replace(/&/g, "&").replace(/"/g, """).replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">"); | ||
| } | ||
| var ErrorInfo = class extends BaseComponent { | ||
| cssFile = new URL("./error_info/style.css", publicDirURL); | ||
| scriptFile = new URL("./error_info/script.js", publicDirURL); | ||
| async toHTML(props) { | ||
| const stacktraceText = buildErrorWithStacktrace(props.error); | ||
| return `<section> | ||
| <h4 id="error-name">${htmlEscape(props.error.name)}</h4> | ||
| <h1 id="error-title">${htmlEscape(props.title)}</h1> | ||
| </section> | ||
| <section> | ||
| <div class="card"> | ||
| <div class="card-body"> | ||
| <h2 id="error-message"> | ||
| <span>${ERROR_ICON_SVG}</span> | ||
| <span>${htmlEscape(props.error.message)}</span> | ||
| <button | ||
| id="copy-error-btn" | ||
| data-error-text="${htmlAttributeEscape(stacktraceText)}" | ||
| onclick="copyErrorMessage(this)" | ||
| title="Copy error with stack trace" | ||
| aria-label="Copy error with stack trace to clipboard" | ||
| > | ||
| ${COPY_ICON_SVG} | ||
| </button> | ||
| </h2> | ||
| ${props.error.hint ? `<div id="error-hint"> | ||
| <span>${HINT_ICON_SVG}</span> | ||
| <span>${props.error.hint}</span> | ||
| </div>` : ""} | ||
| </div> | ||
| </div> | ||
| </section>`; | ||
| } | ||
| async toANSI(props) { | ||
| return `${colors.red(`ℹ ${wordWrap(`${props.error.name}: ${props.error.message}`, { | ||
| width: process.stdout.columns, | ||
| indent: " ", | ||
| newLine: "\n", | ||
| escape: (value) => value | ||
| })}`)}${props.error.hint ? `\n\n${colors.blue("◉")} ${colors.dim().italic(wordWrap(props.error.hint.replace(/(<([^>]+)>)/gi, ""), { | ||
| width: process.stdout.columns, | ||
| indent: " ", | ||
| newLine: "\n", | ||
| escape: (value) => value | ||
| }))}` : ""}`; | ||
| } | ||
| }; | ||
| export { ErrorInfo }; |
| import { n as BaseComponent, t as publicDirURL } from "../../../public_dir-C5bujZKB.js"; | ||
| import { n as htmlEscape } from "../../../helpers-B9BQYaS6.js"; | ||
| import { dump, themes } from "@poppinss/dumper/html"; | ||
| var ErrorMetadata = class extends BaseComponent { | ||
| cssFile = new URL("./error_metadata/style.css", publicDirURL); | ||
| #primitives = [ | ||
| "string", | ||
| "boolean", | ||
| "number", | ||
| "undefined" | ||
| ]; | ||
| #formatRowValue(value, dumpValue, cspNonce) { | ||
| if (dumpValue === true) return dump(value, { | ||
| styles: themes.cssVariables, | ||
| cspNonce | ||
| }); | ||
| if (this.#primitives.includes(typeof value) || value === null) return typeof value === "string" ? htmlEscape(value) : value; | ||
| return dump(value, { | ||
| styles: themes.cssVariables, | ||
| cspNonce | ||
| }); | ||
| } | ||
| #renderRows(rows, cspNonce) { | ||
| return `<table class="card-table"> | ||
| <tbody> | ||
| ${rows.map((row) => { | ||
| return `<tr> | ||
| <td class="table-key">${row.key}</td> | ||
| <td class="table-value"> | ||
| ${this.#formatRowValue(row.value, row.dump, cspNonce)} | ||
| </td> | ||
| </tr>`; | ||
| }).join("\n")} | ||
| </tbody> | ||
| </table>`; | ||
| } | ||
| #renderSection(section, rows, cspNonce) { | ||
| return `<div> | ||
| <h4 class="card-subtitle">${section}</h4> | ||
| ${Array.isArray(rows) ? this.#renderRows(rows, cspNonce) : `<span>${this.#formatRowValue(rows.value, rows.dump, cspNonce)}</span>`} | ||
| </div>`; | ||
| } | ||
| #renderGroup(group, sections, cspNonce) { | ||
| return `<section class="metadata-group"> | ||
| <div class="card"> | ||
| <div class="card-heading"> | ||
| <h3 class="card-title">${group}</h3> | ||
| </div> | ||
| <div class="card-body"> | ||
| ${Object.keys(sections).map((section) => this.#renderSection(section, sections[section], cspNonce)).join("\n")} | ||
| </div> | ||
| </div> | ||
| </section>`; | ||
| } | ||
| async toHTML(props) { | ||
| const groups = props.metadata.toJSON(); | ||
| const groupsNames = Object.keys(groups); | ||
| if (!groupsNames.length) return ""; | ||
| return groupsNames.map((group) => this.#renderGroup(group, groups[group], props.cspNonce)).join("\n"); | ||
| } | ||
| async toANSI() { | ||
| return ""; | ||
| } | ||
| }; | ||
| export { ErrorMetadata }; |
| import { n as BaseComponent, t as publicDirURL } from "../../../public_dir-C5bujZKB.js"; | ||
| import { r as stripAnsi, t as colors } from "../../../helpers-B9BQYaS6.js"; | ||
| import { extname } from "node:path"; | ||
| import { highlightText } from "@speed-highlight/core"; | ||
| import { highlightText as highlightText$1 } from "@speed-highlight/core/terminal"; | ||
| const GUTTER = "┃"; | ||
| const POINTER = "❯"; | ||
| const LANGS_MAP = { | ||
| ".tsx": "ts", | ||
| ".jsx": "js", | ||
| ".js": "js", | ||
| ".ts": "ts", | ||
| ".css": "css", | ||
| ".json": "json", | ||
| ".html": "html", | ||
| ".astro": "ts", | ||
| ".vue": "ts" | ||
| }; | ||
| var ErrorStackSource = class extends BaseComponent { | ||
| cssFile = new URL("./error_stack_source/style.css", publicDirURL); | ||
| async toHTML(props) { | ||
| const frame = props.frame; | ||
| if (frame.type === "native" || !frame.source || !frame.fileName) return ""; | ||
| const language = LANGS_MAP[extname(frame.fileName)] ?? "plain"; | ||
| const highlight = `<div class="line-highlight" style="margin-top: ${`${frame.source.findIndex((chunk) => { | ||
| return chunk.lineNumber === frame.lineNumber; | ||
| }) * 24}px`}"></div>`; | ||
| let code = await highlightText(frame.source.map((chunk) => chunk.chunk).join("\n"), language, true); | ||
| code = code.replace("<div class=\"shj-numbers\">", `<div class="shj-numbers" style="counter-set: line ${frame.source[0].lineNumber - 1}">`); | ||
| return `<pre><code class="shj-lang-js">${highlight}${code}</code></pre>`; | ||
| } | ||
| async toANSI(props) { | ||
| const frame = props.frame; | ||
| if (frame.type === "native" || !frame.source || !frame.fileName) return ""; | ||
| const language = LANGS_MAP[extname(frame.fileName)] ?? "plain"; | ||
| const largestLineNumber = Math.max(...frame.source.map(({ lineNumber }) => lineNumber)); | ||
| const lineNumberCols = String(largestLineNumber).length; | ||
| return `\n\n${(await highlightText$1(frame.source.map(({ chunk }) => chunk).join("\n"), language)).split("\n").map((line, index) => { | ||
| const lineNumber = frame.source[index].lineNumber; | ||
| const alignedLineNumber = String(lineNumber).padStart(lineNumberCols, " "); | ||
| if (lineNumber === props.frame.lineNumber) return ` ${colors.bgRed(`${POINTER} ${alignedLineNumber} ${GUTTER} ${stripAnsi(line)}`)}`; | ||
| return ` ${colors.dim(alignedLineNumber)} ${colors.dim(GUTTER)} ${line}`; | ||
| }).join("\n")}\n`; | ||
| } | ||
| }; | ||
| export { ErrorStackSource }; |
| import { n as BaseComponent, t as publicDirURL } from "../../../public_dir-C5bujZKB.js"; | ||
| import { n as htmlEscape, t as colors } from "../../../helpers-B9BQYaS6.js"; | ||
| import { dump, themes } from "@poppinss/dumper/html"; | ||
| import { dump as dump$1 } from "@poppinss/dumper/console"; | ||
| const CHEVIRON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" width="24" height="24" stroke-width="2"> | ||
| <path d="M6 9l6 6l6 -6"></path> | ||
| </svg>`; | ||
| const EDITORS = { | ||
| textmate: "txmt://open?url=file://%f&line=%l", | ||
| macvim: "mvim://open?url=file://%f&line=%l", | ||
| emacs: "emacs://open?url=file://%f&line=%l", | ||
| sublime: "subl://open?url=file://%f&line=%l", | ||
| phpstorm: "phpstorm://open?file=%f&line=%l", | ||
| atom: "atom://core/open/file?filename=%f&line=%l", | ||
| vscode: "vscode://file/%f:%l" | ||
| }; | ||
| var ErrorStack = class extends BaseComponent { | ||
| cssFile = new URL("./error_stack/style.css", publicDirURL); | ||
| scriptFile = new URL("./error_stack/script.js", publicDirURL); | ||
| #getRelativeFileName(filePath) { | ||
| return filePath.replace(`${process.cwd()}/`, ""); | ||
| } | ||
| #getFirstExpandedFrameIndex(frames) { | ||
| let expandAtIndex = frames.findIndex((frame) => frame.type === "app"); | ||
| if (expandAtIndex === -1) expandAtIndex = frames.findIndex((frame) => frame.type === "module"); | ||
| return expandAtIndex; | ||
| } | ||
| #getEditorLink(ide, frame) { | ||
| const editorURL = EDITORS[ide] || ide; | ||
| if (!editorURL || frame.type === "native") return { text: this.#getRelativeFileName(frame.fileName) }; | ||
| return { | ||
| href: editorURL.replace("%f", frame.fileName).replace("%l", String(frame.lineNumber)), | ||
| text: this.#getRelativeFileName(frame.fileName) | ||
| }; | ||
| } | ||
| #renderFrameLocation(frame, ide) { | ||
| const { text, href } = this.#getEditorLink(ide, frame); | ||
| const fileName = `<a${href ? ` href="${href}"` : ""} class="stack-frame-filepath" title="${text}"> | ||
| ${htmlEscape(text)} | ||
| </a>`; | ||
| const functionName = frame.functionName ? `<span>in <code title="${frame.functionName}"> | ||
| ${htmlEscape(frame.functionName)} | ||
| </code></span>` : ""; | ||
| const loc = `<span>at line <code>${frame.lineNumber}:${frame.columnNumber}</code></span>`; | ||
| if (frame.type !== "native" && frame.source) return `<button class="stack-frame-location"> | ||
| ${fileName} ${functionName} ${loc} | ||
| </button>`; | ||
| return `<div class="stack-frame-location"> | ||
| ${fileName} ${functionName} ${loc} | ||
| </div>`; | ||
| } | ||
| async #renderStackFrame(frame, index, expandAtIndex, props) { | ||
| const label = frame.type === "app" ? "<span class=\"frame-label\">In App</span>" : ""; | ||
| const expandedClass = expandAtIndex === index ? " expanded" : ""; | ||
| const toggleButton = frame.type !== "native" && frame.source ? `<button class="stack-frame-toggle-indicator">${CHEVIRON}</button>` : ""; | ||
| return `<li class="stack-frame stack-frame-${frame.type}${expandedClass}"> | ||
| <div class="stack-frame-contents"> | ||
| ${this.#renderFrameLocation(frame, props.ide)} | ||
| <div class="stack-frame-extras"> | ||
| ${label} | ||
| ${toggleButton} | ||
| </div> | ||
| </div> | ||
| <div class="stack-frame-source"> | ||
| ${await props.sourceCodeRenderer(props.error, frame)} | ||
| </div> | ||
| </li>`; | ||
| } | ||
| async #printStackFrame(frame, index, expandAtIndex, props) { | ||
| const loc = `${this.#getRelativeFileName(frame.fileName)}:${frame.lineNumber}:${frame.columnNumber}`; | ||
| if (index === expandAtIndex) { | ||
| const functionName = frame.functionName ? `at ${frame.functionName} ` : ""; | ||
| const codeSnippet = await props.sourceCodeRenderer(props.error, frame); | ||
| return ` ⁃ ${functionName}${colors.yellow(`(${loc})`)}${codeSnippet}`; | ||
| } | ||
| if (frame.type === "native") { | ||
| const functionName = frame.functionName ? `at ${colors.italic(frame.functionName)} ` : ""; | ||
| return colors.dim(` ⁃ ${functionName}(${colors.italic(loc)})`); | ||
| } | ||
| return ` ⁃ ${frame.functionName ? `at ${frame.functionName} ` : ""}${colors.yellow(`(${loc})`)}`; | ||
| } | ||
| async toHTML(props) { | ||
| return `<section> | ||
| <div class="card"> | ||
| <div class="card-heading"> | ||
| <div> | ||
| <h3 class="card-title"> | ||
| Stack Trace | ||
| </h3> | ||
| </div> | ||
| </div> | ||
| <div class="card-body"> | ||
| <div id="stack-frames-wrapper"> | ||
| <div id="stack-frames-header"> | ||
| <div id="all-frames-toggle-wrapper"> | ||
| <label id="all-frames-toggle"> | ||
| <input type="checkbox" /> | ||
| <span> View All Frames </span> | ||
| </label> | ||
| </div> | ||
| <div> | ||
| <div class="toggle-switch"> | ||
| <button id="formatted-frames-toggle" class="active"> Pretty </button> | ||
| <button id="raw-frames-toggle"> Raw </button> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| <div id="stack-frames-body"> | ||
| <div id="stack-frames-formatted" class="visible"> | ||
| <ul id="stack-frames"> | ||
| ${(await Promise.all(props.error.frames.map((frame, index) => { | ||
| return this.#renderStackFrame(frame, index, this.#getFirstExpandedFrameIndex(props.error.frames), props); | ||
| }))).join("\n")} | ||
| </ul> | ||
| </div> | ||
| <div id="stack-frames-raw"> | ||
| ${dump(props.error.raw, { | ||
| styles: themes.cssVariables, | ||
| expand: true, | ||
| cspNonce: props.cspNonce, | ||
| inspectObjectPrototype: false, | ||
| inspectStaticMembers: false, | ||
| inspectArrayPrototype: false | ||
| })} | ||
| </div> | ||
| </div> | ||
| <div> | ||
| </div> | ||
| </div> | ||
| </section>`; | ||
| } | ||
| async toANSI(props) { | ||
| const displayRaw = process.env.YOUCH_RAW; | ||
| if (displayRaw) { | ||
| const depth = Number.isNaN(Number(displayRaw)) ? 2 : Number(displayRaw); | ||
| return `\n\n${colors.red("[RAW]")}\n${dump$1(props.error.raw, { | ||
| depth, | ||
| inspectObjectPrototype: false, | ||
| inspectStaticMembers: false, | ||
| inspectArrayPrototype: false | ||
| })}`; | ||
| } | ||
| const frames = await Promise.all(props.error.frames.map((frame, index) => { | ||
| return this.#printStackFrame(frame, index, this.#getFirstExpandedFrameIndex(props.error.frames), props); | ||
| })); | ||
| if (frames.length) return `\n\n${frames.join("\n")}`; | ||
| return ""; | ||
| } | ||
| }; | ||
| export { ErrorStack }; |
| import { n as BaseComponent, t as publicDirURL } from "../../../public_dir-C5bujZKB.js"; | ||
| const DARK_MODE_SVG = `<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="15" height="15" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M0 0h24v24H0z" stroke="none"/><path d="M12 3h.393a7.5 7.5 0 0 0 7.92 12.446A9 9 0 1 1 12 2.992z"/></svg>`; | ||
| const LIGHT_MODE_SVG = `<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="15" height="15" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M0 0h24v24H0z" stroke="none"/><circle cx="12" cy="12" r="4"/><path d="M3 12h1m8-9v1m8 8h1m-9 8v1M5.6 5.6l.7.7m12.1-.7-.7.7m0 11.4.7.7m-12.1-.7-.7.7"/></svg>`; | ||
| var Header = class extends BaseComponent { | ||
| cssFile = new URL("./header/style.css", publicDirURL); | ||
| scriptFile = new URL("./header/script.js", publicDirURL); | ||
| async toHTML() { | ||
| return `<header id="header"> | ||
| <div id="header-actions"> | ||
| <div id="toggle-theme-container"> | ||
| <input type="checkbox" id="toggle-theme-checkbox" /> | ||
| <label id="toggle-theme-label" for="toggle-theme-checkbox"> | ||
| <span id="light-theme-indicator" title="Light mode">${LIGHT_MODE_SVG}</span> | ||
| <span id="dark-theme-indicator" title="Dark mode">${DARK_MODE_SVG}</span> | ||
| </label> | ||
| </div> | ||
| </div> | ||
| </header>`; | ||
| } | ||
| async toANSI() { | ||
| return ""; | ||
| } | ||
| }; | ||
| export { Header }; |
| import { n as BaseComponent, t as publicDirURL } from "../../../public_dir-C5bujZKB.js"; | ||
| var Layout = class extends BaseComponent { | ||
| cssFile = new URL("./layout/style.css", publicDirURL); | ||
| scriptFile = new URL("./layout/script.js", publicDirURL); | ||
| async toHTML(props) { | ||
| return `<!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <title>${props.title}</title> | ||
| <!-- STYLES --> | ||
| <!-- GLOBAL SCRIPT --> | ||
| </head> | ||
| <body> | ||
| <div id="layout"> | ||
| ${await props.children()} | ||
| </div> | ||
| <!-- SCRIPTS --> | ||
| </body> | ||
| </html>`; | ||
| } | ||
| async toANSI(props) { | ||
| return `\n${await props.children()}\n`; | ||
| } | ||
| }; | ||
| export { Layout }; |
| import { timingSafeEqual } from "node:crypto"; | ||
| import { defineHandler, HTTPError } from "nitro/h3"; | ||
| import { runCronTasks } from "#nitro/runtime/task"; | ||
| export default defineHandler(async (event) => { | ||
| // Validate CRON_SECRET if set - https://vercel.com/docs/cron-jobs/manage-cron-jobs#securing-cron-jobs | ||
| const cronSecret = process.env.CRON_SECRET; | ||
| if (cronSecret) { | ||
| const authHeader = event.req.headers.get("authorization") || ""; | ||
| const expected = `Bearer ${cronSecret}`; | ||
| const a = Buffer.from(authHeader); | ||
| const b = Buffer.from(expected); | ||
| if (a.length !== b.length || !timingSafeEqual(a, b)) { | ||
| throw new HTTPError("Unauthorized", { status: 401 }); | ||
| } | ||
| } | ||
| const cron = event.req.headers.get("x-vercel-cron-schedule"); | ||
| if (!cron) { | ||
| throw new HTTPError("Missing x-vercel-cron-schedule header", { status: 400 }); | ||
| } | ||
| await runCronTasks(cron, { | ||
| context: { waitUntil: event.req.waitUntil }, | ||
| payload: { scheduledTime: Date.now() } | ||
| }); | ||
| return { success: true }; | ||
| }); |
| import "#nitro/virtual/polyfills"; | ||
| import { createHandler } from "../../cloudflare/runtime/_module-handler.mjs"; | ||
| export default createHandler({ fetch() {} }); |
| import { ModuleRunner, ESModulesEvaluator } from "vite/module-runner"; | ||
| import { createViteTransport } from "env-runner/vite"; | ||
| // Custom evaluator for workerd where `new AsyncFunction()` is disallowed. | ||
| // Uses the unsafeEvalBinding exposed by the env-runner miniflare wrapper. | ||
| class WorkerdModuleEvaluator { | ||
| startOffset = 0; | ||
| async runInlinedModule(context, code) { | ||
| const unsafeEval = globalThis.__ENV_RUNNER_UNSAFE_EVAL__; | ||
| const keys = Object.keys(context); | ||
| const fn = unsafeEval.newAsyncFunction('"use strict";' + code, "runInlinedModule", ...keys); | ||
| await fn(...keys.map((k) => context[k])); | ||
| Object.seal(context[Object.keys(context)[0]]); | ||
| } | ||
| runExternalModule(filepath) { | ||
| return import(filepath); | ||
| } | ||
| } | ||
| // ----- IPC ----- | ||
| let sendMessage; | ||
| const messageListeners = new Set(); | ||
| // ----- Environment runners ----- | ||
| const envs = (globalThis.__nitro_vite_envs__ ??= { | ||
| nitro: undefined, | ||
| ssr: undefined, | ||
| }); | ||
| class ViteEnvRunner { | ||
| constructor({ name, entry }) { | ||
| this.name = name; | ||
| this.entryPath = entry; | ||
| this.entry = undefined; | ||
| this.entryError = undefined; | ||
| // Create Vite Module Runner | ||
| // https://vite.dev/guide/api-environment-runtimes.html#modulerunner | ||
| const onMessage = (listener) => messageListeners.add(listener); | ||
| const transport = createViteTransport((data) => sendMessage?.(data), onMessage, name); | ||
| const evaluator = globalThis.__ENV_RUNNER_UNSAFE_EVAL__ | ||
| ? new WorkerdModuleEvaluator() | ||
| : new ESModulesEvaluator(); | ||
| const debug = | ||
| typeof process !== "undefined" && process.env?.NITRO_DEBUG ? console.debug : undefined; | ||
| this.runner = new ModuleRunner({ transport }, evaluator, debug); | ||
| this.reload(); | ||
| } | ||
| async reload() { | ||
| try { | ||
| this.entry = await this.runner.import(this.entryPath); | ||
| this.entryError = undefined; | ||
| } catch (error) { | ||
| console.error(error); | ||
| this.entryError = error; | ||
| } | ||
| } | ||
| async fetch(req, init) { | ||
| if (this.entryError) { | ||
| return renderError(req, this.entryError); | ||
| } | ||
| for (let i = 0; i < 5 && !(this.entry || this.entryError); i++) { | ||
| await new Promise((r) => setTimeout(r, 100 * Math.pow(2, i))); | ||
| } | ||
| if (this.entryError) { | ||
| return renderError(req, this.entryError); | ||
| } | ||
| if (!this.entry) { | ||
| throw httpError(503, `Vite environment "${this.name}" is unavailable`); | ||
| } | ||
| try { | ||
| const entryFetch = this.entry.fetch || this.entry.default?.fetch; | ||
| if (!entryFetch) { | ||
| throw httpError(500, `No fetch handler exported from ${this.entryPath}`); | ||
| } | ||
| return await entryFetch(req, init); | ||
| } catch (error) { | ||
| return renderError(req, error); | ||
| } | ||
| } | ||
| } | ||
| // ----- RPC ----- | ||
| const rpcRequests = new Map(); | ||
| function rpc(name, data, timeout = 3000) { | ||
| const id = Math.random().toString(36).slice(2); | ||
| return new Promise((resolve, reject) => { | ||
| const timer = setTimeout(() => { | ||
| rpcRequests.delete(id); | ||
| reject(new Error(`RPC "${name}" timed out`)); | ||
| }, timeout); | ||
| rpcRequests.set(id, { resolve, reject, timer }); | ||
| sendMessage?.({ __rpc: name, __rpc_id: id, data }); | ||
| }); | ||
| } | ||
| // Trap unhandled errors to avoid worker crash | ||
| if (typeof process !== "undefined" && typeof process.on === "function") { | ||
| process.on("unhandledRejection", (error) => console.error(error)); | ||
| process.on("uncaughtException", (error) => console.error(error)); | ||
| } | ||
| // ----- RSC Support ----- | ||
| // define __VITE_ENVIRONMENT_RUNNER_IMPORT__ for RSC support | ||
| // https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-rsc/README.md#__vite_environment_runner_import__ | ||
| globalThis.__VITE_ENVIRONMENT_RUNNER_IMPORT__ = async function (environmentName, id) { | ||
| const env = envs[environmentName]; | ||
| if (!env) { | ||
| throw new Error(`Vite environment "${environmentName}" is not registered`); | ||
| } | ||
| return env.runner.import(id); | ||
| }; | ||
| // ----- Reload ----- | ||
| async function reload() { | ||
| try { | ||
| await Promise.all(Object.values(envs).map((env) => env?.reload())); | ||
| } catch (error) { | ||
| console.error(error); | ||
| } | ||
| } | ||
| // eslint-disable-next-line unicorn/prefer-top-level-await | ||
| reload(); | ||
| // ----- HTML Transform ----- | ||
| globalThis.__transform_html__ = async function (html) { | ||
| html = await rpc("transformHTML", html).catch((error) => { | ||
| console.warn("Failed to transform HTML via Vite:", error); | ||
| return html; | ||
| }); | ||
| return html; | ||
| }; | ||
| // ----- Exports (env-runner AppEntry) ----- | ||
| export function fetch(req) { | ||
| const viteEnv = req?.headers.get("x-vite-env") || "nitro"; | ||
| const env = envs[viteEnv]; | ||
| if (!env) { | ||
| return renderError(req, httpError(500, `Unknown vite environment "${viteEnv}"`)); | ||
| } | ||
| return env.fetch(req); | ||
| } | ||
| export function upgrade(context) { | ||
| const handleUpgrade = envs.nitro?.entry?.handleUpgrade; | ||
| if (handleUpgrade) { | ||
| handleUpgrade(context.node.req, context.node.socket, context.node.head); | ||
| } | ||
| } | ||
| export const ipc = { | ||
| onOpen(ctx) { | ||
| sendMessage = ctx.sendMessage; | ||
| }, | ||
| onMessage(message) { | ||
| if (message?.__rpc_id) { | ||
| const req = rpcRequests.get(message.__rpc_id); | ||
| if (req) { | ||
| clearTimeout(req.timer); | ||
| rpcRequests.delete(message.__rpc_id); | ||
| if (message.error) { | ||
| req.reject(typeof message.error === "string" ? new Error(message.error) : message.error); | ||
| } else { | ||
| req.resolve(message.data); | ||
| } | ||
| } | ||
| return; | ||
| } | ||
| if (message?.type === "custom") { | ||
| if (message.event === "nitro:vite-env") { | ||
| const { name, entry } = message.data; | ||
| if (!envs[name]) { | ||
| envs[name] = new ViteEnvRunner({ name, entry }); | ||
| } | ||
| return; | ||
| } | ||
| } | ||
| if (message?.type === "full-reload") { | ||
| reload(); | ||
| return; | ||
| } | ||
| for (const listener of messageListeners) { | ||
| listener(message); | ||
| } | ||
| }, | ||
| onClose() {}, | ||
| }; | ||
| // ----- Error handling ----- | ||
| function httpError(status, message) { | ||
| const error = new Error(message || `HTTP Error ${status}`); | ||
| error.status = status; | ||
| error.name = "NitroViteError"; | ||
| return error; | ||
| } | ||
| async function renderError(req, error) { | ||
| if (req.headers.get("accept")?.includes("application/json")) { | ||
| return new Response( | ||
| JSON.stringify( | ||
| { | ||
| status: error.status || 500, | ||
| name: error.name || "Error", | ||
| message: error.message, | ||
| stack: (error.stack || "") | ||
| .split("\n") | ||
| .splice(1) | ||
| .map((l) => l.trim()), | ||
| }, | ||
| null, | ||
| 2 | ||
| ), | ||
| { | ||
| status: error.status || 500, | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| "Cache-Control": "no-store, max-age=0, must-revalidate", | ||
| Pragma: "no-cache", | ||
| Expires: "0", | ||
| }, | ||
| } | ||
| ); | ||
| } | ||
| try { | ||
| const { Youch } = await import("youch"); | ||
| const youch = new Youch(); | ||
| return new Response(await youch.toHTML(error), { | ||
| status: error.status || 500, | ||
| headers: { | ||
| "Content-Type": "text/html", | ||
| "Cache-Control": "no-store, max-age=0, must-revalidate", | ||
| Pragma: "no-cache", | ||
| Expires: "0", | ||
| }, | ||
| }); | ||
| } catch { | ||
| return new Response(`<pre>${error.stack || error.message || error}</pre>`, { | ||
| status: error.status || 500, | ||
| headers: { | ||
| "Content-Type": "text/html", | ||
| "Cache-Control": "no-store, max-age=0, must-revalidate", | ||
| Pragma: "no-cache", | ||
| Expires: "0", | ||
| }, | ||
| }); | ||
| } | ||
| } |
Sorry, the diff of this file is too big to display
| [ | ||
| { | ||
| "slug": "docs", | ||
| "path": "/docs", | ||
| "title": "Docs", | ||
| "order": 1, | ||
| "icon": "i-lucide-book-open", | ||
| "children": [ | ||
| { | ||
| "slug": "", | ||
| "path": "/docs", | ||
| "title": "Introduction", | ||
| "order": 1, | ||
| "icon": "i-lucide-compass" | ||
| }, | ||
| { | ||
| "slug": "quick-start", | ||
| "path": "/docs/quick-start", | ||
| "title": "Quick Start", | ||
| "order": 2, | ||
| "icon": "i-lucide-zap" | ||
| }, | ||
| { | ||
| "slug": "renderer", | ||
| "path": "/docs/renderer", | ||
| "title": "Renderer", | ||
| "order": 4, | ||
| "icon": "ri:layout-masonry-line" | ||
| }, | ||
| { | ||
| "slug": "routing", | ||
| "path": "/docs/routing", | ||
| "title": "Routing", | ||
| "order": 5, | ||
| "icon": "ri:direction-line" | ||
| }, | ||
| { | ||
| "slug": "server-entry", | ||
| "path": "/docs/server-entry", | ||
| "title": "Server Entry", | ||
| "order": 6, | ||
| "icon": "ri:server-line" | ||
| }, | ||
| { | ||
| "slug": "cache", | ||
| "path": "/docs/cache", | ||
| "title": "Cache", | ||
| "order": 7, | ||
| "icon": "ri:speed-line" | ||
| }, | ||
| { | ||
| "slug": "storage", | ||
| "path": "/docs/storage", | ||
| "title": "KV Storage", | ||
| "order": 8, | ||
| "icon": "carbon:datastore" | ||
| }, | ||
| { | ||
| "slug": "assets", | ||
| "path": "/docs/assets", | ||
| "title": "Assets", | ||
| "order": 50, | ||
| "icon": "ri:image-2-line" | ||
| }, | ||
| { | ||
| "slug": "configuration", | ||
| "path": "/docs/configuration", | ||
| "title": "Configuration", | ||
| "order": 50, | ||
| "icon": "ri:settings-3-line" | ||
| }, | ||
| { | ||
| "slug": "database", | ||
| "path": "/docs/database", | ||
| "title": "Database", | ||
| "order": 50, | ||
| "icon": "ri:database-2-line" | ||
| }, | ||
| { | ||
| "slug": "lifecycle", | ||
| "path": "/docs/lifecycle", | ||
| "title": "Lifecycle", | ||
| "order": 50, | ||
| "icon": "i-lucide-layers" | ||
| }, | ||
| { | ||
| "slug": "plugins", | ||
| "path": "/docs/plugins", | ||
| "title": "Plugins", | ||
| "order": 50, | ||
| "icon": "ri:plug-line" | ||
| }, | ||
| { | ||
| "slug": "tasks", | ||
| "path": "/docs/tasks", | ||
| "title": "Tasks", | ||
| "order": 50, | ||
| "icon": "codicon:run-all" | ||
| }, | ||
| { | ||
| "slug": "migration", | ||
| "path": "/docs/migration", | ||
| "title": "Migration Guide", | ||
| "order": 99, | ||
| "icon": "ri:arrow-right-up-line" | ||
| }, | ||
| { | ||
| "slug": "nightly", | ||
| "path": "/docs/nightly", | ||
| "title": "Nightly Channel", | ||
| "order": 99, | ||
| "icon": "ri:moon-fill" | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| "slug": "deploy", | ||
| "path": "/deploy", | ||
| "title": "Deploy", | ||
| "order": 2, | ||
| "children": [ | ||
| { | ||
| "slug": "", | ||
| "path": "/deploy", | ||
| "title": "Deploy", | ||
| "order": 0, | ||
| "icon": "ri:upload-cloud-2-line" | ||
| }, | ||
| { | ||
| "slug": "runtimes", | ||
| "path": "/deploy/runtimes", | ||
| "title": "Runtimes", | ||
| "order": 10, | ||
| "page": false, | ||
| "children": [ | ||
| { | ||
| "slug": "node", | ||
| "path": "/deploy/runtimes/node", | ||
| "title": "Node.js", | ||
| "order": 1, | ||
| "icon": "akar-icons:node-fill" | ||
| }, | ||
| { | ||
| "slug": "bun", | ||
| "path": "/deploy/runtimes/bun", | ||
| "title": "Bun", | ||
| "order": null, | ||
| "icon": "simple-icons:bun" | ||
| }, | ||
| { | ||
| "slug": "deno", | ||
| "path": "/deploy/runtimes/deno", | ||
| "title": "Deno", | ||
| "order": null, | ||
| "icon": "simple-icons:deno" | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| "slug": "providers", | ||
| "path": "/deploy/providers", | ||
| "title": "Providers", | ||
| "order": 20, | ||
| "page": false, | ||
| "children": [ | ||
| { | ||
| "slug": "alwaysdata", | ||
| "path": "/deploy/providers/alwaysdata", | ||
| "title": "Alwaysdata", | ||
| "order": null | ||
| }, | ||
| { | ||
| "slug": "aws", | ||
| "path": "/deploy/providers/aws", | ||
| "title": "AWS Lambda", | ||
| "order": null | ||
| }, | ||
| { | ||
| "slug": "aws-amplify", | ||
| "path": "/deploy/providers/aws-amplify", | ||
| "title": "AWS Amplify", | ||
| "order": null | ||
| }, | ||
| { | ||
| "slug": "azure", | ||
| "path": "/deploy/providers/azure", | ||
| "title": "Azure", | ||
| "order": null | ||
| }, | ||
| { | ||
| "slug": "cleavr", | ||
| "path": "/deploy/providers/cleavr", | ||
| "title": "Cleavr", | ||
| "order": null | ||
| }, | ||
| { | ||
| "slug": "cloudflare", | ||
| "path": "/deploy/providers/cloudflare", | ||
| "title": "Cloudflare", | ||
| "order": null | ||
| }, | ||
| { | ||
| "slug": "deno-deploy", | ||
| "path": "/deploy/providers/deno-deploy", | ||
| "title": "Deno Deploy", | ||
| "order": null | ||
| }, | ||
| { | ||
| "slug": "digitalocean", | ||
| "path": "/deploy/providers/digitalocean", | ||
| "title": "DigitalOcean", | ||
| "order": null | ||
| }, | ||
| { | ||
| "slug": "firebase", | ||
| "path": "/deploy/providers/firebase", | ||
| "title": "Firebase", | ||
| "order": null | ||
| }, | ||
| { | ||
| "slug": "flightcontrol", | ||
| "path": "/deploy/providers/flightcontrol", | ||
| "title": "Flightcontrol", | ||
| "order": null | ||
| }, | ||
| { | ||
| "slug": "genezio", | ||
| "path": "/deploy/providers/genezio", | ||
| "title": "Genezio", | ||
| "order": null | ||
| }, | ||
| { | ||
| "slug": "github-pages", | ||
| "path": "/deploy/providers/github-pages", | ||
| "title": "GitHub Pages", | ||
| "order": null | ||
| }, | ||
| { | ||
| "slug": "gitlab-pages", | ||
| "path": "/deploy/providers/gitlab-pages", | ||
| "title": "GitLab Pages", | ||
| "order": null | ||
| }, | ||
| { | ||
| "slug": "heroku", | ||
| "path": "/deploy/providers/heroku", | ||
| "title": "Heroku", | ||
| "order": null | ||
| }, | ||
| { | ||
| "slug": "iis", | ||
| "path": "/deploy/providers/iis", | ||
| "title": "IIS", | ||
| "order": null | ||
| }, | ||
| { | ||
| "slug": "koyeb", | ||
| "path": "/deploy/providers/koyeb", | ||
| "title": "Koyeb", | ||
| "order": null | ||
| }, | ||
| { | ||
| "slug": "netlify", | ||
| "path": "/deploy/providers/netlify", | ||
| "title": "Netlify", | ||
| "order": null | ||
| }, | ||
| { | ||
| "slug": "platform-sh", | ||
| "path": "/deploy/providers/platform-sh", | ||
| "title": "Platform.sh", | ||
| "order": null | ||
| }, | ||
| { | ||
| "slug": "render", | ||
| "path": "/deploy/providers/render", | ||
| "title": "Render.com", | ||
| "order": null | ||
| }, | ||
| { | ||
| "slug": "stormkit", | ||
| "path": "/deploy/providers/stormkit", | ||
| "title": "StormKit", | ||
| "order": null | ||
| }, | ||
| { | ||
| "slug": "vercel", | ||
| "path": "/deploy/providers/vercel", | ||
| "title": "Vercel", | ||
| "order": null | ||
| }, | ||
| { | ||
| "slug": "zeabur", | ||
| "path": "/deploy/providers/zeabur", | ||
| "title": "Zeabur", | ||
| "order": null | ||
| }, | ||
| { | ||
| "slug": "zephyr", | ||
| "path": "/deploy/providers/zephyr", | ||
| "title": "Zephyr Cloud", | ||
| "order": null | ||
| }, | ||
| { | ||
| "slug": "zerops", | ||
| "path": "/deploy/providers/zerops", | ||
| "title": "Zerops", | ||
| "order": null | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| "slug": "config", | ||
| "path": "/config", | ||
| "title": "Config", | ||
| "order": 3, | ||
| "children": [ | ||
| { | ||
| "slug": "", | ||
| "path": "/config", | ||
| "title": "Config", | ||
| "order": 0, | ||
| "icon": "ri:settings-3-line" | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| "slug": "examples", | ||
| "path": "/examples", | ||
| "title": "Examples", | ||
| "order": 4, | ||
| "children": [ | ||
| { | ||
| "slug": "", | ||
| "path": "/examples", | ||
| "title": "Examples", | ||
| "order": 0, | ||
| "icon": "i-lucide-folder-code" | ||
| }, | ||
| { | ||
| "slug": "api-routes", | ||
| "path": "/examples/api-routes", | ||
| "title": "API Routes", | ||
| "order": null, | ||
| "icon": "i-lucide-route", | ||
| "category": "features" | ||
| }, | ||
| { | ||
| "slug": "auto-imports", | ||
| "path": "/examples/auto-imports", | ||
| "title": "Auto Imports", | ||
| "order": null, | ||
| "icon": "i-lucide-import", | ||
| "category": "config" | ||
| }, | ||
| { | ||
| "slug": "cached-handler", | ||
| "path": "/examples/cached-handler", | ||
| "title": "Cached Handler", | ||
| "order": null, | ||
| "icon": "i-lucide-clock", | ||
| "category": "features" | ||
| }, | ||
| { | ||
| "slug": "custom-error-handler", | ||
| "path": "/examples/custom-error-handler", | ||
| "title": "Custom Error Handler", | ||
| "order": null, | ||
| "icon": "i-lucide-alert-circle", | ||
| "category": "features" | ||
| }, | ||
| { | ||
| "slug": "database", | ||
| "path": "/examples/database", | ||
| "title": "Database", | ||
| "order": null, | ||
| "icon": "i-lucide-database", | ||
| "category": "features" | ||
| }, | ||
| { | ||
| "slug": "elysia", | ||
| "path": "/examples/elysia", | ||
| "title": "Elysia", | ||
| "order": null, | ||
| "icon": "i-skill-icons-elysia-dark", | ||
| "category": "backend frameworks" | ||
| }, | ||
| { | ||
| "slug": "express", | ||
| "path": "/examples/express", | ||
| "title": "Express", | ||
| "order": null, | ||
| "icon": "i-simple-icons-express", | ||
| "category": "backend frameworks" | ||
| }, | ||
| { | ||
| "slug": "fastify", | ||
| "path": "/examples/fastify", | ||
| "title": "Fastify", | ||
| "order": null, | ||
| "icon": "i-simple-icons-fastify", | ||
| "category": "backend frameworks" | ||
| }, | ||
| { | ||
| "slug": "hello-world", | ||
| "path": "/examples/hello-world", | ||
| "title": "Hello World", | ||
| "order": null, | ||
| "icon": "i-lucide-sparkles", | ||
| "category": "features" | ||
| }, | ||
| { | ||
| "slug": "hono", | ||
| "path": "/examples/hono", | ||
| "title": "Hono", | ||
| "order": null, | ||
| "icon": "i-logos-hono", | ||
| "category": "backend frameworks" | ||
| }, | ||
| { | ||
| "slug": "import-alias", | ||
| "path": "/examples/import-alias", | ||
| "title": "Import Alias", | ||
| "order": null, | ||
| "icon": "i-lucide-at-sign", | ||
| "category": "config" | ||
| }, | ||
| { | ||
| "slug": "middleware", | ||
| "path": "/examples/middleware", | ||
| "title": "Middleware", | ||
| "order": null, | ||
| "icon": "i-lucide-layers", | ||
| "category": "features" | ||
| }, | ||
| { | ||
| "slug": "mono-jsx", | ||
| "path": "/examples/mono-jsx", | ||
| "title": "Mono JSX", | ||
| "order": null, | ||
| "icon": "i-lucide-brackets", | ||
| "category": "server side rendering" | ||
| }, | ||
| { | ||
| "slug": "nano-jsx", | ||
| "path": "/examples/nano-jsx", | ||
| "title": "Nano JSX", | ||
| "order": null, | ||
| "icon": "i-lucide-brackets", | ||
| "category": "server side rendering" | ||
| }, | ||
| { | ||
| "slug": "plugins", | ||
| "path": "/examples/plugins", | ||
| "title": "Plugins", | ||
| "order": null, | ||
| "icon": "i-lucide-plug", | ||
| "category": "features" | ||
| }, | ||
| { | ||
| "slug": "renderer", | ||
| "path": "/examples/renderer", | ||
| "title": "Custom Renderer", | ||
| "order": null, | ||
| "icon": "i-lucide-code", | ||
| "category": "server side rendering" | ||
| }, | ||
| { | ||
| "slug": "runtime-config", | ||
| "path": "/examples/runtime-config", | ||
| "title": "Runtime Config", | ||
| "order": null, | ||
| "icon": "i-lucide-settings", | ||
| "category": "config" | ||
| }, | ||
| { | ||
| "slug": "server-fetch", | ||
| "path": "/examples/server-fetch", | ||
| "title": "Server Fetch", | ||
| "order": null, | ||
| "icon": "i-lucide-arrow-right-left", | ||
| "category": "features" | ||
| }, | ||
| { | ||
| "slug": "shiki", | ||
| "path": "/examples/shiki", | ||
| "title": "Shiki", | ||
| "order": null, | ||
| "icon": "i-lucide-highlighter", | ||
| "category": "integrations" | ||
| }, | ||
| { | ||
| "slug": "virtual-routes", | ||
| "path": "/examples/virtual-routes", | ||
| "title": "Virtual Routes", | ||
| "order": null, | ||
| "icon": "i-lucide-box", | ||
| "category": "features" | ||
| }, | ||
| { | ||
| "slug": "vite-nitro-plugin", | ||
| "path": "/examples/vite-nitro-plugin", | ||
| "title": "Vite Nitro Plugin", | ||
| "order": null, | ||
| "icon": "i-logos-vitejs", | ||
| "category": "vite" | ||
| }, | ||
| { | ||
| "slug": "vite-rsc", | ||
| "path": "/examples/vite-rsc", | ||
| "title": "Vite RSC", | ||
| "order": null, | ||
| "icon": "i-logos-react", | ||
| "category": "vite" | ||
| }, | ||
| { | ||
| "slug": "vite-ssr-html", | ||
| "path": "/examples/vite-ssr-html", | ||
| "title": "Vite SSR HTML", | ||
| "order": null, | ||
| "icon": "i-logos-html-5", | ||
| "category": "server side rendering" | ||
| }, | ||
| { | ||
| "slug": "vite-ssr-preact", | ||
| "path": "/examples/vite-ssr-preact", | ||
| "title": "SSR with Preact", | ||
| "order": null, | ||
| "icon": "i-logos-preact", | ||
| "category": "server side rendering" | ||
| }, | ||
| { | ||
| "slug": "vite-ssr-react", | ||
| "path": "/examples/vite-ssr-react", | ||
| "title": "SSR with React", | ||
| "order": null, | ||
| "icon": "i-logos-react", | ||
| "category": "server side rendering" | ||
| }, | ||
| { | ||
| "slug": "vite-ssr-solid", | ||
| "path": "/examples/vite-ssr-solid", | ||
| "title": "SSR with SolidJS", | ||
| "order": null, | ||
| "icon": "i-logos-solidjs-icon", | ||
| "category": "server side rendering" | ||
| }, | ||
| { | ||
| "slug": "vite-ssr-tsr-react", | ||
| "path": "/examples/vite-ssr-tsr-react", | ||
| "title": "SSR with TanStack Router", | ||
| "order": null, | ||
| "icon": "i-simple-icons-tanstack", | ||
| "category": "server side rendering" | ||
| }, | ||
| { | ||
| "slug": "vite-ssr-tss-react", | ||
| "path": "/examples/vite-ssr-tss-react", | ||
| "title": "SSR with TanStack Start", | ||
| "order": null, | ||
| "icon": "i-simple-icons-tanstack", | ||
| "category": "server side rendering" | ||
| }, | ||
| { | ||
| "slug": "vite-ssr-vue-router", | ||
| "path": "/examples/vite-ssr-vue-router", | ||
| "title": "SSR with Vue Router", | ||
| "order": null, | ||
| "icon": "i-logos-vue", | ||
| "category": "server side rendering" | ||
| }, | ||
| { | ||
| "slug": "vite-trpc", | ||
| "path": "/examples/vite-trpc", | ||
| "title": "Vite + tRPC", | ||
| "order": null, | ||
| "icon": "i-simple-icons-trpc", | ||
| "category": "vite" | ||
| }, | ||
| { | ||
| "slug": "websocket", | ||
| "path": "/examples/websocket", | ||
| "title": "WebSocket", | ||
| "order": null, | ||
| "icon": "i-lucide-radio", | ||
| "category": "features" | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| "slug": "", | ||
| "path": "/", | ||
| "title": "Index", | ||
| "order": null, | ||
| "seo": { | ||
| "title": "Build Full-Stack Servers", | ||
| "description": "Nitro extends your Vite application with a production-ready server, compatible with any runtime. Add server routes to your application and deploy many hosting platform with a zero-config experience." | ||
| }, | ||
| "orientation": "horizontal", | ||
| "filename": "nitro.config.ts", | ||
| "features": [ | ||
| { | ||
| "title": "Fast", | ||
| "description": "Enjoy the fast Vite 8 (rolldown powered) development experience with HMR on the server and optimized for production.", | ||
| "icon": "i-lucide-zap", | ||
| "color": "text-amber-500", | ||
| "bgColor": "bg-amber-500/10", | ||
| "borderColor": "group-hover:border-amber-500/30" | ||
| }, | ||
| { | ||
| "title": "Agnostic", | ||
| "description": "Deploy the same codebase to any deployment provider with zero config and locked-in.", | ||
| "icon": "i-lucide-globe", | ||
| "color": "text-sky-500", | ||
| "bgColor": "bg-sky-500/10", | ||
| "borderColor": "group-hover:border-sky-500/30" | ||
| }, | ||
| { | ||
| "title": "Minimal", | ||
| "description": "Nitro adds no overhead to runtime. Build your servers with any modern tool you like.", | ||
| "icon": "i-lucide-feather", | ||
| "color": "text-emerald-500", | ||
| "bgColor": "bg-emerald-500/10", | ||
| "borderColor": "group-hover:border-emerald-500/30" | ||
| } | ||
| ], | ||
| "metrics": [ | ||
| { | ||
| "label": "Bare metal perf", | ||
| "value": "~Native", | ||
| "unit": "RPS", | ||
| "description": "Using compile router, and fast paths for request handling.", | ||
| "icon": "i-lucide-gauge", | ||
| "color": "text-emerald-500", | ||
| "bgColor": "bg-emerald-500/10", | ||
| "barWidth": "95%", | ||
| "barColor": "bg-emerald-500" | ||
| }, | ||
| { | ||
| "label": "Minimum install Size", | ||
| "value": "Tiny", | ||
| "unit": "deps", | ||
| "description": "Minimal dependencies. No bloated node_modules.", | ||
| "icon": "i-lucide-package", | ||
| "color": "text-sky-500", | ||
| "bgColor": "bg-sky-500/10", | ||
| "barWidth": "15%", | ||
| "barColor": "bg-sky-500" | ||
| }, | ||
| { | ||
| "label": "Small and portable output", | ||
| "value": "‹ 10", | ||
| "unit": "kB", | ||
| "description": "Standard server builds produce ultra-small output bundles.", | ||
| "icon": "i-lucide-file-output", | ||
| "color": "text-violet-500", | ||
| "bgColor": "bg-violet-500/10", | ||
| "barWidth": "10%", | ||
| "barColor": "bg-violet-500" | ||
| }, | ||
| { | ||
| "label": "FAST builds", | ||
| "value": "‹ 1", | ||
| "unit": "sec", | ||
| "description": "Cold production builds complete in seconds, not minutes.", | ||
| "icon": "i-lucide-timer", | ||
| "color": "text-amber-500", | ||
| "bgColor": "bg-amber-500/10", | ||
| "barWidth": "12%", | ||
| "barColor": "bg-amber-500" | ||
| } | ||
| ], | ||
| "headline": "Assets", | ||
| "link": "/docs/assets", | ||
| "link-label": "Assets docs" | ||
| } | ||
| ] |
| # Config | ||
| <read-more></read-more> | ||
| ## General | ||
| ### `preset` | ||
| Use `preset` option or `NITRO_PRESET` environment variable for custom **production** preset. | ||
| Preset for development mode is always `nitro_dev` and default `node_server` for production building a standalone Node.js server. | ||
| The preset will automatically be detected when the `preset` option is not set and running in known environments. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| preset: "cloudflare_pages", // deploy to Cloudflare Pages | ||
| }); | ||
| ``` | ||
| ### `debug` | ||
| - Default: `false` (`true` when `DEBUG` environment variable is set) | ||
| Enable debug mode for verbose logging and additional development information. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| debug: true, | ||
| }); | ||
| ``` | ||
| ### `logLevel` | ||
| - Default: `3` (`1` when the testing environment is detected) | ||
| Log verbosity level. See [consola](https://github.com/unjs/consola?tab=readme-ov-file#log-level) for more information. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| logLevel: 4, // verbose logging | ||
| }); | ||
| ``` | ||
| ### `runtimeConfig` | ||
| - Default: `{ nitro: { ... }, ...yourOptions }` | ||
| Server runtime configuration. | ||
| **Note:** `nitro` namespace is reserved. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| runtimeConfig: { | ||
| apiSecret: "default-secret", // override with NITRO_API_SECRET | ||
| }, | ||
| }); | ||
| ``` | ||
| ### `compatibilityDate` | ||
| Deployment providers introduce new features that Nitro presets can leverage, but some of them need to be explicitly opted into. | ||
| Set it to latest tested date in `YYYY-MM-DD` format to leverage latest preset features. | ||
| If this configuration is not provided, Nitro will use `"latest"` behavior by default. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| compatibilityDate: "2025-01-01", | ||
| }); | ||
| ``` | ||
| ### `static` | ||
| - Default: `false` | ||
| Enable static site generation mode. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| static: true, // prerender all routes | ||
| }); | ||
| ``` | ||
| ## Features | ||
| ### `features` | ||
| - Default: `{}` | ||
| Enable built-in features. | ||
| #### `runtimeHooks` | ||
| - Default: auto-detected (enabled if there is at least one nitro plugin) | ||
| Enable runtime hooks for request and response. | ||
| #### `websocket` | ||
| - Default: `false` | ||
| Enable WebSocket support. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| features: { | ||
| runtimeHooks: true, | ||
| websocket: true, // enable WebSocket support | ||
| }, | ||
| }); | ||
| ``` | ||
| ### `experimental` | ||
| - Default: `{}` | ||
| Enable experimental features. | ||
| #### `openAPI` | ||
| - Default: `false` | ||
| Enable `/_scalar`, `/_swagger` and `/_openapi.json` endpoints. | ||
| <note> | ||
| Prefer using the top-level [`openAPI`](#openapi) option for configuration. | ||
| </note> | ||
| #### `typescriptBundlerResolution` | ||
| Enable TypeScript bundler module resolution. See [TypeScript#51669](https://github.com/microsoft/TypeScript/pull/51669). | ||
| #### `asyncContext` | ||
| Enable native async context support for `useRequest()`. | ||
| #### `sourcemapMinify` | ||
| Set to `false` to disable experimental sourcemap minification. | ||
| #### `envExpansion` | ||
| Allow env expansion in runtime config. See [#2043](https://github.com/nitrojs/nitro/pull/2043). | ||
| #### `database` | ||
| Enable experimental database support. See [Database](/docs/database). | ||
| #### `tasks` | ||
| Enable experimental tasks support. See [Tasks](/docs/tasks). | ||
| #### `tsconfigPaths` | ||
| - Default: `true` | ||
| Infer path aliases from `tsconfig.json`. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| experimental: { | ||
| typescriptBundlerResolution: true, | ||
| asyncContext: true, | ||
| envExpansion: true, | ||
| database: true, | ||
| tasks: true, | ||
| }, | ||
| }); | ||
| ``` | ||
| ### `openAPI` | ||
| Top-level OpenAPI configuration. | ||
| You can pass an object to modify your OpenAPI specification: | ||
| ```js | ||
| openAPI: { | ||
| meta: { | ||
| title: 'My Awesome Project', | ||
| description: 'This might become the next big thing.', | ||
| version: '1.0' | ||
| } | ||
| } | ||
| ``` | ||
| These routes are disabled by default in production. To enable them, use the `production` key. | ||
| `"runtime"` allows middleware usage, and `"prerender"` is the most efficient because the JSON response is constant. | ||
| ```js | ||
| openAPI: { | ||
| // IMPORTANT: make sure to protect OpenAPI routes if necessary! | ||
| production: "runtime", // or "prerender" | ||
| } | ||
| ``` | ||
| If you like to customize the Scalar integration, you can [pass a configuration object](https://github.com/scalar/scalar) like this: | ||
| ```js | ||
| openAPI: { | ||
| ui: { | ||
| scalar: { | ||
| theme: 'purple' | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
| Or if you want to customize the endpoints: | ||
| ```js | ||
| openAPI: { | ||
| route: "/_docs/openapi.json", | ||
| ui: { | ||
| scalar: { | ||
| route: "/_docs/scalar" | ||
| }, | ||
| swagger: { | ||
| route: "/_docs/swagger" | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
| ### `future` | ||
| - Default: `{}` | ||
| New features pending for a major version to avoid breaking changes. | ||
| #### `nativeSWR` | ||
| Uses built-in SWR functionality (using caching layer and storage) for Netlify and Vercel presets instead of falling back to ISR behavior. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| future: { | ||
| nativeSWR: true, | ||
| }, | ||
| }); | ||
| ``` | ||
| ### `storage` | ||
| - Default: `{}` | ||
| Storage configuration, read more in the [Storage Layer](/docs/storage) section. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| storage: { | ||
| redis: { | ||
| driver: "redis", | ||
| url: "redis://localhost:6379", | ||
| }, | ||
| }, | ||
| }); | ||
| ``` | ||
| ### `devStorage` | ||
| - Default: `{}` | ||
| Storage configuration overrides for development mode. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| devStorage: { | ||
| redis: { | ||
| driver: "fs", | ||
| base: "./data/redis", // use filesystem in development | ||
| }, | ||
| }, | ||
| }); | ||
| ``` | ||
| ### `database` | ||
| Database connection configurations. Requires `experimental.database: true`. | ||
| ```ts | ||
| database: { | ||
| default: { | ||
| connector: "sqlite", | ||
| options: { name: "db" } | ||
| } | ||
| } | ||
| ``` | ||
| ### `devDatabase` | ||
| Database connection configuration overrides for development mode. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| devDatabase: { | ||
| default: { | ||
| connector: "sqlite", | ||
| options: { name: "db-dev" }, // separate dev database | ||
| }, | ||
| }, | ||
| }); | ||
| ``` | ||
| ### `renderer` | ||
| - Type: `false` | `{ handler?: string, static?: boolean, template?: string }` | ||
| Points to main render entry (file should export an event handler as default). | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| renderer: { | ||
| handler: "~/renderer", // path to the render handler | ||
| }, | ||
| }); | ||
| ``` | ||
| ### `serveStatic` | ||
| - Type: `boolean` | `'node'` | `'deno'` | `'inline'` | ||
| - Default: depends on the deployment preset used. | ||
| Serve `public/` assets in production. | ||
| **Note:** It is highly recommended that your edge CDN (Nginx, Apache, Cloud) serves the `.output/public/` directory instead to enable compression and higher level caching. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| serveStatic: "node", // serve static assets using Node.js | ||
| }); | ||
| ``` | ||
| ### `noPublicDir` | ||
| - Default: `false` | ||
| If enabled, disables `.output/public` directory creation. Skips copying `public/` dir and also disables pre-rendering. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| noPublicDir: true, // skip public directory output | ||
| }); | ||
| ``` | ||
| ### `publicAssets` | ||
| Public asset directories to serve in development and bundle in production. | ||
| If a `public/` directory is detected, it will be added by default, but you can add more by yourself too! | ||
| It's possible to set Cache-Control headers for assets using the `maxAge` option: | ||
| ```ts | ||
| publicAssets: [ | ||
| { | ||
| baseURL: "images", | ||
| dir: "public/images", | ||
| maxAge: 60 * 60 * 24 * 7, // 7 days | ||
| }, | ||
| ], | ||
| ``` | ||
| The config above generates the following header in the assets under `public/images/` folder: | ||
| `cache-control: public, max-age=604800, immutable` | ||
| The `dir` option is where your files live on your file system; the `baseURL` option is the folder they will be accessible from when served/bundled. | ||
| ### `compressPublicAssets` | ||
| - Default: `{ gzip: false, brotli: false, zstd: false }` | ||
| If enabled, Nitro will generate a pre-compressed (gzip, brotli, and/or zstd) version of supported types of public assets and prerendered routes | ||
| larger than 1024 bytes into the public directory. Default compression levels are used. Using this option you can support zero overhead asset compression without using a CDN. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| compressPublicAssets: { | ||
| gzip: true, | ||
| brotli: true, // enable gzip and brotli pre-compression | ||
| }, | ||
| }); | ||
| ``` | ||
| ### `serverAssets` | ||
| Assets can be accessed in server logic and bundled in production. [Read more](/docs/assets#server-assets). | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| serverAssets: [ | ||
| { | ||
| baseName: "templates", | ||
| dir: "./templates", // bundle templates/ as server assets | ||
| }, | ||
| ], | ||
| }); | ||
| ``` | ||
| ### `modules` | ||
| - Default: `[]` | ||
| An array of Nitro modules. Modules can be a string (path), a module object with a `setup` function, or a function. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| modules: [ | ||
| "./modules/my-module.ts", | ||
| (nitro) => { | ||
| nitro.hooks.hook("compiled", () => { /* ... */ }); | ||
| }, | ||
| ], | ||
| }); | ||
| ``` | ||
| ### `plugins` | ||
| - Default: `[]` | ||
| An array of paths to nitro plugins. They will be executed by order on the first initialization. | ||
| Note that Nitro auto-registers the plugins in the `plugins/` directory, [learn more](/docs/plugins). | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| plugins: [ | ||
| "~/plugins/my-plugin.ts", | ||
| ], | ||
| }); | ||
| ``` | ||
| ### `tasks` | ||
| - Default: `{}` | ||
| Task definitions. Each key is a task name with a `handler` path and optional `description`. | ||
| ```ts | ||
| tasks: { | ||
| 'db:migrate': { | ||
| handler: './tasks/db-migrate', | ||
| description: 'Run database migrations' | ||
| } | ||
| } | ||
| ``` | ||
| ### `scheduledTasks` | ||
| - Default: `{}` | ||
| Map of cron expressions to task name(s). | ||
| ```ts | ||
| scheduledTasks: { | ||
| '0 * * * *': 'cleanup:temp', | ||
| '*/5 * * * *': ['health:check', 'metrics:collect'] | ||
| } | ||
| ``` | ||
| ### `imports` | ||
| - Default: `false` | ||
| Auto import options. Set to an object to enable. See [unimport](https://github.com/unjs/unimport) for more information. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| imports: { | ||
| dirs: ["./utils"], // auto-import from utils/ directory | ||
| }, | ||
| }); | ||
| ``` | ||
| ### `virtual` | ||
| - Default: `{}` | ||
| A map from dynamic virtual import names to their contents or an (async) function that returns it. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| virtual: { | ||
| "#config": `export default { version: "1.0.0" }`, | ||
| }, | ||
| }); | ||
| ``` | ||
| ### `ignore` | ||
| - Default: `[]` | ||
| Array of glob patterns to ignore when scanning directories. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| ignore: [ | ||
| "routes/_legacy/**", // skip legacy route handlers | ||
| ], | ||
| }); | ||
| ``` | ||
| ### `wasm` | ||
| - Default: `{}` | ||
| - Type: `false` | `UnwasmPluginOptions` | ||
| WASM support configuration. See [unwasm](https://github.com/unjs/unwasm) for options. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| wasm: {}, // enable WASM import support | ||
| }); | ||
| ``` | ||
| ## Dev | ||
| ### `devServer` | ||
| - Default: `{ watch: [] }` | ||
| Dev server options. You can use `watch` to make the dev server reload if any file changes in specified paths. | ||
| Supports `port`, `hostname`, `watch`, and `runner` options. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| devServer: { | ||
| port: 3001, | ||
| watch: ["./server/plugins"], | ||
| }, | ||
| }); | ||
| ``` | ||
| ### `watchOptions` | ||
| Watch options for development mode. See [chokidar](https://github.com/paulmillr/chokidar) for more information. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| watchOptions: { | ||
| ignored: ["**/node_modules/**", "**/dist/**"], | ||
| }, | ||
| }); | ||
| ``` | ||
| ### `devProxy` | ||
| Proxy configuration for development server. | ||
| You can use this option to override development server routes and proxy-pass requests. | ||
| ```js | ||
| { | ||
| devProxy: { | ||
| '/proxy/test': 'http://localhost:3001', | ||
| '/proxy/example': { target: 'https://example.com', changeOrigin: true } | ||
| } | ||
| } | ||
| ``` | ||
| See [httpxy](https://github.com/unjs/httpxy) for all available target options. | ||
| ## Logging | ||
| ### `logging` | ||
| - Default: `{ compressedSizes: true, buildSuccess: true }` | ||
| Control build logging behavior. Set `compressedSizes` to `false` to skip reporting compressed bundle sizes. Set `buildSuccess` to `false` to suppress the build success message. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| logging: { | ||
| compressedSizes: false, // skip compressed size reporting | ||
| buildSuccess: false, | ||
| }, | ||
| }); | ||
| ``` | ||
| ## Routing | ||
| ### `baseURL` | ||
| Default: `/` (or `NITRO_APP_BASE_URL` environment variable if provided) | ||
| Server's main base URL. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| baseURL: "/app/", // serve app under /app/ prefix | ||
| }); | ||
| ``` | ||
| ### `apiBaseURL` | ||
| - Default: `/api` | ||
| Changes the default API base URL prefix. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| apiBaseURL: "/server/api", // api routes under /server/api/ | ||
| }); | ||
| ``` | ||
| ### `handlers` | ||
| Server handlers and routes. | ||
| If `routes/`, `api/` or `middleware/` directories exist inside the server directory, they will be automatically added to the handlers array. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| handlers: [ | ||
| { route: "/health", handler: "./handlers/health.ts" }, | ||
| { route: "/admin/**", handler: "./handlers/admin.ts", method: "get" }, | ||
| ], | ||
| }); | ||
| ``` | ||
| ### `devHandlers` | ||
| Regular handlers refer to the path of handlers to be imported and transformed by the bundler. | ||
| There are situations in that we directly want to provide a handler instance with programmatic usage. | ||
| We can use `devHandlers` but note that they are **only available in development mode** and **not in production build**. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| devHandlers: [ | ||
| { route: "/__dev", handler: eventHandler(() => "dev-only route") }, | ||
| ], | ||
| }); | ||
| ``` | ||
| ### `routes` | ||
| - Default: `{}` | ||
| Inline route definitions. A map from route pattern to handler path or handler options. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| routes: { | ||
| "/hello": "./routes/hello.ts", | ||
| "/greet": { handler: "./routes/greet.ts", method: "post" }, | ||
| }, | ||
| }); | ||
| ``` | ||
| ### `errorHandler` | ||
| - Type: `string` | `string[]` | ||
| Path(s) to custom runtime error handler(s). Replaces nitro's built-in error page. | ||
| **Example:** | ||
| ```js [nitro.config] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| errorHandler: "~/error", | ||
| }); | ||
| ``` | ||
| ```js [error.ts] | ||
| export default defineNitroErrorHandler((error, event) => { | ||
| return new Response('[custom error handler] ' + error.stack, { | ||
| headers: { 'Content-Type': 'text/plain' } | ||
| }); | ||
| }); | ||
| ``` | ||
| ### `routeRules` | ||
| **🧪 Experimental!** | ||
| Route options. It is a map from route pattern (following [rou3](https://github.com/h3js/rou3)) to route options. | ||
| When `cache` option is set, handlers matching pattern will be automatically wrapped with `defineCachedEventHandler`. | ||
| See the [Cache API](/docs/cache) for all available cache options. | ||
| <note> | ||
| `swr: true|number` is shortcut for `cache: { swr: true, maxAge: number }` | ||
| </note> | ||
| **Example:** | ||
| ```js | ||
| routeRules: { | ||
| '/blog/**': { swr: true }, | ||
| '/blog/**': { swr: 600 }, | ||
| '/blog/**': { static: true }, | ||
| '/blog/**': { cache: { /* cache options*/ } }, | ||
| '/assets/**': { headers: { 'cache-control': 's-maxage=0' } }, | ||
| '/api/v1/**': { cors: true, headers: { 'access-control-allow-methods': 'GET' } }, | ||
| '/old-page': { redirect: '/new-page' }, // uses status code 307 (Temporary Redirect) | ||
| '/old-page2': { redirect: { to:'/new-page2', statusCode: 301 } }, | ||
| '/old-page/**': { redirect: '/new-page/**' }, | ||
| '/proxy/example': { proxy: 'https://example.com' }, | ||
| '/proxy/**': { proxy: '/api/**' }, | ||
| '/admin/**': { basicAuth: { username: 'admin', password: 'secret' } }, | ||
| } | ||
| ``` | ||
| ### `prerender` | ||
| Default: | ||
| ```ts | ||
| { | ||
| autoSubfolderIndex: true, | ||
| concurrency: 1, | ||
| interval: 0, | ||
| failOnError: false, | ||
| crawlLinks: false, | ||
| ignore: [], | ||
| routes: [], | ||
| retry: 3, | ||
| retryDelay: 500 | ||
| } | ||
| ``` | ||
| Prerendered options. Any route specified will be fetched during the build and copied to the `.output/public` directory as a static asset. | ||
| Any route (string) that starts with a prefix listed in `ignore` or matches a regular expression or function will be ignored. | ||
| If `crawlLinks` option is set to `true`, nitro starts with `/` by default (or all routes in `routes` array) and for HTML pages extracts `<a>` tags and prerender them as well. | ||
| You can set `failOnError` option to `true` to stop the CI when Nitro could not prerender a route. | ||
| The `interval` and `concurrency` options lets you control the speed of pre-rendering, can be useful to avoid hitting some rate-limit if you call external APIs. | ||
| Set `autoSubfolderIndex` lets you control how to generate the files in the `.output/public` directory: | ||
| ```bash | ||
| # autoSubfolderIndex: true (default) | ||
| /about -> .output/public/about/index.html | ||
| # autoSubfolderIndex: false | ||
| /about -> .output/public/about.html | ||
| ``` | ||
| This option is useful when your hosting provider does not give you an option regarding the trailing slash. | ||
| The prerenderer will attempt to render pages 3 times with a delay of 500ms. Use `retry` and `retryDelay` to change this behavior. | ||
| ## Directories | ||
| ### `workspaceDir` | ||
| Project workspace root directory. | ||
| The workspace (e.g. pnpm workspace) directory is automatically detected when the `workspaceDir` option is not set. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| workspaceDir: "../", // monorepo root | ||
| }); | ||
| ``` | ||
| ### `rootDir` | ||
| Project main directory. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| rootDir: "./src/server", | ||
| }); | ||
| ``` | ||
| ### `serverDir` | ||
| - Default: `false` | ||
| - Type: `boolean` | `"./"` | `"./server"` | `string` | ||
| Server directory for scanning `api/`, `routes/`, `plugins/`, `utils/`, `middleware/`, `assets/`, and `tasks/` folders. | ||
| When set to `false`, automatic directory scanning is disabled. Set to `"./"` to use the root directory, or `"./server"` to use a `server/` subdirectory. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| serverDir: "./server", // scan server/ subdirectory | ||
| }); | ||
| ``` | ||
| ### `scanDirs` | ||
| - Default: (source directory when empty array) | ||
| List of directories to scan and auto-register files, such as API routes. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| scanDirs: ["./modules/auth/api", "./modules/billing/api"], | ||
| }); | ||
| ``` | ||
| ### `apiDir` | ||
| - Default: `api` | ||
| Defines a different directory to scan for api route handlers. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| apiDir: "endpoints", // scan endpoints/ instead of api/ | ||
| }); | ||
| ``` | ||
| ### `routesDir` | ||
| - Default: `routes` | ||
| Defines a different directory to scan for route handlers. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| routesDir: "pages", // scan pages/ instead of routes/ | ||
| }); | ||
| ``` | ||
| ### `buildDir` | ||
| - Default: `node_modules/.nitro` | ||
| Nitro's temporary working directory for generating build-related files. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| buildDir: ".nitro", // use .nitro/ in project root | ||
| }); | ||
| ``` | ||
| ### `output` | ||
| - Default: `{ dir: '.output', serverDir: '.output/server', publicDir: '.output/public' }` | ||
| Output directories for production bundle. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| output: { | ||
| dir: "dist", | ||
| serverDir: "dist/server", | ||
| publicDir: "dist/public", | ||
| }, | ||
| }); | ||
| ``` | ||
| ## Build | ||
| ### `builder` | ||
| - Type: `"rollup"` | `"rolldown"` | `"vite"` | ||
| - Default: `undefined` (auto-detected) | ||
| Specify the bundler to use for building. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| builder: "vite", | ||
| }); | ||
| ``` | ||
| ### `rollupConfig` | ||
| Additional rollup configuration. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| rollupConfig: { | ||
| output: { manualChunks: { vendor: ["lodash-es"] } }, | ||
| }, | ||
| }); | ||
| ``` | ||
| ### `rolldownConfig` | ||
| Additional rolldown configuration. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| rolldownConfig: { | ||
| output: { banner: "/* built with nitro */" }, | ||
| }, | ||
| }); | ||
| ``` | ||
| ### `entry` | ||
| Bundler entry point. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| entry: "./server/entry.ts", // custom entry file | ||
| }); | ||
| ``` | ||
| ### `unenv` | ||
| [unenv](https://github.com/unjs/unenv/) preset(s) for environment compatibility. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| unenv: { | ||
| alias: { "my-module": "my-module/web" }, | ||
| }, | ||
| }); | ||
| ``` | ||
| ### `alias` | ||
| Path aliases for module resolution. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| alias: { | ||
| "~utils": "./src/utils", | ||
| "#shared": "./shared", | ||
| }, | ||
| }); | ||
| ``` | ||
| ### `minify` | ||
| - Default: `false` | ||
| Minify bundle. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| minify: true, // minify production bundle | ||
| }); | ||
| ``` | ||
| ### `inlineDynamicImports` | ||
| - Default: `false` | ||
| Bundle all code into a single file instead of creating separate chunks per route. | ||
| When `false`, each route handler becomes a separate chunk loaded on-demand. When `true`, everything is bundled together. Some presets enable this by default. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| inlineDynamicImports: true, // single output file | ||
| }); | ||
| ``` | ||
| ### `sourcemap` | ||
| - Default: `false` | ||
| Enable source map generation. See [options](https://rollupjs.org/configuration-options/#output-sourcemap). | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| sourcemap: true, // generate .map files | ||
| }); | ||
| ``` | ||
| ### `node` | ||
| - Default: `true` | ||
| Specify whether the build is used for Node.js or not. If set to `false`, nitro tries to mock Node.js dependencies using [unenv](https://github.com/unjs/unenv) and adjust its behavior. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| node: false, // target non-Node.js runtimes | ||
| }); | ||
| ``` | ||
| ### `moduleSideEffects` | ||
| Default: `['unenv/polyfill/']` | ||
| Specifies module imports that have side-effects. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| moduleSideEffects: ["unenv/polyfill/", "reflect-metadata"], | ||
| }); | ||
| ``` | ||
| ### `replace` | ||
| Build-time string replacements. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| replace: { | ||
| "process.env.APP_VERSION": JSON.stringify("1.0.0"), | ||
| }, | ||
| }); | ||
| ``` | ||
| ### `commonJS` | ||
| Specifies additional configuration for the rollup CommonJS plugin. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| commonJS: { | ||
| requireReturnsDefault: "auto", | ||
| }, | ||
| }); | ||
| ``` | ||
| ### `exportConditions` | ||
| Custom export conditions for module resolution. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| exportConditions: ["worker", "production"], | ||
| }); | ||
| ``` | ||
| ### `noExternals` | ||
| - Default: `false` | ||
| Prevent specific packages from being externalized. Set to `true` to bundle all dependencies, or pass an array of package names/patterns. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| noExternals: true, // bundle all dependencies | ||
| }); | ||
| ``` | ||
| ### `traceDeps` | ||
| - Default: `[]` | ||
| Additional dependencies to trace and include in the build output. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| traceDeps: ["sharp", "better-sqlite3"], | ||
| }); | ||
| ``` | ||
| ### `oxc` | ||
| OXC options for rolldown builds. Includes `minify` and `transform` sub-options. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| oxc: { | ||
| minify: { compress: true, mangle: true }, | ||
| }, | ||
| }); | ||
| ``` | ||
| ## Advanced | ||
| ### `dev` | ||
| - Default: `true` for development and `false` for production. | ||
| **⚠️ Caution! This is an advanced configuration. Things can go wrong if misconfigured.** | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| dev: true, // force development mode behavior | ||
| }); | ||
| ``` | ||
| ### `typescript` | ||
| Default: `{ strict: true, generateRuntimeConfigTypes: false, generateTsConfig: false }` | ||
| TypeScript configuration options including `strict`, `generateRuntimeConfigTypes`, `generateTsConfig`, `tsConfig`, `generatedTypesDir`, and `tsconfigPath`. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| typescript: { | ||
| strict: true, | ||
| generateTsConfig: true, | ||
| }, | ||
| }); | ||
| ``` | ||
| ### `hooks` | ||
| **⚠️ Caution! This is an advanced configuration. Things can go wrong if misconfigured.** | ||
| nitro hooks. See [hookable](https://github.com/unjs/hookable) for more information. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| hooks: { | ||
| compiled(nitro) { | ||
| console.log("Build compiled successfully!"); | ||
| }, | ||
| }, | ||
| }); | ||
| ``` | ||
| ### `commands` | ||
| **⚠️ Caution! This is an advanced configuration. Things can go wrong if misconfigured.** | ||
| Preview and deploy command hints are usually filled by deployment presets. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| commands: { | ||
| preview: "node ./server/index.mjs", | ||
| }, | ||
| }); | ||
| ``` | ||
| ### `devErrorHandler` | ||
| **⚠️ Caution! This is an advanced configuration. Things can go wrong if misconfigured.** | ||
| A custom error handler function for development errors. | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| devErrorHandler: (error, event) => { | ||
| return new Response(`Dev error: ${error.message}`, { status: 500 }); | ||
| }, | ||
| }); | ||
| ``` | ||
| ### `framework` | ||
| - Default: `{ name: "nitro", version: "<current>" }` | ||
| Framework information. Used by presets and build info. Typically set by higher-level frameworks (e.g. Nuxt). | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| framework: { name: "my-framework", version: "2.0.0" }, | ||
| }); | ||
| ``` | ||
| ## Preset options | ||
| ### `firebase` | ||
| The options for the firebase functions preset. See [Preset Docs](/deploy/providers/firebase#options) | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| firebase: { | ||
| gen: 2, // use Cloud Functions 2nd gen | ||
| region: "us-central1", | ||
| }, | ||
| }); | ||
| ``` | ||
| ### `vercel` | ||
| The options for the vercel preset. See [Preset Docs](/deploy/providers/vercel) | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| vercel: { | ||
| config: { runtime: "nodejs20.x" }, | ||
| }, | ||
| }); | ||
| ``` | ||
| ### `cloudflare` | ||
| The options for the cloudflare preset. See [Preset Docs](/deploy/providers/cloudflare) | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| cloudflare: { | ||
| wrangler: { compatibility_date: "2025-01-01" }, | ||
| }, | ||
| }); | ||
| ``` | ||
| ### `zephyr` | ||
| The options for the zephyr preset. See [Preset Docs](/deploy/providers/zephyr#options) |
| # Deploy | ||
| > Learn more about Nitro deploy providers. | ||
| Nitro can generate different output formats suitable for different hosting providers from the same code base. | ||
| Using built-in presets, you can easily configure Nitro to adjust its output format with almost no additional code or configuration! | ||
| ## Default output | ||
| The default production output preset is [Node.js server](/deploy/runtimes/node). | ||
| When running Nitro in development mode, Nitro will always use a special preset called `nitro-dev` using Node.js with ESM in an isolated Worker environment with behavior as close as possible to the production environment. | ||
| ## Zero-Config Providers | ||
| When deploying to production using CI/CD, Nitro tries to automatically detect the provider environment and set the right one without any additional configuration required. Currently, the providers below can be auto-detected with zero config. | ||
| - [aws amplify](/deploy/providers/aws-amplify) | ||
| - [azure](/deploy/providers/azure) | ||
| - [cloudflare](/deploy/providers/cloudflare) | ||
| - [firebase app hosting](/deploy/providers/firebase#firebase-app-hosting) | ||
| - [netlify](/deploy/providers/netlify) | ||
| - [stormkit](/deploy/providers/stormkit) | ||
| - [vercel](/deploy/providers/vercel) | ||
| - [zeabur](/deploy/providers/zeabur) | ||
| <warning> | ||
| For Turborepo users, zero config detection will be interferenced by its Strict Environment Mode. You may need to allowing the variables explictly or use its Loose Environment Mode (with `--env-mode=loose` flag). | ||
| </warning> | ||
| Other built-in providers are available with an explicit preset, including [zephyr](/deploy/providers/zephyr). | ||
| ## Changing the deployment preset | ||
| If you need to build Nitro against a specific provider, you can target it by defining an environment variable named `NITRO_PRESET` or `SERVER_PRESET`, or by updating your Nitro [configuration](/docs/configuration) or using `--preset` argument. | ||
| Using the environment variable approach is recommended for deployments depending on CI/CD. | ||
| **Example:** Defining a `NITRO_PRESET` environment variable | ||
| ```bash | ||
| nitro build --preset cloudflare_pages | ||
| ``` | ||
| **Example:** Updating the `nitro.config.ts` file | ||
| ```ts | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| preset: 'cloudflare_pages' | ||
| }) | ||
| ``` | ||
| ## Compatibility date | ||
| Deployment providers regularly update their runtime behavior. Nitro presets are updated to support these new features. | ||
| To prevent breaking existing deployments, Nitro uses compatibility dates. These dates let you lock in behavior at the project creation time. You can also opt in to future updates when ready. | ||
| When you create a new project, the `compatibilityDate` is set to the current date. This setting is saved in your project's configuration. | ||
| You should update the compatibility date periodically. Always test your deployment thoroughly after updating. Below is a list of key dates and their effects. |
| # Alwaysdata | ||
| > Deploy Nitro apps to alwaysdata. | ||
| **Preset:** `alwaysdata` | ||
| <read-more></read-more> | ||
| ## Set up application | ||
| ### Pre-requisites | ||
| 1. [Register a new profile](https://www.alwaysdata.com/en/register/) on alwaysdata platform if you don't have one. | ||
| 2. Get a free 100Mb plan to host your app. | ||
| > [!NOTE] | ||
| > Keep in mind your *account name* will be used to provide you a default URL in the form of `account_name.alwaysdata.net`, so choose it wisely. You can also link your existing domains to your account later or register as many accounts under your profile as you need. | ||
| ### Local deployment | ||
| 1. Build your project locally with `npm run build -- preset alwaysdata` | ||
| 2. [Upload your app](https://help.alwaysdata.com/en/remote-access/) to your account in its own directory (e.g. `$HOME/www/my-app`). You can use any protocol you prefer (SSH/FTP/WebDAV…) to do so. | ||
| 3. On your admin panel, [create a new site](https://admin.alwaysdata.com/site/add/) for your app with the following features: | ||
| - *Addresses*: `[account_name].alwaysdata.net` | ||
| - *Type*: Node.js | ||
| - *Command*: `node .output/server/index.mjs` | ||
| - *Working directory*: `www/my-app` (adapt it to your deployment path) | ||
| - *Environment*: | ||
| ```ini | ||
| NITRO_PRESET=alwaysdata | ||
| ``` | ||
| - *Node.js version*: `Default version` is fine; pick no less than `20.0.0` (you can also [set your Node.js version globally](https://help.alwaysdata.com/en/languages/nodejs/configuration/#supported-versions)) | ||
| - *Hot restart*: `SIGHUP` | ||
| <read-more></read-more> | ||
| - Your app is now live at `http(s)://[account_name].alwaysdata.net`. | ||
| # AWS Amplify | ||
| > Deploy Nitro apps to AWS Amplify Hosting. | ||
| **Preset:** `aws_amplify` | ||
| <read-more></read-more> | ||
| ## Deploy to AWS Amplify Hosting | ||
| <tip> | ||
| Integration with this provider is possible with [zero configuration](/deploy/#zero-config-providers). | ||
| </tip> | ||
| 1. Login to the [AWS Amplify Hosting Console](https://console.aws.amazon.com/amplify/) | ||
| 2. Click on "Get Started" > Amplify Hosting (Host your web app) | ||
| 3. Select and authorize access to your Git repository provider and select the main branch | ||
| 4. Choose a name for your app, make sure build settings are auto-detected and optionally set requirement environment variables under the advanced section | ||
| 5. Optionally, select Enable SSR logging to enable server-side logging to your Amazon CloudWatch account | ||
| 6. Confirm configuration and click on "Save and Deploy" | ||
| ## Advanced Configuration | ||
| You can configure advanced options of this preset using `awsAmplify` option. | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| awsAmplify: { | ||
| // catchAllStaticFallback: true, | ||
| // imageOptimization: { path: "/_image", cacheControl: "public, max-age=3600, immutable" }, | ||
| // imageSettings: { ... }, | ||
| // runtime: "nodejs18.x", // default: "nodejs18.x" | "nodejs16.x" | "nodejs20.x" | ||
| } | ||
| }) | ||
| ``` | ||
| ### `amplify.yml` | ||
| You might need a custom `amplify.yml` file for advanced configuration. Here are two template examples: | ||
| <code-group> | ||
| ```yml [amplify.yml] | ||
| version: 1 | ||
| frontend: | ||
| phases: | ||
| preBuild: | ||
| commands: | ||
| - nvm use 18 && node --version | ||
| - corepack enable && npx --yes nypm install | ||
| build: | ||
| commands: | ||
| - pnpm build | ||
| artifacts: | ||
| baseDirectory: .amplify-hosting | ||
| files: | ||
| - "**/*" | ||
| ``` | ||
| ```yml [amplify.yml (monorepo)] | ||
| version: 1 | ||
| applications: | ||
| - frontend: | ||
| phases: | ||
| preBuild: | ||
| commands: | ||
| - nvm use 18 && node --version | ||
| - corepack enable && npx --yes nypm install | ||
| build: | ||
| commands: | ||
| - pnpm --filter website1 build | ||
| artifacts: | ||
| baseDirectory: apps/website1/.amplify-hosting | ||
| files: | ||
| - '**/*' | ||
| buildPath: / | ||
| appRoot: apps/website1 | ||
| ``` | ||
| </code-group> |
| # AWS Lambda | ||
| > Deploy Nitro apps to AWS Lambda. | ||
| **Preset:** `aws_lambda` | ||
| <read-more></read-more> | ||
| Nitro provides a built-in preset to generate output format compatible with [AWS Lambda](https://aws.amazon.com/lambda/). | ||
| The output entrypoint in `.output/server/index.mjs` is compatible with [AWS Lambda format](https://docs.aws.amazon.com/lex/latest/dg/lambda-input-response-format.html). | ||
| It can be used programmatically or as part of a deployment. | ||
| ```ts | ||
| import { handler } from './.output/server' | ||
| // Use programmatically | ||
| const { statusCode, headers, body } = handler({ rawPath: '/' }) | ||
| ``` | ||
| ## Inlining chunks | ||
| Nitro output, by default uses dynamic chunks for lazy loading code only when needed. However this sometimes can not be ideal for performance. (See discussions in [nitrojs/nitro#650](https://github.com/nitrojs/nitro/pull/650)). You can enabling chunk inlining behavior using [`inlineDynamicImports`](/config#inlinedynamicimports) config. | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| inlineDynamicImports: true | ||
| }); | ||
| ``` | ||
| ## Response streaming | ||
| <read-more></read-more> | ||
| In order to enable response streaming, enable `awsLambda.streaming` flag: | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| awsLambda: { | ||
| streaming: true | ||
| } | ||
| }); | ||
| ``` |
| # Azure | ||
| > Deploy Nitro apps to Azure Static Web apps or functions. | ||
| ## Azure static web apps | ||
| **Preset:** `azure-swa` | ||
| <read-more></read-more> | ||
| <note> | ||
| Integration with this provider is possible with [zero configuration](/deploy/#zero-config-providers). | ||
| </note> | ||
| [Azure Static Web Apps](https://azure.microsoft.com/en-us/products/app-service/static) are designed to be deployed continuously in a [GitHub Actions workflow](https://docs.microsoft.com/en-us/azure/static-web-apps/github-actions-workflow). By default, Nitro will detect this deployment environment and enable the `azure` preset. | ||
| ### Local preview | ||
| Install [Azure Functions Core Tools](https://docs.microsoft.com/en-us/azure/azure-functions/functions-run-local) if you want to test locally. | ||
| You can invoke a development environment to preview before deploying. | ||
| ```bash | ||
| NITRO_PRESET=azure npx nypm@latest build | ||
| npx @azure/static-web-apps-cli start .output/public --api-location .output/server | ||
| ``` | ||
| ### Configuration | ||
| Azure Static Web Apps are [configured](https://learn.microsoft.com/en-us/azure/static-web-apps/configuration) using the `staticwebapp.config.json` file. | ||
| Nitro automatically generates this configuration file whenever the application is built with the `azure` preset. | ||
| Nitro will automatically add the following properties based on the following criteria: | ||
| | Property | Criteria | Default | | ||
| | --- | --- | --- | | ||
| | **[platform.apiRuntime](https://learn.microsoft.com/en-us/azure/static-web-apps/configuration#platform)** | Will automatically set to `node:16` or `node:14` depending on your package configuration. | `node:16` | | ||
| | **[navigationFallback.rewrite](https://learn.microsoft.com/en-us/azure/static-web-apps/configuration#fallback-routes)** | Is always `/api/server` | `/api/server` | | ||
| | **[routes](https://learn.microsoft.com/en-us/azure/static-web-apps/configuration#routes)** | All prerendered routes are added. Additionally, if you do not have an `index.html` file an empty one is created for you for compatibility purposes and also requests to `/index.html` are redirected to the root directory which is handled by `/api/server`. | `[]` | | ||
| ### Custom configuration | ||
| You can alter the Nitro generated configuration using `azure.config` option. | ||
| Custom routes will be added and matched first. In the case of a conflict (determined if an object has the same route property), custom routes will override generated ones. | ||
| ### Deploy from CI/CD via GitHub actions | ||
| When you link your GitHub repository to Azure Static Web Apps, a workflow file is added to the repository. | ||
| When you are asked to select your framework, select custom and provide the following information: | ||
| | Input | Value | | ||
| | --- | --- | | ||
| | **app_location** | '/' | | ||
| | **api_location** | '.output/server' | | ||
| | **output_location** | '.output/public' | | ||
| If you miss this step, you can always find the build configuration section in your workflow and update the build configuration: | ||
| ```yaml [.github/workflows/azure-static-web-apps-<RANDOM_NAME>.yml] | ||
| ###### Repository/Build Configurations ###### | ||
| app_location: '/' | ||
| api_location: '.output/server' | ||
| output_location: '.output/public' | ||
| ###### End of Repository/Build Configurations ###### | ||
| ``` | ||
| That's it! Now Azure Static Web Apps will automatically deploy your Nitro-powered application on push. | ||
| If you are using runtimeConfig, you will likely want to configure the corresponding [environment variables on Azure](https://docs.microsoft.com/en-us/azure/static-web-apps/application-settings). |
| # Cleavr | ||
| > Deploy Nitro apps to Cleavr. | ||
| **Preset:** `cleavr` | ||
| <read-more></read-more> | ||
| <note> | ||
| Integration with this provider is possible with [zero configuration](/deploy/#zero-config-providers). | ||
| </note> | ||
| ## Set up your web app | ||
| In your project, set Nitro preset to `cleavr`. | ||
| ```js | ||
| export default { | ||
| nitro: { | ||
| preset: 'cleavr' | ||
| } | ||
| } | ||
| ``` | ||
| Push changes to your code repository. | ||
| **In your Cleavr panel:** | ||
| 1. Provision a new server | ||
| 2. Add a website, selecting **Nuxt 3** as the app type | ||
| 3. In web app > settings > Code Repo, point to your project's code repository | ||
| You're now all set to deploy your project! |
| # Cloudflare | ||
| > Deploy Nitro apps to Cloudflare. | ||
| ## Cloudflare Workers | ||
| **Preset:** `cloudflare_module` | ||
| <read-more></read-more> | ||
| <note> | ||
| Integration with this provider is possible with [zero configuration](/deploy#zero-config-providers) supporting [workers builds (beta)](https://developers.cloudflare.com/workers/ci-cd/builds/). | ||
| </note> | ||
| <important> | ||
| To use Workers with Static Assets, you need a Nitro compatibility date set to `2024-09-19` or later. | ||
| </important> | ||
| The following shows an example `nitro.config.ts` file for deploying a Nitro app to Cloudflare Workers. | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| compatibilityDate: "2024-09-19", | ||
| preset: "cloudflare_module", | ||
| cloudflare: { | ||
| deployConfig: true, | ||
| nodeCompat: true | ||
| } | ||
| }) | ||
| ``` | ||
| By setting `deployConfig: true`, Nitro will automatically generate a `wrangler.json` for you with the correct configuration. | ||
| If you need to add [Cloudflare Workers configuration](https://developers.cloudflare.com/workers/wrangler/configuration/), such as [bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/), you can either: | ||
| - Set these in your Nitro config under the `cloudflare: { wrangler : {} }`. This has the same type as `wrangler.json`. | ||
| - Provide your own `wrangler.json`. Nitro will merge your config with the appropriate settings, including pointing to the build output. | ||
| ### Local Preview | ||
| You can use [Wrangler](https://github.com/cloudflare/workers-sdk/tree/main/packages/wrangler) to preview your app locally: | ||
| <pm-run></pm-run> | ||
| <pm-x></pm-x> | ||
| ### Manual Deploy | ||
| After having built your application you can manually deploy it with Wrangler. | ||
| First make sure to be logged into your Cloudflare account: | ||
| <pm-x></pm-x> | ||
| Then you can deploy the application with: | ||
| <pm-x></pm-x> | ||
| ### Runtime Hooks | ||
| You can use [runtime hooks](/docs/plugins#nitro-runtime-hooks) below in order to extend [Worker handlers](https://developers.cloudflare.com/workers/runtime-apis/handlers/). | ||
| <read-more></read-more> | ||
| - [`cloudflare:scheduled`](https://developers.cloudflare.com/workers/runtime-apis/handlers/scheduled/) | ||
| - [`cloudflare:email`](https://developers.cloudflare.com/email-routing/email-workers/runtime-api/) | ||
| - [`cloudflare:queue`](https://developers.cloudflare.com/queues/configuration/javascript-apis/#consumer) | ||
| - [`cloudflare:tail`](https://developers.cloudflare.com/workers/runtime-apis/handlers/tail/) | ||
| - `cloudflare:trace` | ||
| ### Additional Exports | ||
| You can add a `exports.cloudflare.ts` file to your project root to export additional handlers or properties to the Cloudflare Worker entrypoint. | ||
| ```ts [exports.cloudflare.ts] | ||
| export class MyWorkflow extends WorkflowEntrypoint { | ||
| async run(event: WorkflowEvent, step: WorkflowStep) { | ||
| // ... | ||
| } | ||
| } | ||
| ``` | ||
| Nitro will automatically detect this file and include its exports in the final build. | ||
| <warning> | ||
| The `exports.cloudflare.ts` file must not have a default export. | ||
| </warning> | ||
| You can also customize the entrypoint file location using the `cloudflare.exports` option in your `nitro.config.ts`: | ||
| ```ts [nitro.config.ts] | ||
| export default defineConfig({ | ||
| cloudflare: { | ||
| exports: "custom-exports-entry.ts" | ||
| } | ||
| }) | ||
| ``` | ||
| ### Scheduled Tasks (Cron Triggers) | ||
| When using [Nitro tasks](/docs/tasks) with `scheduledTasks`, Nitro automatically generates [Cron Triggers](https://developers.cloudflare.com/workers/configuration/cron-triggers/) in the wrangler config at build time. | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| preset: "cloudflare_module", | ||
| experimental: { | ||
| tasks: true, | ||
| }, | ||
| scheduledTasks: { | ||
| "* * * * *": ["cms:update"], | ||
| "0 15 1 * *": ["db:cleanup"], | ||
| }, | ||
| cloudflare: { | ||
| deployConfig: true, | ||
| }, | ||
| }) | ||
| ``` | ||
| No manual Wrangler configuration is needed - Nitro handles it for you. | ||
| ## Cloudflare Pages | ||
| **Preset:** `cloudflare_pages` | ||
| <read-more></read-more> | ||
| <note> | ||
| Integration with this provider is possible with [zero configuration](/deploy#zero-config-providers). | ||
| </note> | ||
| <warning> | ||
| Cloudflare [Workers Module](#cloudflare-workers) is the new recommended preset for deployments. Please consider using the pages only if you need specific features. | ||
| </warning> | ||
| The following shows an example `nitro.config.ts` file for deploying a Nitro app to Cloudflare Pages. | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| preset: "cloudflare_pages", | ||
| cloudflare: { | ||
| deployConfig: true, | ||
| nodeCompat:true | ||
| } | ||
| }) | ||
| ``` | ||
| Nitro automatically generates a `_routes.json` file that controls which routes get served from files and which are served from the Worker script. The auto-generated routes file can be overridden with the config option `cloudflare.pages.routes` ([read more](https://developers.cloudflare.com/pages/platform/functions/routing/#functions-invocation-routes)). | ||
| ### Local Preview | ||
| You can use [Wrangler](https://github.com/cloudflare/workers-sdk/tree/main/packages/wrangler) to preview your app locally: | ||
| <pm-run></pm-run> | ||
| <pm-x></pm-x> | ||
| ### Manual Deploy | ||
| After having built your application you can manually deploy it with Wrangler, in order to do so first make sure to be | ||
| logged into your Cloudflare account: | ||
| <pm-x></pm-x> | ||
| Then you can deploy the application with: | ||
| <pm-x></pm-x> | ||
| ## Deploy within CI/CD using GitHub Actions | ||
| Regardless on whether you're using Cloudflare Pages or Cloudflare Workers, you can use the [Wrangler GitHub actions](https://github.com/marketplace/actions/deploy-to-cloudflare-workers-with-wrangler) to deploy your application. | ||
| <note> | ||
| **Note:** Remember to [instruct Nitro to use the correct preset](/deploy#changing-the-deployment-preset) (note that this is necessary for all presets including the `cloudflare_pages` one). | ||
| </note> | ||
| ## Environment Variables | ||
| Nitro allows you to universally access environment variables using `process.env` or `import.meta.env` or the runtime config. | ||
| <note> | ||
| Make sure to only access environment variables **within the event lifecycle** and not in global contexts since Cloudflare only makes them available during the request lifecycle and not before. | ||
| </note> | ||
| **Example:** If you have set the `SECRET` and `NITRO_HELLO_THERE` environment variables set you can access them in the following way: | ||
| ```ts | ||
| import { defineHandler } from "nitro"; | ||
| import { useRuntimeConfig } from "nitro/runtime-config"; | ||
| console.log(process.env.SECRET) // note that this is in the global scope! so it doesn't actually work and the variable is undefined! | ||
| export default defineHandler((event) => { | ||
| // note that all the below are valid ways of accessing the above mentioned variables | ||
| useRuntimeConfig().helloThere | ||
| useRuntimeConfig().secret | ||
| process.env.NITRO_HELLO_THERE | ||
| import.meta.env.SECRET | ||
| }); | ||
| ``` | ||
| ### Specify Variables in Development Mode | ||
| For development, you can use a `.env` or `.env.local` file to specify environment variables: | ||
| ```ini | ||
| NITRO_HELLO_THERE="captain" | ||
| SECRET="top-secret" | ||
| ``` | ||
| <note> | ||
| **Note:** Make sure you add `.env` and `.env.local` to the `.gitignore` file so that you don't commit it as it can contain sensitive information. | ||
| </note> | ||
| ### Specify Variables for local previews | ||
| After build, when you try out your project locally with `wrangler dev` or `wrangler pages dev`, in order to have access to environment variables you will need to specify the in a `.dev.vars` file in the root of your project (as presented in the [Pages](https://developers.cloudflare.com/pages/functions/bindings/#interact-with-your-environment-variables-locally) and [Workers](https://developers.cloudflare.com/workers/configuration/environment-variables/#interact-with-environment-variables-locally) documentation). | ||
| If you are using a `.env` or `.env.local` file while developing, your `.dev.vars` should be identical to it. | ||
| <note> | ||
| **Note:** Make sure you add `.dev.vars` to the `.gitignore` file so that you don't commit it as it can contain sensitive information. | ||
| </note> | ||
| ### Specify Variables for Production | ||
| For production, use the Cloudflare dashboard or the [`wrangler secret`](https://developers.cloudflare.com/workers/wrangler/commands/#secret) command to set environment variables and secrets. | ||
| ### Specify Variables using `wrangler.toml`/`wrangler.json` | ||
| You can specify a custom `wrangler.toml`/`wrangler.json` file and define vars inside. | ||
| <warning> | ||
| Note that this isn't recommend for sensitive data like secrets. | ||
| </warning> | ||
| **Example:** | ||
| <code-group> | ||
| ```ini [wrangler.toml] | ||
| # Shared | ||
| [vars] | ||
| NITRO_HELLO_THERE="general" | ||
| SECRET="secret" | ||
| # Override values for `--env production` usage | ||
| [env.production.vars] | ||
| NITRO_HELLO_THERE="captain" | ||
| SECRET="top-secret" | ||
| ``` | ||
| ```json [wrangler.json] | ||
| { | ||
| "vars": { | ||
| "NITRO_HELLO_THERE": "general", | ||
| "SECRET": "secret" | ||
| }, | ||
| "env": { | ||
| "production": { | ||
| "vars": { | ||
| "NITRO_HELLO_THERE": "captain", | ||
| "SECRET": "top-secret" | ||
| } | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
| </code-group> | ||
| ## Direct access to Cloudflare bindings | ||
| Bindings are what allows you to interact with resources from the Cloudflare platform, examples of such resources are key-value data storages ([KVs](https://developers.cloudflare.com/kv/)) and serverless SQL databases ([D1s](https://developers.cloudflare.com/d1/)). | ||
| <read-more> | ||
| For more details on Bindings and how to use them please refer to the Cloudflare [Pages](https://developers.cloudflare.com/pages/functions/bindings/) and [Workers](https://developers.cloudflare.com/workers/configuration/bindings/#bindings) documentation. | ||
| </read-more> | ||
| > [!TIP] | ||
| > Nitro provides high level API to interact with primitives such as [KV Storage](/docs/storage) and [Database](/docs/database) and you are highly recommended to prefer using them instead of directly depending on low-level APIs for usage stability. | ||
| <read-more></read-more> | ||
| <read-more></read-more> | ||
| In runtime, you can access bindings from the request event via `event.req.runtime.cloudflare.env`. This is for example how you can access a D1 binding: | ||
| <warning> | ||
| **Nitro v3 Breaking Change:** The `event.context.cloudflare.env` pattern from Nitro v2 no longer works in production. Use `event.req.runtime.cloudflare.env` instead. The old pattern may still appear to work in local dev (via the Wrangler proxy plugin) but will be `undefined` in production deployments. | ||
| </warning> | ||
| ```ts | ||
| import { defineHandler } from "nitro"; | ||
| defineHandler(async (event) => { | ||
| // Nitro v3: access Cloudflare bindings via event.req.runtime.cloudflare.env | ||
| const { env } = event.req.runtime.cloudflare | ||
| const stmt = await env.MY_D1.prepare('SELECT id FROM table') | ||
| const { results } = await stmt.all() | ||
| }) | ||
| ``` | ||
| ### Access to the bindings in local dev | ||
| To access bindings in dev mode, we first define them. You can do this in a `wrangler.jsonc`/`wrangler.json`/`wrangler.toml` file | ||
| For example, to define a variable and a KV namespace in `wrangler.toml`: | ||
| <code-group> | ||
| ```ini [wrangler.toml] | ||
| [vars] | ||
| MY_VARIABLE="my-value" | ||
| [[kv_namespaces]] | ||
| binding = "MY_KV" | ||
| id = "xxx" | ||
| ``` | ||
| ```json [wrangler.json] | ||
| { | ||
| "vars": { | ||
| "MY_VARIABLE": "my-value", | ||
| }, | ||
| "kv_namespaces": [ | ||
| { | ||
| "binding": "MY_KV", | ||
| "id": "xxx" | ||
| } | ||
| ] | ||
| } | ||
| ``` | ||
| </code-group> | ||
| Next we install the required `wrangler` package (if not already installed): | ||
| <pm-install></pm-install> | ||
| From this moment, when running | ||
| <pm-run></pm-run> | ||
| you will be able to access the `MY_VARIABLE` and `MY_KV` from the request event just as illustrated above. | ||
| #### Wrangler environments | ||
| If you have multiple Wrangler environments, you can specify which Wrangler environment to use during Cloudflare dev emulation: | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| preset: 'cloudflare_module', | ||
| cloudflare: { | ||
| dev: { | ||
| environment: 'preview' | ||
| } | ||
| } | ||
| }) | ||
| ``` |
| # Deno Deploy | ||
| > Deploy Nitro apps to [Deno Deploy](https://deno.com/deploy). | ||
| **Preset:** `deno_deploy` | ||
| <read-more></read-more> | ||
| ## Deploy with the CLI | ||
| You can use [deployctl](https://deno.com/deploy/docs/deployctl) to deploy your app. | ||
| Login to [Deno Deploy](https://dash.deno.com/account#access-tokens) to obtain a `DENO_DEPLOY_TOKEN` access token, and set it as an environment variable. | ||
| ```bash | ||
| # Build with the deno_deploy NITRO preset | ||
| NITRO_PRESET=deno_deploy npm run build | ||
| # Make sure to run the deployctl command from the output directory | ||
| cd .output | ||
| deployctl deploy --project=my-project server/index.ts | ||
| ``` | ||
| ## Deploy within CI/CD using GitHub actions | ||
| You just need to include the deployctl GitHub Action as a step in your workflow. | ||
| You do not need to set up any secrets for this to work. You do need to link your GitHub repository to your Deno Deploy project and choose the "GitHub Actions" deployment mode. You can do this in your project settings on [Deno Deploy](https://dash.deno.com). | ||
| Create the following workflow file in your `.github/workflows` directory: | ||
| ```yaml [.github/workflows/deno_deploy.yml] | ||
| name: deno-deploy | ||
| on: | ||
| push: | ||
| branches: | ||
| - main | ||
| pull_request: | ||
| branches: | ||
| - main | ||
| jobs: | ||
| deploy: | ||
| steps: | ||
| - uses: actions/checkout@v5 | ||
| - run: corepack enable | ||
| - uses: actions/setup-node@v6 | ||
| with: | ||
| node-version: 18 | ||
| cache: pnpm | ||
| - run: pnpm install | ||
| - run: pnpm build | ||
| env: | ||
| NITRO_PRESET: deno_deploy | ||
| - name: Deploy to Deno Deploy | ||
| uses: denoland/deployctl@v1 | ||
| with: | ||
| project: my-project | ||
| entrypoint: server/index.ts | ||
| root: .output | ||
| ``` | ||
| ## Deno runtime | ||
| <read-more></read-more> |
| # DigitalOcean | ||
| > Deploy Nitro apps to DigitalOcean. | ||
| **Preset:** `digital_ocean` | ||
| <read-more></read-more> | ||
| ## Set up application | ||
| 1. Create a new Digital Ocean app following the [guide](https://docs.digitalocean.com/products/app-platform/how-to/create-apps/). | ||
| 2. Next, you'll need to configure environment variables. In your app settings, ensure the following app-level environment variables are set: | ||
| ```bash | ||
| NITRO_PRESET=digital_ocean | ||
| ``` | ||
| [More information](https://docs.digitalocean.com/products/app-platform/how-to/use-environment-variables/). | ||
| 3. You will need to ensure you set an `engines.node` field in your app's `package.json` to ensure Digital Ocean uses a supported version of Node.js: | ||
| ```json | ||
| { | ||
| "engines": { | ||
| "node": "20.x" | ||
| } | ||
| } | ||
| ``` | ||
| [See more information](https://docs.digitalocean.com/products/app-platform/languages-frameworks/nodejs/#node-version). | ||
| 4. You'll also need to add a run command so Digital Ocean knows what command to run after a build. You can do so by adding a start script to your `package.json`: | ||
| ```json | ||
| { | ||
| "scripts": { | ||
| "start": "node .output/server/index.mjs" | ||
| } | ||
| } | ||
| ``` | ||
| 5. Finally, you'll need to add this start script to your Digital Ocean app's run command. Go to `Components > Settings > Commands`, click "Edit", then add `npm run start` | ||
| Your app should be live at a Digital Ocean generated URL and you can now follow [the rest of the Digital Ocean deployment guide](https://docs.digitalocean.com/products/app-platform/how-to/manage-deployments/). |
| # Firebase | ||
| > Deploy Nitro apps to Firebase. | ||
| <note> | ||
| You will need to be on the [**Blaze plan**](https://firebase.google.com/pricing) (Pay as you go) to get started. | ||
| </note> | ||
| ## Firebase app hosting | ||
| Preset: `firebase_app_hosting` | ||
| <read-more></read-more> | ||
| <tip> | ||
| You can integrate with this provider using [zero configuration](/deploy/#zero-config-providers). | ||
| </tip> | ||
| ### Project setup | ||
| 1. Go to the Firebase [console](https://console.firebase.google.com/) and set up a new project. | ||
| 2. Select **Build > App Hosting** from the sidebar. - You may need to upgrade your billing plan at this step. | ||
| - Click **Get Started**. - Choose a region. | ||
| - Import a GitHub repository (you’ll need to link your GitHub account). | ||
| - Configure deployment settings (project root directory and branch), and enable automatic rollouts. | ||
| - Choose a unique ID for your backend. | ||
| - Click Finish & Deploy to create your first rollout. | ||
| When you deploy with Firebase App Hosting, the App Hosting preset will be run automatically at build time. |
| # Flightcontrol | ||
| > Deploy Nitro apps to AWS via Flightcontrol. | ||
| **Preset:** `flightcontrol` | ||
| <read-more></read-more> | ||
| ## Set Up your flightcontrol account | ||
| On a high level, the steps you will need to follow to deploy a project for the first time are: | ||
| 1. Create an account at [Flightcontrol](https://app.flightcontrol.dev/signup?ref=nitro) | ||
| 2. Create an account at [AWS](https://portal.aws.amazon.com/billing/signup) (if you don't already have one) | ||
| 3. Link your AWS account to the Flightcontrol | ||
| 4. Authorize the Flightcontrol GitHub App to access your chosen repositories, public or private. | ||
| 5. Create a Flightcontrol project with configuration via the Dashboard or with configuration via `flightcontrol.json`. | ||
| ### Create a project with configuration via the dashboard | ||
| 1. Create a Flightcontrol project from the Dashboard. Select a repository for the source. | ||
| 2. Select the `GUI` config type. | ||
| 3. Select the Nuxt preset. This preset will also work for any Nitro-based applications. | ||
| 4. Select your preferred AWS server size. | ||
| 5. Submit the new project form. | ||
| ### Create a project with configuration via `flightcontrol.json` | ||
| 1. Create a Flightcontrol project from your dashboard. Select a repository for the source. | ||
| 2. Select the `flightcontrol.json` config type. | ||
| 3. Add a new file at the root of your repository called `flightcontrol.json`. Here is an example configuration that creates an AWS fargate service for your app: | ||
| ```json [flightcontrol.json] | ||
| { | ||
| "$schema": "https://app.flightcontrol.dev/schema.json", | ||
| "environments": [ | ||
| { | ||
| "id": "production", | ||
| "name": "Production", | ||
| "region": "us-west-2", | ||
| "source": { | ||
| "branch": "main" | ||
| }, | ||
| "services": [ | ||
| { | ||
| "id": "nitro", | ||
| "buildType": "nixpacks", | ||
| "name": "My Nitro site", | ||
| "type": "fargate", | ||
| "domain": "www.yourdomain.com", | ||
| "outputDirectory": ".output", | ||
| "startCommand": "node .output/server/index.mjs", | ||
| "cpu": 0.25, | ||
| "memory": 0.5 | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| } | ||
| ``` | ||
| 4. Submit the new project form. | ||
| <read-more> | ||
| Learn more about Flightcontrol's [configuration](https://www.flightcontrol.dev/docs?ref=nitro). | ||
| </read-more> |
| # Genezio | ||
| > Deploy Nitro apps to Genezio. | ||
| **Preset:** `genezio` | ||
| <read-more></read-more> | ||
| > [!IMPORTANT] | ||
| > 🚧 This preset is currently experimental. | ||
| ## 1. Project Setup | ||
| Create `genezio.yaml` file: | ||
| ```yaml | ||
| # The name of the project. | ||
| name: nitro-app | ||
| # The version of the Genezio YAML configuration to parse. | ||
| yamlVersion: 2 | ||
| backend: | ||
| # The root directory of the backend. | ||
| path: .output/ | ||
| # Information about the backend's programming language. | ||
| language: | ||
| # The name of the programming language. | ||
| name: js | ||
| # The package manager used by the backend. | ||
| packageManager: npm | ||
| # Information about the backend's functions. | ||
| functions: | ||
| # The name (label) of the function. | ||
| - name: nitroServer | ||
| # The path to the function's code. | ||
| path: server/ | ||
| # The name of the function handler | ||
| handler: handler | ||
| # The entry point for the function. | ||
| entry: index.mjs | ||
| ``` | ||
| <read-more> | ||
| To further customize the file to your needs, you can consult the | ||
| [official documentation](https://genezio.com/docs/project-structure/genezio-configuration-file/). | ||
| </read-more> | ||
| ## 2. Deploy your project | ||
| Build with the genezio nitro preset: | ||
| ```bash | ||
| NITRO_PRESET=genezio npm run build | ||
| ``` | ||
| Deploy with [`genezio`](https://npmjs.com/package/genezio) cli: | ||
| <pm-x></pm-x> | ||
| <read-more> | ||
| To set environment viarables, please check out [Genezio - Environment Variables](https://genezio.com/docs/project-structure/backend-environment-variables). | ||
| </read-more> | ||
| ## 3. Monitor your project | ||
| You can monitor and manage your application through the [Genezio App Dashboard](https://app.genez.io/dashboard). The dashboard URL, also provided after deployment, allows you to access comprehensive views of your project's status and logs. |
| # GitHub Pages | ||
| > Deploy Nitro apps to GitHub Pages. | ||
| **Preset:** `github_pages` | ||
| <read-more></read-more> | ||
| ## Setup | ||
| Follow the steps to [create a GitHub Pages site](https://docs.github.com/en/pages/getting-started-with-github-pages/creating-a-github-pages-site). | ||
| ## Deployment | ||
| Here is an example GitHub Actions workflow to deploy your site to GitHub Pages using the `github_pages` preset: | ||
| ```yaml [.github/workflows/deploy.yml] | ||
| # https://github.com/actions/deploy-pages#usage | ||
| name: Deploy to GitHub Pages | ||
| on: | ||
| workflow_dispatch: | ||
| push: | ||
| branches: | ||
| - main | ||
| jobs: | ||
| build: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v5 | ||
| - run: corepack enable | ||
| - uses: actions/setup-node@v6 | ||
| with: | ||
| node-version: "18" | ||
| - run: npx nypm install | ||
| - run: npm run build | ||
| env: | ||
| NITRO_PRESET: github_pages | ||
| - name: Upload artifact | ||
| uses: actions/upload-pages-artifact@v1 | ||
| with: | ||
| path: ./.output/public | ||
| # Deployment job | ||
| deploy: | ||
| # Add a dependency to the build job | ||
| needs: build | ||
| # Grant GITHUB_TOKEN the permissions required to make a Pages deployment | ||
| permissions: | ||
| pages: write # to deploy to Pages | ||
| id-token: write # to verify the deployment originates from an appropriate source | ||
| # Deploy to the github_pages environment | ||
| environment: | ||
| name: github-pages | ||
| url: ${{ steps.deployment.outputs.page_url }} | ||
| # Specify runner + deployment step | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Deploy to GitHub Pages | ||
| id: deployment | ||
| uses: actions/deploy-pages@v1 | ||
| ``` |
| # GitLab Pages | ||
| > Deploy Nitro apps to GitLab Pages. | ||
| **Preset:** `gitlab_pages` | ||
| <read-more></read-more> | ||
| ## Setup | ||
| Follow the steps to [create a GitLab Pages site](https://docs.gitlab.com/ee/user/project/pages/#getting-started). | ||
| ## Deployment | ||
| 1. Here is an example GitLab Pages workflow to deploy your site to GitLab Pages: | ||
| ```yaml [.gitlab-ci.yml] | ||
| image: node:lts | ||
| before_script: | ||
| - npx nypm install | ||
| pages: | ||
| cache: | ||
| paths: | ||
| - node_modules/ | ||
| variables: | ||
| NITRO_PRESET: gitlab_pages | ||
| script: | ||
| - npm run build | ||
| artifacts: | ||
| paths: | ||
| - .output/public | ||
| publish: .output/public | ||
| rules: | ||
| # This ensures that only pushes to the default branch | ||
| # will trigger a pages deploy | ||
| - if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH | ||
| ``` |
| # Heroku | ||
| > Deploy Nitro apps to Heroku. | ||
| **Preset:** `heroku` | ||
| <read-more></read-more> | ||
| ## Using the heroku CLI | ||
| 1. Create a new Heroku app. | ||
| ```bash | ||
| heroku create myapp | ||
| ``` | ||
| 2. Configure Heroku to use the nodejs buildpack. | ||
| ```bash | ||
| heroku buildpacks:set heroku/nodejs | ||
| ``` | ||
| 3. Configure your app. | ||
| ```bash | ||
| heroku config:set NITRO_PRESET=heroku | ||
| ``` | ||
| 4. Ensure you have `start` and `build` commands in your `package.json` file. | ||
| ```json5 | ||
| "scripts": { | ||
| "build": "nitro build", // or `nuxt build` if using nuxt | ||
| "start": "node .output/server/index.mjs" | ||
| } | ||
| ``` | ||
| ## With nginx | ||
| 1. Add the heroku Nginx buildpack [here](https://github.com/heroku/heroku-buildpack-nginx.git) | ||
| 2. Change to the 'node' preset in your `nitro.config` | ||
| ```json5 | ||
| "nitro": { | ||
| "preset":"node", | ||
| } | ||
| ``` | ||
| 3. From the **Existing app** section of buildpack doc, 2 key steps are required to get things running | ||
| Step 1: Listen on a socket at 'tmp/nginx.socket' | ||
| Step 2: Create a file '/tmp/app-initialized' when your app is ready to accept connections | ||
| 4. Create custom app runner, eg: apprunner.mjs at the root of the project (or any other preferred location), in this file, create a server, using the listener generated by the node preset, then listen on the socket as detailed in the buildpack doc | ||
| ```ts | ||
| import { createServer } from 'node:http' | ||
| import { listener } from './.output/server/index.mjs' | ||
| const server = createServer(listener) | ||
| server.listen('/tmp/nginx.socket') //following the buildpack doc | ||
| ``` | ||
| 5. To create the 'tmp/app-initialized' file, use a nitro plugin, create file 'initServer.ts' at the root of the project (or any other preferred location) | ||
| ```ts | ||
| import fs from "fs" | ||
| export default definePlugin((nitroApp) => { | ||
| if((process.env.NODE_ENV || 'development') != 'development') { | ||
| fs.openSync('/tmp/app-initialized', 'w') | ||
| } | ||
| }) | ||
| ``` | ||
| 6. Finally, create file 'Procfile' at the root of the project, with the Procfile, we tell heroku to start nginx and use the custom apprunner.mjs to start the server | ||
| web: bin/start-nginx node apprunner.mjs | ||
| 7. Bonus: create file 'config/nginx.conf.erb' to customize your nginx config. With the node preset, by default, static files handlers will not be generated, you can use nginx to server static files, just add the right location rule to the server block(s), or, force the node preset to generate handlers for the static files by setting serveStatic to true. | ||
| # IIS | ||
| > Deploy Nitro apps to IIS. | ||
| ## Using [IISnode](https://github.com/Azure/iisnode) | ||
| **Preset:** `iis_node` | ||
| 1. Install the latest LTS version of [Node.js](https://nodejs.org/en/) on your Windows Server. | ||
| 2. Install [IISnode](https://github.com/azure/iisnode/releases) | ||
| 3. Install [IIS `URLRewrite` Module](https://www.iis.net/downloads/microsoft/url-rewrite). | ||
| 4. In IIS, add `.mjs` as a new mime type and set its content type to `application/javascript`. | ||
| 5. Deploy the contents of your `.output` folder to your website in IIS. | ||
| ## Using IIS handler | ||
| **Preset:** `iis_handler` | ||
| You can use IIS http handler directly. | ||
| 1. Install the latest LTS version of [Node.js](https://nodejs.org/en/) on your Windows Server. | ||
| 2. Install [IIS `HttpPlatformHandler` Module](https://www.iis.net/downloads/microsoft/httpplatformhandler) | ||
| 3. Copy your `.output` directory into the Windows Server, and create a website on IIS pointing to that exact directory. | ||
| ## IIS config options | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| // IIS options default | ||
| iis: { | ||
| // merges in a pre-existing web.config file to the nitro default file | ||
| mergeConfig: true, | ||
| // overrides the default nitro web.config file all together | ||
| overrideConfig: false, | ||
| }, | ||
| }); | ||
| ``` |
| # Koyeb | ||
| > Deploy Nitro apps to Koyeb. | ||
| **Preset:** `koyeb` | ||
| <read-more></read-more> | ||
| ## Using the control panel | ||
| 1. In the [Koyeb control panel](https://app.koyeb.com/), click **Create App**. | ||
| 2. Choose **GitHub** as your deployment method. | ||
| 3. Choose the GitHub **repository** and **branch** containing your application code. | ||
| 4. Name your Service. | ||
| 5. If you did not add a `start` command to your `package.json` file, under the **Build and deployment settings**, toggle the override switch associated with the run command field. In the **Run command** field, enter: | ||
| ```bash | ||
| node .output/server/index.mjs` | ||
| ``` | ||
| 6. In the **Advanced** section, click **Add Variable** and add a `NITRO_PRESET` variable set to `koyeb`. | ||
| 7. Name the App. | ||
| 8. Click the **Deploy** button. | ||
| ## Using the Koyeb CLI | ||
| 1. Follow the instructions targeting your operating system to [install the Koyeb CLI client](https://www.koyeb.com/docs/cli/installation) with an installer. Alternatively, visit the [releases page on GitHub](https://github.com/koyeb/koyeb-cli/releases) to directly download required files. | ||
| 2. Create a Koyeb API access token by visiting the [API settings for your organization](https://app.koyeb.com/settings/api) in the Koyeb control panel. | ||
| 3. Log into your account with the Koyeb CLI by typing: | ||
| ```bash | ||
| koyeb login | ||
| ``` | ||
| Paste your API credentials when prompted. | ||
| 4. Deploy your Nitro application from a GitHub repository with the following command. Be sure to substitute your own values for `<APPLICATION_NAME>`, `<YOUR_GITHUB_USERNAME>`, and `<YOUR_REPOSITORY_NAME>`: | ||
| ```bash | ||
| koyeb app init <APPLICATION_NAME> \ | ||
| --git github.com/<YOUR_GITHUB_USERNAME>/<YOUR_REPOSITORY_NAME> \ | ||
| --git-branch main \ | ||
| --git-run-command "node .output/server/index.mjs" \ | ||
| --ports 3000:http \ | ||
| --routes /:3000 \ | ||
| --env PORT=3000 \ | ||
| --env NITRO_PRESET=koyeb | ||
| ``` | ||
| ## Using a docker container | ||
| 1. Create a `.dockerignore` file in the root of your project and add the following lines: | ||
| ``` | ||
| Dockerfile | ||
| .dockerignore | ||
| node_modules | ||
| npm-debug.log | ||
| .nitro | ||
| .output | ||
| .git | ||
| dist | ||
| README.md | ||
| ``` | ||
| 2. Add a `Dockerfile` to the root of your project: | ||
| ``` | ||
| FROM node:18-alpine AS base | ||
| FROM base AS deps | ||
| RUN apk add --no-cache libc6-compat | ||
| WORKDIR /app | ||
| COPY package.json package-lock.json ./ | ||
| RUN npm ci | ||
| FROM base AS builder | ||
| WORKDIR /app | ||
| COPY --from=deps /app/node_modules ./node_modules | ||
| COPY . . | ||
| RUN npm run build && npm cache clean --force | ||
| FROM base AS runner | ||
| WORKDIR /app | ||
| RUN addgroup --system --gid 1001 nodejs | ||
| RUN adduser --system --uid 1001 nitro | ||
| COPY --from=builder /app . | ||
| USER nitro | ||
| EXPOSE 3000 | ||
| ENV PORT 3000 | ||
| CMD ["npm", "run", "start"] | ||
| ``` | ||
| The Dockerfile above provides the minimum requirements to run the Nitro application. You can easily extend it depending on your needs. | ||
| You will then need to push your Docker image to a registry. You can use [Docker Hub](https://hub.docker.com/) or [GitHub Container Registry](https://docs.github.com/en/packages/guides/about-github-container-registry) for example. | ||
| In the Koyeb control panel, use the image and the tag field to specify the image you want to deploy. | ||
| You can also use the [Koyeb CLI](https://www.koyeb.com/docs/build-and-deploy/cli/installation) | ||
| Refer to the Koyeb [Docker documentation](https://www.koyeb.com/docs/build-and-deploy/prebuilt-docker-images) for more information. |
| # Netlify | ||
| > Deploy Nitro apps to Netlify functions or edge. | ||
| **Preset:** `netlify` | ||
| <read-more></read-more> | ||
| <note> | ||
| Integration with this provider is possible with [zero configuration](/deploy/#zero-config-providers). | ||
| </note> | ||
| Normally, the deployment to Netlify does not require any configuration. | ||
| Nitro will auto-detect that you are in a [Netlify](https://www.netlify.com) build environment and build the correct version of your server. | ||
| For new sites, Netlify will detect that you are using Nitro and set the publish directory to `dist` and build command to `npm run build`. | ||
| If you are upgrading an existing site you should check these and update them if needed. | ||
| If you want to add custom redirects, you can do so with [`routeRules`](/config#routerules) or by adding a [`_redirects`](https://docs.netlify.com/routing/redirects/#syntax-for-the-redirects-file) file to your `public` directory. | ||
| For deployment, just push to your git repository [as you would normally do for Netlify](https://docs.netlify.com/configure-builds/get-started/). | ||
| <note> | ||
| Make sure the publish directory is set to `dist` when creating a new project. | ||
| </note> | ||
| ## Netlify edge functions | ||
| **Preset:** `netlify_edge` | ||
| Netlify Edge Functions use Deno and the powerful V8 JavaScript runtime to let you run globally distributed functions for the fastest possible response times. | ||
| <read-more></read-more> | ||
| Nitro output can directly run the server at the edge. Closer to your users. | ||
| <note> | ||
| Make sure the publish directory is set to `dist` when creating a new project. | ||
| </note> | ||
| ## Custom deploy configuration | ||
| You can provide additional deploy configuration using the `netlify` key inside `nitro.config`. It will be merged with built-in auto-generated config. Currently the only supported value is `images.remote_images`, for [configuring Netlify Image CDN](https://docs.netlify.com/image-cdn/create-integration/). |
| # Platform.sh | ||
| > Deploy Nitro apps to platform.sh | ||
| **Preset:** `platform_sh` | ||
| <read-more></read-more> | ||
| ## Setup | ||
| First, create a new project on platform.sh and link it to the repository you want to auto-deploy with. | ||
| Then in repository create `.platform.app.yaml` file: | ||
| ```yaml [.platform.app.yaml] | ||
| name: nitro-app | ||
| type: 'nodejs:20' | ||
| disk: 128 | ||
| web: | ||
| commands: | ||
| start: "node .output/server/index.mjs" | ||
| build: | ||
| flavor: none | ||
| hooks: | ||
| build: | | ||
| corepack enable | ||
| npx nypm install | ||
| NITRO_PRESET=platform_sh npm run build | ||
| mounts: | ||
| '.data': | ||
| source: local | ||
| source_path: .data | ||
| ``` | ||
| <read-more></read-more> | ||
| <read-more></read-more> |
| # Render.com | ||
| > Deploy Nitro apps to Render.com. | ||
| **Preset:** `render_com` | ||
| <read-more></read-more> | ||
| ## Set up application | ||
| 1. [Create a new Web Service](https://dashboard.render.com/select-repo?type=web) and select the repository that contains your code. | ||
| 2. Ensure the 'Node' environment is selected. | ||
| 3. Update the start command to `node .output/server/index.mjs` | ||
| 4. Click 'Advanced' and add an environment variable with `NITRO_PRESET` set to `render_com`. You may also need to add a `NODE_VERSION` environment variable set to `20` for the build to succeed ([docs](https://render.com/docs/node-version)). | ||
| 5. Click 'Create Web Service'. | ||
| ## Infrastructure as Code (IaC) | ||
| 1. Create a file called `render.yaml` with following content at the root of your repository. | ||
| This file followed by [Infrastructure as Code](https://render.com/docs/infrastructure-as-code) on Render | ||
| ```yaml | ||
| services: | ||
| - type: web | ||
| name: <PROJECTNAME> | ||
| env: node | ||
| branch: main | ||
| startCommand: node .output/server/index.mjs | ||
| buildCommand: npx nypm install && npm run build | ||
| envVars: | ||
| - key: NITRO_PRESET | ||
| value: render_com | ||
| ``` | ||
| 1. [Create a new Blueprint Instance](https://dashboard.render.com/select-repo?type=blueprint) and select the repository containing your `render.yaml` file. | ||
| You should be good to go! |
| # StormKit | ||
| > Deploy Nitro apps to StormKit. | ||
| **Preset:** `stormkit` | ||
| <read-more></read-more> | ||
| <note> | ||
| Integration with [Stormkit](https://www.stormkit.io/) is possible with [zero configuration](/deploy#zero-config-providers). | ||
| </note> | ||
| ## Setup | ||
| Follow the steps to [create a new app](https://app.stormkit.io/apps/new) on Stormkit. | ||
|  | ||
| ## Deployment | ||
| By default, Stormkit will deploy your apps automatically when you push changes to your main branch. But to trigger a manual deploy (for example, you might do this for the very first deployment), you may click `Deploy now`. | ||
|  |
| # Vercel | ||
| > Deploy Nitro apps to Vercel. | ||
| **Preset:** `vercel` | ||
| <read-more></read-more> | ||
| <note> | ||
| Integration with this provider is possible with [zero configuration](/deploy/#zero-config-providers). | ||
| </note> | ||
| ## Getting started | ||
| Deploying to Vercel comes with the following features: | ||
| - [Preview deployments](https://vercel.com/docs/deployments/environments) | ||
| - [Fluid compute](https://vercel.com/docs/fluid-compute) | ||
| - [Observability](https://vercel.com/docs/observability) | ||
| - [Vercel Firewall](https://vercel.com/docs/vercel-firewall) | ||
| And much more. Learn more in [the Vercel documentation](https://vercel.com/docs). | ||
| ### Deploy with Git | ||
| Vercel supports Nitro with zero-configuration. [Deploy Nitro to Vercel now](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fvercel%2Ftree%2Fmain%2Fexamples%2Fnitro). | ||
| ## API routes | ||
| Nitro `/api` directory isn't compatible with Vercel. Instead, you should use: | ||
| - `routes/api/` for standalone usage | ||
| ## Bun runtime | ||
| <read-more></read-more> | ||
| You can use [Bun](https://bun.com) instead of Node.js by specifying the runtime using the `vercel.functions` key inside `nitro.config`: | ||
| ```ts [nitro.config.ts] | ||
| export default defineNitroConfig({ | ||
| vercel: { | ||
| functions: { | ||
| runtime: "bun1.x" | ||
| } | ||
| } | ||
| }) | ||
| ``` | ||
| Alternatively, Nitro also detects Bun automatically if you specify a `bunVersion` property in your `vercel.json`: | ||
| ```json [vercel.json] | ||
| { | ||
| "$schema": "https://openapi.vercel.sh/vercel.json", | ||
| "bunVersion": "1.x" | ||
| } | ||
| ``` | ||
| ## Proxy route rules | ||
| Nitro automatically optimizes `proxy` route rules on Vercel by generating [CDN-level rewrites](https://vercel.com/docs/rewrites) at build time. This means matching requests are proxied at the edge without invoking a serverless function, reducing latency and cost. | ||
| ```ts [nitro.config.ts] | ||
| export default defineNitroConfig({ | ||
| routeRules: { | ||
| // Proxied at CDN level — no function invocation | ||
| "/api/**": { | ||
| proxy: "https://api.example.com/**", | ||
| }, | ||
| }, | ||
| }); | ||
| ``` | ||
| ### When CDN rewrites apply | ||
| A proxy rule is offloaded to a Vercel CDN rewrite when **all** of the following are true: | ||
| - The target is an **external URL** (starts with `http://` or `https://`). | ||
| - No advanced `ProxyOptions` are set on the rule. | ||
| ### Fallback to runtime proxy | ||
| When the proxy rule uses any of the following `ProxyOptions`, Nitro keeps it as a runtime proxy handled by the serverless function: | ||
| - `headers` — custom headers on the outgoing request to the upstream | ||
| - `forwardHeaders` / `filterHeaders` — header filtering | ||
| - `fetchOptions` — custom fetch options | ||
| - `cookieDomainRewrite` / `cookiePathRewrite` — cookie manipulation | ||
| - `onResponse` — response callback | ||
| <note> | ||
| Response headers defined on the route rule via the `headers` option are still applied to CDN-level rewrites. Only request-level `ProxyOptions.headers` (sent to the upstream) require a runtime proxy. | ||
| </note> | ||
| ## Scheduled tasks (Cron Jobs) | ||
| <read-more></read-more> | ||
| Nitro automatically converts your [`scheduledTasks`](/docs/tasks#scheduled-tasks) configuration into [Vercel Cron Jobs](https://vercel.com/docs/cron-jobs) at build time. Define your schedules in your Nitro config and deploy - no manual `vercel.json` cron configuration required. | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| experimental: { | ||
| tasks: true | ||
| }, | ||
| scheduledTasks: { | ||
| // Run `cms:update` every hour | ||
| '0 * * * *': ['cms:update'], | ||
| // Run `db:cleanup` every day at midnight | ||
| '0 0 * * *': ['db:cleanup'] | ||
| } | ||
| }) | ||
| ``` | ||
| ### Secure cron job endpoints | ||
| <read-more></read-more> | ||
| To prevent unauthorized access to the cron handler, set a `CRON_SECRET` environment variable in your Vercel project settings. When `CRON_SECRET` is set, Nitro validates the `Authorization` header on every cron invocation. | ||
| ## Custom build output configuration | ||
| You can provide additional [build output configuration](https://vercel.com/docs/build-output-api/v3) using `vercel.config` key inside `nitro.config`. It will be merged with built-in auto-generated config. | ||
| ## On-Demand incremental static regeneration (ISR) | ||
| On-demand revalidation allows you to purge the cache for an ISR route whenever you want, foregoing the time interval required with background revalidation. | ||
| To revalidate a page on demand: | ||
| 1. Create an Environment Variable which will store a revalidation secret | ||
| - You can use the command `openssl rand -base64 32` or [Generate a Secret](https://generate-secret.vercel.app/32) to generate a random value. | ||
| - Update your configuration: | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| vercel: { | ||
| config: { | ||
| bypassToken: process.env.VERCEL_BYPASS_TOKEN | ||
| } | ||
| } | ||
| }) | ||
| ``` | ||
| - To trigger "On-Demand Incremental Static Regeneration (ISR)" and revalidate a path to a Prerender Function, make a GET or HEAD request to that path with a header of x-prerender-revalidate: `bypassToken`. When that Prerender Function endpoint is accessed with this header set, the cache will be revalidated. The next request to that function should return a fresh response. | ||
| ### Fine-grained ISR config via route rules | ||
| By default, query params affect cache keys but are not passed to the route handler unless specified. | ||
| You can pass an options object to `isr` route rule to configure caching behavior. | ||
| - `expiration`: Expiration time (in seconds) before the cached asset will be re-generated by invoking the Serverless Function. Setting the value to `false` (or `isr: true` route rule) means it will never expire. | ||
| - `group`: Group number of the asset. Prerender assets with the same group number will all be re-validated at the same time. | ||
| - `allowQuery`: List of query string parameter names that will be cached independently. - If an empty array, query values are not considered for caching. | ||
| - If `undefined` each unique query value is cached independently. | ||
| - For wildcard `/**` route rules, `url` is always added | ||
| - `passQuery`: When `true`, the query string will be present on the `request` argument passed to the invoked function. The `allowQuery` filter still applies. | ||
| - `exposeErrBody`: When `true`, expose the response body regardless of status code including error status codes. (default `false` | ||
| ```ts | ||
| export default defineNitroConfig({ | ||
| routeRules: { | ||
| "/products/**": { | ||
| isr: { | ||
| allowQuery: ["q"], | ||
| passQuery: true, | ||
| exposeErrBody: true | ||
| }, | ||
| }, | ||
| }, | ||
| }); | ||
| ``` |
| # Zeabur | ||
| > Deploy Nitro apps to [Zeabur](https://zeabur.com). | ||
| **Preset:** `zeabur` | ||
| <read-more></read-more> | ||
| <note> | ||
| Integration with this provider is possible with [zero configuration](/deploy/#zero-config-providers). | ||
| </note> | ||
| ## Deploy using git | ||
| 1. Push your code to your git repository (Currently only GitHub supported). | ||
| 2. [Import your project](https://zeabur.com/docs/get-started) into Zeabur. | ||
| 3. Zeabur will detect that you are using Nitro and will enable the correct settings for your deployment. | ||
| 4. Your application is deployed! |
| # Zephyr Cloud | ||
| > Deploy Nitro apps to [Zephyr Cloud](https://zephyr-cloud.io). | ||
| **Preset:** `zephyr` | ||
| <read-more></read-more> | ||
| Zephyr support is built into Nitro through the `zephyr` preset. | ||
| For most Zephyr-specific topics such as BYOC, cloud integrations, environments, and CI/CD authentication, refer to the [Zephyr Cloud docs](https://docs.zephyr-cloud.io). | ||
| <note> | ||
| Zephyr is a little different from most Nitro deployment providers. Instead of targeting a single hosting vendor directly, Zephyr acts as a deployment control plane on top of either Zephyr-managed infrastructure or your own cloud integrations. | ||
| </note> | ||
| ## BYOC model | ||
| Zephyr supports a BYOC (Bring Your Own Cloud) model. In Zephyr's architecture, the control plane stays managed by Zephyr, while the data plane (workers and storage) runs in your cloud accounts. | ||
| This lets you keep Zephyr's deployment workflow while using any supported Zephyr cloud integration. See the [Zephyr BYOC docs](https://docs.zephyr-cloud.io/features/byoc) for the current list of supported providers. | ||
| ## Deploy with Nitro CLI | ||
| Use Nitro's deploy command to build and upload your app to Zephyr in one step: | ||
| ```bash | ||
| npx nitro deploy --preset zephyr | ||
| ``` | ||
| Nitro will upload the generated output using `zephyr-agent`. If `zephyr-agent` is missing, Nitro will prompt to install it locally and will install it automatically in CI. | ||
| ## Deploy during build | ||
| Zephyr is a little different here from most Nitro providers: we recommend enabling deployment during `nitro build` and treating build as the primary deployment step. | ||
| If your CI pipeline already runs `nitro build`, enable deployment during the build step: | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| preset: "zephyr", | ||
| zephyr: { | ||
| deployOnBuild: true, | ||
| }, | ||
| }); | ||
| ``` | ||
| Then your normal build command is enough: | ||
| <pm-run></pm-run> | ||
| After the build finishes, Nitro uploads the generated output to Zephyr, deploys it to the edge, and prints the deployment URL: | ||
| ```txt | ||
| ◐ Building [Nitro] (preset: zephyr, compatibility: YYYY-MM-DD) | ||
| ... | ||
| ZEPHYR Uploaded local snapshot in 110ms | ||
| ZEPHYR Deployed to Zephyr's edge in 700ms. | ||
| ZEPHYR | ||
| ZEPHYR https://my-app.zephyrcloud.app | ||
| ``` | ||
| ## CI authentication | ||
| Zephyr requires an API token for non-interactive deployments. The example below uses the simpler personal-token style setup with `ZE_SECRET_TOKEN` together with `zephyr.deployOnBuild`. | ||
| ```yaml [.github/workflows/deploy.yml] | ||
| name: Deploy with Zephyr | ||
| on: | ||
| push: | ||
| branches: [main] | ||
| jobs: | ||
| deploy: | ||
| runs-on: ubuntu-latest | ||
| env: | ||
| ZE_SECRET_TOKEN: ${{ secrets.ZEPHYR_AUTH_TOKEN }} | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: 20 | ||
| - run: npm ci | ||
| - run: npm run build | ||
| ``` | ||
| For more advanced CI/CD setups, Zephyr also documents organization-level server-token authentication using `ZE_SERVER_TOKEN`. See the [Zephyr CI/CD server token docs](https://docs.zephyr-cloud.io/features/ci-cd-server-token). | ||
| ## Options | ||
| ### `zephyr.deployOnBuild` | ||
| Deploy to Zephyr during `nitro build` when using the `zephyr` preset. | ||
| - Default: `false` |
| # Zerops | ||
| > Deploy Nitro apps to [Zerops](https://zerops.io). | ||
| **Preset:** `zerops` | ||
| <read-more></read-more> | ||
| > [!IMPORTANT] | ||
| > 🚧 This preset is currently experimental. | ||
| Zerops supports deploying both static and server-side rendered apps with a simple configuration file in your project root. | ||
| ## Starter templates | ||
| If you want to quckly get started with zerops and nitro you can use repositories [`zeropsio/recipe-nitro-nodejs`](https://github.com/zeropsio/recipe-nitro-nodejs) and [`zeropsio/recipe-nitro-static`](https://github.com/zeropsio/recipe-nitro-static) starter templates. | ||
| ## Project setup | ||
| Projects and services can be added either through [project add wizard](https://app.zerops.io/dashboard/project-add) or imported using `zerops-project-import.yml`. | ||
| <code-group> | ||
| ```yml [zerops-project-import.yml (node.js)] | ||
| project: | ||
| name: nitro-app | ||
| services: | ||
| - hostname: app | ||
| type: nodejs@20 | ||
| ``` | ||
| ```yml [zerops-project-import.yml (static)] | ||
| project: | ||
| name: nitro-app | ||
| services: | ||
| - hostname: app | ||
| type: static | ||
| ``` | ||
| </code-group> | ||
| Then create a `zerops.yml` config in your project root: | ||
| <code-group> | ||
| ```yml [zerops.yml (node.js)] | ||
| zerops: | ||
| - setup: app | ||
| build: | ||
| base: nodejs@20 | ||
| envVariables: | ||
| NITRO_PRESET: zerops | ||
| buildCommands: | ||
| - pnpm i | ||
| - pnpm run build | ||
| deployFiles: | ||
| - .output | ||
| - package.json | ||
| - node_modules | ||
| run: | ||
| base: nodejs@20 | ||
| ports: | ||
| - port: 3000 | ||
| httpSupport: true | ||
| start: node .output/server/index.mjs | ||
| ``` | ||
| ```yml [zerops.yml (static)] | ||
| zerops: | ||
| - setup: app | ||
| build: | ||
| base: nodejs@20 | ||
| envVariables: | ||
| NITRO_PRESET: zerops-static | ||
| buildCommands: | ||
| - pnpm i | ||
| - pnpm build | ||
| deployFiles: | ||
| - .zerops/output/static/~ | ||
| run: | ||
| base: static | ||
| ``` | ||
| </code-group> | ||
| Now you can trigger the [build & deploy pipeline using the Zerops CLI](#building-deploying-your-app) or by connecting the app service with your [GitHub](https://docs.zerops.io/references/github-integration/) / [GitLab](https://docs.zerops.io/references/gitlab-integration) repository from inside the service detail. | ||
| ## Build and deploy | ||
| Open [Settings > Access Token Management](https://app.zerops.io/settings/token-management) in the Zerops app and generate a new access token. | ||
| Log in using your access token with the following command: | ||
| <pm-x></pm-x> | ||
| Navigate to the root of your app (where `zerops.yml` is located) and run the following command to trigger the deploy: | ||
| <pm-x></pm-x> | ||
| Your code can be deployed automatically on each commit or a new tag by connecting the service with your [GitHub](https://docs.zerops.io/references/gitlab-integration) / [GitLab](https://docs.zerops.io/references/gitlab-integration) repository. This connection can be set up in the service detail. | ||
| <read-more></read-more> |
| # Bun | ||
| > Run Nitro apps with Bun runtime. | ||
| **Preset:** `bun` | ||
| Nitro output is compatible with Bun runtime. While using default [Node.js](/deploy/runtimes/node) you can also run the output in bun, using `bun` preset has advantage of better optimizations. | ||
| After building with bun preset using `bun` as preset, you can run server in production using: | ||
| ```bash | ||
| bun run ./.output/server/index.mjs | ||
| ``` | ||
| <read-more></read-more> |
| # Deno | ||
| > Run Nitro apps with [Deno](https://deno.com/) runtime. | ||
| **Preset:** `deno_server` | ||
| You can build your Nitro server using Node.js to run within [Deno Runtime](https://deno.com/runtime) in a custom server. | ||
| ```bash | ||
| # Build with the deno NITRO preset | ||
| NITRO_PRESET=deno_server npm run build | ||
| # Start production server | ||
| deno run --unstable --allow-net --allow-read --allow-env .output/server/index.ts | ||
| ``` | ||
| ## Deno Deploy | ||
| <read-more></read-more> |
| # Node.js | ||
| > Run Nitro apps with Node.js runtime. | ||
| **Preset:** `node_server` | ||
| Node.js is the default nitro output preset for production builds and Nitro has native Node.js runtime support. | ||
| Build project using nitro CLI: | ||
| ```bash | ||
| nitro build | ||
| ``` | ||
| When running `nitro build` with the Node server preset, the result will be an entry point that launches a ready-to-run Node server. To try output: | ||
| ```bash | ||
| $ node .output/server/index.mjs | ||
| Listening on http://localhost:3000 | ||
| ``` | ||
| You can now deploy fully standalone `.output` directory to the hosting of your choice. | ||
| ### Environment Variables | ||
| You can customize server behavior using following environment variables: | ||
| - `NITRO_PORT` or `PORT` (defaults to `3000`) | ||
| - `NITRO_HOST` or `HOST` | ||
| - `NITRO_UNIX_SOCKET` - if provided (a path to the desired socket file) the service will be served over the provided UNIX socket. | ||
| - `NITRO_SSL_CERT` and `NITRO_SSL_KEY` - if both are present, this will launch the server in HTTPS mode. In the vast majority of cases, this should not be used other than for testing, and the Nitro server should be run behind a reverse proxy like nginx or Cloudflare which terminates SSL. | ||
| - `NITRO_SHUTDOWN_DISABLED` - Disables the graceful shutdown feature when set to `'true'`. If it's set to `'true'`, the graceful shutdown is bypassed to speed up the development process. Defaults to `'false'`. | ||
| - `NITRO_SHUTDOWN_SIGNALS` - Allows you to specify which signals should be handled. Each signal should be separated with a space. Defaults to `'SIGINT SIGTERM'`. | ||
| - `NITRO_SHUTDOWN_TIMEOUT` - Sets the amount of time (in milliseconds) before a forced shutdown occurs. Defaults to `'30000'` milliseconds. | ||
| - `NITRO_SHUTDOWN_FORCE` - When set to true, it triggers `process.exit()` at the end of the shutdown process. If it's set to `'false'`, the process will simply let the event loop clear. Defaults to `'true'`. | ||
| ## Cluster mode | ||
| **Preset:** `node_cluster` | ||
| For more performance and leveraging multi-core handling, you can use cluster preset. | ||
| ### Environment Variables | ||
| In addition to environment variables from the `node_server` preset, you can customize behavior: | ||
| - `NITRO_CLUSTER_WORKERS`: Number of cluster workers (default is Number of available cpu cores) | ||
| ## Handler (advanced) | ||
| **Preset:** `node_middleware` | ||
| Nitro also has a more low-level preset that directly exports a middleware usable for custom servers. | ||
| When running `nitro build` with the Node middleware preset, the result will be an entry point exporting a middleware handler. | ||
| **Example:** | ||
| ```js | ||
| import { createServer } from 'node:http' | ||
| import { listener } from './.output/server' | ||
| const server = createServer(listener) | ||
| server.listen(8080) | ||
| ``` |
| # Assets | ||
| Nitro supports two types of assets: **public assets** served directly to clients and **server assets** bundled into the server for programmatic access. | ||
| ## Public Assets | ||
| Nitro handles assets via the `public/` directory. | ||
| All assets in `public/` directory will be automatically served. This means that you can access them directly from the browser without any special configuration. | ||
| ```md | ||
| public/ | ||
| image.png <-- /image.png | ||
| video.mp4 <-- /video.mp4 | ||
| robots.txt <-- /robots.txt | ||
| ``` | ||
| ### Caching and Headers | ||
| Public assets are served with automatic `ETag` and `Last-Modified` headers for conditional requests. When the client sends `If-None-Match` or `If-Modified-Since` headers, Nitro returns a `304 Not Modified` response. | ||
| For assets served from a non-root `baseURL` (such as `/build/`), Nitro prevents fallthrough to application handlers. If a request matches a public asset base but the file is not found, a `404` is returned immediately. | ||
| ### Production Public Assets | ||
| When building your Nitro app, the `public/` directory will be copied to `.output/public/` and a manifest with metadata will be created and embedded in the server bundle. | ||
| ```json | ||
| { | ||
| "/image.png": { | ||
| "type": "image/png", | ||
| "etag": "\"4a0c-6utWq0Kbk5OqDmksYCa9XV8irnM\"", | ||
| "mtime": "2023-03-04T21:39:45.086Z", | ||
| "size": 18956 | ||
| }, | ||
| "/robots.txt": { | ||
| "type": "text/plain; charset=utf-8", | ||
| "etag": "\"8-hMqyDrA8fJ0R904zgEPs3L55Jls\"", | ||
| "mtime": "2023-03-04T21:39:45.086Z", | ||
| "size": 8 | ||
| }, | ||
| "/video.mp4": { | ||
| "type": "video/mp4", | ||
| "etag": "\"9b943-4UwfQXKUjPCesGPr6J5j7GzNYGU\"", | ||
| "mtime": "2023-03-04T21:39:45.085Z", | ||
| "size": 637251 | ||
| } | ||
| } | ||
| ``` | ||
| This allows Nitro to know the public assets without scanning the directory, giving high performance with caching headers. | ||
| ### Custom Public Asset Directories | ||
| You can configure additional public asset directories using the `publicAssets` config option. Each entry supports the following properties: | ||
| - `dir` -- Path to the directory (resolved relative to `rootDir`). | ||
| - `baseURL` -- URL prefix for serving assets (default: `"/"`). | ||
| - `maxAge` -- Cache `max-age` in seconds. When set, a `Cache-Control: public, max-age=<value>, immutable` header is applied via route rules. | ||
| - `fallthrough` -- Whether requests should fall through to application handlers when the asset is not found. Top-level (`baseURL: "/"`) directories default to `true`; non-root directories default to `false`. | ||
| - `ignore` -- Pass `false` to disable ignore patterns, or an array of glob patterns to override the global `ignore` option. | ||
| ```js [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| publicAssets: [ | ||
| { | ||
| baseURL: "build", | ||
| dir: "public/build", | ||
| maxAge: 3600, | ||
| }, | ||
| ], | ||
| }); | ||
| ``` | ||
| In this example, files in `public/build/` are served under `/build/` with a one-hour cache and no fallthrough to application handlers. | ||
| ### Compressed Public Assets | ||
| Nitro can generate pre-compressed versions of your public assets during the build. When a client sends an `Accept-Encoding` header, the server will serve the compressed version if available. Supported encodings are gzip (`.gz`), brotli (`.br`), and zstd (`.zst`). | ||
| Set `compressPublicAssets: true` to enable all encodings: | ||
| ```js [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| compressPublicAssets: true, | ||
| }); | ||
| ``` | ||
| Or pick specific encodings: | ||
| ```js [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| compressPublicAssets: { | ||
| gzip: true, | ||
| brotli: true, | ||
| zstd: false, | ||
| }, | ||
| }); | ||
| ``` | ||
| > [!NOTE] | ||
| > Only compressible MIME types (text, JavaScript, JSON, XML, WASM, fonts, SVG, etc.) with a file size of at least 1 KB are compressed. Source map files (`.map`) are excluded. | ||
| ## Server Assets | ||
| All assets in `assets/` directory will be added to the server bundle. After building your application, you can find them in the `.output/server/chunks/raw/` directory. Be careful with the size of your assets, as they will be bundled with the server bundle. | ||
| > [!TIP] | ||
| > Unless using `useStorage()`, assets won't be included in the server bundle. | ||
| They can be addressed by the `assets:server` mount point using the [storage layer](/docs/storage). | ||
| For example, you could store a json file in `assets/data.json` and retrieve it in your handler: | ||
| ```js | ||
| import { defineHandler } from "nitro"; | ||
| export default defineHandler(async () => { | ||
| const data = await useStorage("assets:server").get("data.json"); | ||
| return data; | ||
| }); | ||
| ``` | ||
| ### Custom Server Assets | ||
| In order to add assets from a custom directory, you will need to define a path in your nitro config. This allows you to add assets from a directory outside of the `assets/` directory. | ||
| Each entry in `serverAssets` supports the following properties: | ||
| - `baseName` -- Name used as the storage mount point (accessed via `assets:<baseName>`). | ||
| - `dir` -- Path to the directory (resolved relative to `rootDir`). | ||
| - `pattern` -- Glob pattern for file inclusion (default: `"**/*"`). | ||
| - `ignore` -- Array of glob patterns to exclude files. | ||
| ```js [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| serverAssets: [ | ||
| { | ||
| baseName: "templates", | ||
| dir: "./templates", | ||
| }, | ||
| ], | ||
| }); | ||
| ``` | ||
| Then you can use the `assets:templates` base to retrieve your assets. | ||
| ```ts [handlers/success.ts] | ||
| import { defineHandler } from "nitro"; | ||
| export default defineHandler(async (event) => { | ||
| const html = await useStorage("assets:templates").get("success.html"); | ||
| return html; | ||
| }); | ||
| ``` | ||
| > [!TIP] | ||
| > During development, server assets are read directly from the filesystem using the `fs` unstorage driver. In production, they are bundled into the server as lazy imports with pre-computed metadata (MIME type, ETag, modification time). |
| # Cache | ||
| > Nitro provides a caching system built on top of the storage layer, powered by [ocache](https://github.com/unjs/ocache). | ||
| ## Cached handlers | ||
| To cache an event handler, you simply need to use the `defineCachedHandler` method. | ||
| It works like `defineHandler` but with an second parameter for the [cache options](#options). | ||
| ```ts [routes/cached.ts] | ||
| import { defineCachedHandler } from "nitro/cache"; | ||
| export default defineCachedHandler((event) => { | ||
| return "I am cached for an hour"; | ||
| }, { maxAge: 60 * 60 }); | ||
| ``` | ||
| With this example, the response will be cached for 1 hour and a stale value will be sent to the client while the cache is being updated in the background. If you want to immediately return the updated response set `swr: false`. | ||
| See the [options](#options) section for more details about the available options. | ||
| <important> | ||
| **Request headers are dropped** when handling cached responses. Use the [`varies` option](#options) to consider specific headers when caching and serving the responses. | ||
| </important> | ||
| ### Automatic HTTP headers | ||
| When using `defineCachedHandler`, Nitro automatically manages HTTP cache headers on cached responses: | ||
| - **`etag`** -- A weak ETag (`W/"..."`) is generated from the response body hash if not already set by the handler. | ||
| - **`last-modified`** -- Set to the current time when the response is first cached, if not already set. | ||
| - **`cache-control`** -- Automatically set based on the `swr`, `maxAge`, and `staleMaxAge` options: - With `swr: true`: `s-maxage=<maxAge>, stale-while-revalidate=<staleMaxAge>` | ||
| - With `swr: false`: `max-age=<maxAge>` | ||
| ### Conditional requests (304 Not Modified) | ||
| Cached handlers automatically support conditional requests. When a client sends `if-none-match` or `if-modified-since` headers matching the cached response, Nitro returns a `304 Not Modified` response without a body. | ||
| ### Request method filtering | ||
| Only `GET` and `HEAD` requests are cached. All other HTTP methods (`POST`, `PUT`, `DELETE`, etc.) automatically bypass the cache and call the handler directly. | ||
| ### Request deduplication | ||
| When multiple concurrent requests hit the same cache key while the cache is being resolved, only one invocation of the handler runs. All concurrent requests wait for and share the same result. | ||
| ## Cached functions | ||
| You can also cache a function using the `defineCachedFunction` function. This is useful for caching the result of a function that is not an event handler, but is part of one, and reusing it in multiple handlers. | ||
| For example, you might want to cache the result of an API call for one hour: | ||
| ```ts [routes/api/stars/[...repo\].ts] | ||
| import { defineCachedFunction } from "nitro/cache"; | ||
| import { defineHandler, type H3Event } from "nitro"; | ||
| export default defineHandler(async (event) => { | ||
| const { repo } = event.context.params; | ||
| const stars = await cachedGHStars(repo).catch(() => 0) | ||
| return { repo, stars } | ||
| }); | ||
| const cachedGHStars = defineCachedFunction(async (repo: string) => { | ||
| const data = await fetch(`https://api.github.com/repos/${repo}`).then(res => res.json()); | ||
| return data.stargazers_count; | ||
| }, { | ||
| maxAge: 60 * 60, | ||
| name: "ghStars", | ||
| getKey: (repo: string) => repo | ||
| }); | ||
| ``` | ||
| The stars will be cached in development inside `.nitro/cache/functions/ghStars/<owner>/<repo>.json` with `value` being the number of stars. | ||
| ```json | ||
| {"expires":1677851092249,"value":43991,"mtime":1677847492540,"integrity":"ZUHcsxCWEH"} | ||
| ``` | ||
| <important> | ||
| Because the cached data is serialized to JSON, it is important that the cached function does not return anything that cannot be serialized, such as Symbols, Maps, Sets... | ||
| </important> | ||
| <callout> | ||
| If you are using edge workers to host your application, you should follow the instructions below. | ||
| :<collapsible></collapsible> | ||
| In edge workers, the instance is destroyed after each request. Nitro automatically uses `event.waitUntil` to keep the instance alive while the cache is being updated while the response is sent to the client. | ||
| To ensure that your cached functions work as expected in edge workers, **you should always pass the `event` as the first argument to the function using `defineCachedFunction`.** | ||
| ```ts [routes/api/stars/[...repo\].ts] {5,10,17} | ||
| import { defineCachedFunction } from "nitro/cache"; | ||
| export default defineHandler(async (event) => { | ||
| const { repo } = event.context.params; | ||
| const stars = await cachedGHStars(event, repo).catch(() => 0) | ||
| return { repo, stars } | ||
| }); | ||
| const cachedGHStars = defineCachedFunction(async (event: H3Event, repo: string) => { | ||
| const data = await fetch(`https://api.github.com/repos/${repo}`).then(res => res.json()); | ||
| return data.stargazers_count; | ||
| }, { | ||
| maxAge: 60 * 60, | ||
| name: "ghStars", | ||
| getKey: (event: H3Event, repo: string) => repo | ||
| }); | ||
| ``` | ||
| This way, the function will be able to keep the instance alive while the cache is being updated without slowing down the response to the client. | ||
| </callout> | ||
| :: | ||
| ## Using route rules | ||
| This feature enables you to add caching routes based on a glob pattern directly in the main configuration file. This is especially useful to have a global cache strategy for a part of your application. | ||
| Cache all the blog routes for 1 hour with `stale-while-revalidate` behavior: | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| routeRules: { | ||
| "/blog/**": { cache: { maxAge: 60 * 60 } }, | ||
| }, | ||
| }); | ||
| ``` | ||
| If we want to use a [custom cache storage](#cache-storage) mount point, we can use the `base` option. | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| storage: { | ||
| redis: { | ||
| driver: "redis", | ||
| url: "redis://localhost:6379", | ||
| }, | ||
| }, | ||
| routeRules: { | ||
| "/blog/**": { cache: { maxAge: 60 * 60, base: "redis" } }, | ||
| }, | ||
| }); | ||
| ``` | ||
| ### Route rules shortcuts | ||
| You can use the `swr` shortcut for enabling `stale-while-revalidate` caching on route rules. When set to `true`, SWR is enabled with the default `maxAge`. When set to a number, it is used as the `maxAge` value in seconds. | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| routeRules: { | ||
| "/blog/**": { swr: true }, | ||
| "/api/**": { swr: 3600 }, | ||
| }, | ||
| }); | ||
| ``` | ||
| To explicitly disable caching on a route, set `cache: false`: | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| routeRules: { | ||
| "/api/realtime/**": { cache: false }, | ||
| }, | ||
| }); | ||
| ``` | ||
| <note> | ||
| When using route rules, cached handlers use the group `'nitro/route-rules'` instead of the default `'nitro/handlers'`. | ||
| </note> | ||
| ## Cache storage | ||
| Nitro stores the data in the `cache` storage mount point. | ||
| - In production, it will use the [memory driver](https://unstorage.unjs.io/drivers/memory) by default. | ||
| - In development, it will use the [filesystem driver](https://unstorage.unjs.io/drivers/fs), writing to a temporary dir (`.nitro/cache`). | ||
| To overwrite the production storage, set the `cache` mount point using the `storage` option: | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| storage: { | ||
| cache: { | ||
| driver: 'redis', | ||
| /* redis connector options */ | ||
| } | ||
| } | ||
| }) | ||
| ``` | ||
| In development, you can also overwrite the cache mount point using the `devStorage` option: | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| storage: { | ||
| cache: { | ||
| // production cache storage | ||
| }, | ||
| }, | ||
| devStorage: { | ||
| cache: { | ||
| // development cache storage | ||
| } | ||
| } | ||
| }) | ||
| ``` | ||
| ## Options | ||
| The `defineCachedHandler` and `defineCachedFunction` functions accept the following options: | ||
| ### Shared options | ||
| These options are available for both `defineCachedHandler` and `defineCachedFunction`: | ||
| <field-group> | ||
| <field> | ||
| Name of the storage mountpoint to use for caching. :br | ||
| Default to `cache`. | ||
| </field> | ||
| <field> | ||
| Guessed from function name if not provided, and falls back to `'_'` otherwise. | ||
| </field> | ||
| <field> | ||
| Defaults to `'nitro/handlers'` for handlers and `'nitro/functions'` for functions. | ||
| </field> | ||
| <field> | ||
| A function that accepts the same arguments as the original function and returns a cache key (`String`). :br | ||
| If not provided, a built-in hash function will be used to generate a key based on the function arguments. For cached handlers, the key is derived from the request URL path and search params. | ||
| </field> | ||
| <field> | ||
| A value that invalidates the cache when changed. :br | ||
| By default, it is computed from **function code**, used in development to invalidate the cache when the function code changes. | ||
| </field> | ||
| <field> | ||
| Maximum age that cache is valid, in seconds. :br | ||
| Default to `1` (second). | ||
| </field> | ||
| <field> | ||
| Maximum age that a stale cache is valid, in seconds. If set to `-1` a stale value will still be sent to the client while the cache updates in the background. :br | ||
| Defaults to `0` (disabled). | ||
| </field> | ||
| <field> | ||
| Enable `stale-while-revalidate` behavior to serve a stale cached response while asynchronously revalidating it. :br | ||
| When enabled, stale cached values are returned immediately while revalidation happens in the background. When disabled, the caller waits for the fresh value before responding (the stale entry is cleared). :br | ||
| Defaults to `true`. | ||
| </field> | ||
| <field> | ||
| A function that returns a `boolean` to invalidate the current cache and create a new one. | ||
| </field> | ||
| <field> | ||
| A function that returns a `boolean` to bypass the current cache without invalidating the existing entry. | ||
| </field> | ||
| <field> | ||
| A custom error handler called when the cached function throws. :br | ||
| By default, errors are logged to the console and captured by the Nitro error handler. | ||
| </field> | ||
| </field-group> | ||
| ### Handler-only options | ||
| These options are only available for `defineCachedHandler`: | ||
| <field-group> | ||
| <field> | ||
| When `true`, skip full response caching and only handle conditional request headers (`if-none-match`, `if-modified-since`) for `304 Not Modified` responses. The handler is called on every request but benefits from conditional caching. | ||
| </field> | ||
| <field> | ||
| An array of request header names to vary the cache key on. Headers listed here are preserved on the request during cache resolution and included in the cache key, making the cache unique per combination of header values. :br :br | ||
| Headers **not** listed in `varies` are stripped from the request before calling the handler to ensure consistent cache hits. :br :br | ||
| For multi-tenant environments, you may want to pass `['host', 'x-forwarded-host']` to ensure these headers are not discarded and that the cache is unique per tenant. | ||
| </field> | ||
| </field-group> | ||
| ### Function-only options | ||
| These options are only available for `defineCachedFunction`: | ||
| <field-group> | ||
| <field> | ||
| Transform the cache entry before returning. The return value replaces the cached value. | ||
| </field> | ||
| <field> | ||
| Validate a cache entry. Return `false` to treat the entry as invalid and trigger re-resolution. | ||
| </field> | ||
| </field-group> | ||
| ## SWR behavior | ||
| The `stale-while-revalidate` (SWR) pattern is enabled by default (`swr: true`). Understanding how it interacts with other options: | ||
| | `swr` | `maxAge` | Behavior | | ||
| | --- | --- | --- | | ||
| | `true` (default) | `1` (default) | Cache for 1 second, serve stale while revalidating | | ||
| | `true` | `3600` | Cache for 1 hour, serve stale while revalidating | | ||
| | `false` | `3600` | Cache for 1 hour, wait for fresh value when expired | | ||
| | `true` | `3600` with `staleMaxAge: 600` | Cache for 1 hour, serve stale for up to 10 minutes while revalidating | | ||
| When `swr` is enabled and a cached value exists but has expired: | ||
| 1. The stale cached value is returned immediately to the client. | ||
| 2. The function/handler is called in the background to refresh the cache. | ||
| 3. On edge workers, `event.waitUntil` is used to keep the background refresh alive. | ||
| When `swr` is disabled and a cached value has expired: | ||
| 1. The stale entry is cleared. | ||
| 2. The client waits for the function/handler to resolve with a fresh value. | ||
| ## Cache keys and invalidation | ||
| When using the `defineCachedFunction` or `defineCachedHandler` functions, the cache key is generated using the following pattern: | ||
| ```ts | ||
| `${options.base}:${options.group}:${options.name}:${options.getKey(...args)}.json` | ||
| ``` | ||
| For example, the following function: | ||
| ```ts | ||
| import { defineCachedFunction } from "nitro/cache"; | ||
| const getAccessToken = defineCachedFunction(() => { | ||
| return String(Date.now()) | ||
| }, { | ||
| maxAge: 10, | ||
| name: "getAccessToken", | ||
| getKey: () => "default" | ||
| }); | ||
| ``` | ||
| Will generate the following cache key: | ||
| ```ts | ||
| cache:nitro/functions:getAccessToken:default.json | ||
| ``` | ||
| You can invalidate the cached function entry with: | ||
| ```ts | ||
| import { useStorage } from "nitro/storage"; | ||
| await useStorage('cache').removeItem('nitro/functions:getAccessToken:default.json') | ||
| ``` | ||
| <note> | ||
| For cached handlers, the cache key includes a hash of the URL path and, when using the [`varies`](#handler-only-options) option, hashes of the specified header values appended to the key. | ||
| </note> | ||
| <note> | ||
| Responses with HTTP status codes `>= 400` or with an undefined body are not cached. This prevents caching error responses. | ||
| </note> | ||
| <read-more> | ||
| Read more about the Nitro storage. | ||
| </read-more> |
| # Configuration | ||
| > Customize and extend Nitro defaults. | ||
| <read-more> | ||
| See [config reference](/config) for available options. | ||
| </read-more> | ||
| ## Config file | ||
| You can customize your Nitro builder with a configuration file. | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| // Nitro options | ||
| }) | ||
| ``` | ||
| ```ts [vite.config.ts] | ||
| import { defineConfig } from 'vite' | ||
| import { nitro } from 'nitro/vite' | ||
| export default defineConfig({ | ||
| plugins: [ | ||
| nitro() | ||
| ], | ||
| nitro: { | ||
| // Nitro options | ||
| } | ||
| }) | ||
| ``` | ||
| > [!TIP] | ||
| > Nitro loads the configuration using [c12](https://github.com/unjs/c12), giving more possibilities such as using `.nitrorc` file in current working directory or in the user's home directory. | ||
| ### Environment-specific config | ||
| Using [c12](https://github.com/unjs/c12) conventions, you can provide environment-specific overrides using `$development` and `$production` keys: | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| logLevel: 3, | ||
| $development: { | ||
| // Options applied only in development mode | ||
| debug: true, | ||
| }, | ||
| $production: { | ||
| // Options applied only in production builds | ||
| minify: true, | ||
| }, | ||
| }) | ||
| ``` | ||
| The environment name is `"development"` during `nitro dev` and `"production"` during `nitro build`. | ||
| ### Extending configs | ||
| You can extend from other configs or presets using the `extends` key: | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| extends: "./base.config", | ||
| }) | ||
| ``` | ||
| ### Config from `package.json` | ||
| You can also provide Nitro configuration under the `nitro` key in your `package.json` file. | ||
| ## Directory options | ||
| Nitro provides several options for controlling directory structure: | ||
| | Option | Default | Description | | ||
| | --- | --- | --- | | ||
| | `rootDir` | `.` (current directory) | The root directory of the project. | | ||
| | `serverDir` | `false` | Server source directory (set to `"server"` or `"./"` to enable). | | ||
| | `buildDir` | `node_modules/.nitro` | Directory for build artifacts. | | ||
| | `output.dir` | `.output` | Production output directory. | | ||
| | `output.serverDir` | `.output/server` | Server output directory. | | ||
| | `output.publicDir` | `.output/public` | Public assets output directory. | | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| serverDir: "server", | ||
| buildDir: "node_modules/.nitro", | ||
| output: { | ||
| dir: ".output", | ||
| }, | ||
| }) | ||
| ``` | ||
| > [!NOTE] | ||
| > The `srcDir` option is deprecated. Use `serverDir` instead. | ||
| ## Environment variables | ||
| Certain Nitro behaviors can be configured using environment variables: | ||
| | Variable | Description | | ||
| | --- | --- | | ||
| | `NITRO_PRESET` | Override the deployment preset. | | ||
| | `NITRO_COMPATIBILITY_DATE` | Set the compatibility date. | | ||
| | `NITRO_APP_BASE_URL` | Override the base URL (default: `/`). | | ||
| ## Runtime configuration | ||
| Nitro provides a runtime config API to expose configuration within your application, with the ability to update it at runtime by setting environment variables. This is useful when you want to expose different configuration values for different environments (e.g. development, staging, production). For example, you can use this to expose different API endpoints for different environments or to expose different feature flags. | ||
| First, you need to define the runtime config in your configuration file. | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| runtimeConfig: { | ||
| apiToken: "dev_token", // `dev_token` is the default value | ||
| } | ||
| }); | ||
| ``` | ||
| You can now access the runtime config using `useRuntimeConfig()`. | ||
| ```ts [api/example.get.ts] | ||
| import { defineHandler } from "nitro"; | ||
| import { useRuntimeConfig } from "nitro/runtime-config"; | ||
| export default defineHandler((event) => { | ||
| return useRuntimeConfig().apiToken; // Returns `dev_token` | ||
| }); | ||
| ``` | ||
| ### Nested objects | ||
| Runtime config supports nested objects. Keys at any depth are mapped to environment variables using the `NITRO_` prefix and `UPPER_SNAKE_CASE` conversion: | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| runtimeConfig: { | ||
| database: { | ||
| host: "localhost", | ||
| port: 5432, | ||
| }, | ||
| }, | ||
| }); | ||
| ``` | ||
| ```bash [.env] | ||
| NITRO_DATABASE_HOST="db.example.com" | ||
| NITRO_DATABASE_PORT="5433" | ||
| ``` | ||
| > [!NOTE] | ||
| > Only keys defined in `runtimeConfig` in your config file will be considered. You cannot introduce new keys using environment variables alone. | ||
| ### Serialization | ||
| Runtime config values must be serializable (strings, numbers, booleans, plain objects, and arrays). Non-serializable values (class instances, functions, etc.) will trigger a warning at build time. | ||
| Values that are `undefined` or `null` in the config are replaced with empty strings (`""`) as a fallback. | ||
| ### Local development | ||
| You can update the runtime config using environment variables. You can use a `.env` or `.env.local` file in development and use platform variables in production (see below). | ||
| Create an `.env` file in your project root: | ||
| ```bash [.env] | ||
| NITRO_API_TOKEN="123" | ||
| ``` | ||
| Re-start the development server, fetch the `/api/example` endpoint and you should see `123` as the response instead of `dev_token`. | ||
| > [!NOTE] | ||
| > The `.env` and `.env.local` files are only loaded during development (`nitro dev`). In production, use your platform's native environment variable mechanism. | ||
| Do not forget that you can still universally access environment variables using `import.meta.env` or `process.env` but avoid using them in ambient global contexts to prevent unexpected behavior. | ||
| ### Production | ||
| You can define variables in your production environment to update the runtime config. | ||
| <warning> | ||
| All variables must be prefixed with `NITRO_` to be applied to the runtime config. They will override the runtime config variables defined within your `nitro.config.ts` file. | ||
| </warning> | ||
| ```bash [.env] | ||
| NITRO_API_TOKEN="123" | ||
| ``` | ||
| In runtime config, define key using camelCase. In environment variables, define key using snake_case and uppercase. | ||
| ```ts | ||
| { | ||
| helloWorld: "foo" | ||
| } | ||
| ``` | ||
| ```bash | ||
| NITRO_HELLO_WORLD="foo" | ||
| ``` | ||
| ### Custom env prefix | ||
| You can configure a secondary environment variable prefix using the `nitro.envPrefix` runtime config key. This prefix is checked in addition to the default `NITRO_` prefix: | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| runtimeConfig: { | ||
| nitro: { | ||
| envPrefix: "APP_", | ||
| }, | ||
| apiToken: "", | ||
| }, | ||
| }); | ||
| ``` | ||
| With this configuration, both `NITRO_API_TOKEN` and `APP_API_TOKEN` will be checked as overrides. | ||
| ### Env expansion | ||
| When enabled, environment variable references using `{{VAR_NAME}}` syntax in runtime config string values are expanded at runtime: | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| experimental: { | ||
| envExpansion: true, | ||
| }, | ||
| runtimeConfig: { | ||
| url: "https://{{APP_DOMAIN}}/api", | ||
| }, | ||
| }); | ||
| ``` | ||
| ```bash | ||
| APP_DOMAIN="example.com" | ||
| ``` | ||
| At runtime, `useRuntimeConfig().url` will resolve to `"https://example.com/api"`. |
| > Nitro provides a built-in and lightweight SQL database layer. | ||
| The default database connection is **preconfigured** with [SQLite](https://db0.unjs.io/connectors/sqlite) and works out of the box for development mode and any Node.js compatible production deployments. By default, data will be stored in `.data/db.sqlite`. | ||
| <read-more></read-more> | ||
| > [!IMPORTANT] | ||
| > Database support is currently experimental. | ||
| > Refer to the [db0 issues](https://github.com/unjs/db0/issues) for status and bug report. | ||
| In order to enable database layer you need to enable experimental feature flag. | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| experimental: { | ||
| database: true | ||
| } | ||
| }) | ||
| ``` | ||
| > [!TIP] | ||
| > You can change default connection or define more connections to any of the [supported databases](https://db0.unjs.io/connectors/sqlite). | ||
| > [!TIP] | ||
| > You can integrate database instance to any of the [supported ORMs](https://db0.unjs.io/integrations). | ||
| ## Usage | ||
| ```ts [server.ts] | ||
| import { defineHandler } from "nitro"; | ||
| import { useDatabase } from "nitro/database"; | ||
| export default defineHandler(async () => { | ||
| const db = useDatabase(); | ||
| // Create users table | ||
| await db.sql`DROP TABLE IF EXISTS users`; | ||
| await db.sql`CREATE TABLE IF NOT EXISTS users ("id" TEXT PRIMARY KEY, "firstName" TEXT, "lastName" TEXT, "email" TEXT)`; | ||
| // Add a new user | ||
| const userId = String(Math.round(Math.random() * 10_000)); | ||
| await db.sql`INSERT INTO users VALUES (${userId}, 'John', 'Doe', '')`; | ||
| // Query for users | ||
| const { rows } = await db.sql`SELECT * FROM users WHERE id = ${userId}`; | ||
| return { | ||
| rows, | ||
| }; | ||
| }); | ||
| ``` | ||
| ### `useDatabase` | ||
| Use `useDatabase` to get a database instance. It accepts an optional connection name (defaults to `"default"`). | ||
| ```ts | ||
| import { useDatabase } from "nitro/database"; | ||
| // Use the default connection | ||
| const db = useDatabase(); | ||
| // Use a named connection | ||
| const usersDb = useDatabase("users"); | ||
| ``` | ||
| > [!NOTE] | ||
| > When `experimental.database` is enabled, `useDatabase` is auto-imported and available without an explicit import statement. | ||
| Database instances are created lazily on first use and cached for subsequent calls with the same connection name. If a connection name is not configured, an error will be thrown. | ||
| ### `db.sql` | ||
| Execute SQL queries using tagged template literals with automatic parameter binding: | ||
| ```ts | ||
| const db = useDatabase(); | ||
| // Insert with parameterized values (safe from SQL injection) | ||
| const id = "1001"; | ||
| await db.sql`INSERT INTO users VALUES (${id}, 'John', 'Doe', 'john@example.com')`; | ||
| // Query with parameters | ||
| const { rows } = await db.sql`SELECT * FROM users WHERE id = ${id}`; | ||
| // The result includes rows, changes count, and last insert ID | ||
| const result = await db.sql`INSERT INTO posts (title) VALUES (${"Hello"})`; | ||
| // result.rows, result.changes, result.lastInsertRowid | ||
| ``` | ||
| ### `db.exec` | ||
| Execute a raw SQL string directly: | ||
| ```ts | ||
| const db = useDatabase(); | ||
| await db.exec("CREATE TABLE IF NOT EXISTS users (id TEXT PRIMARY KEY, name TEXT)"); | ||
| ``` | ||
| ### `db.prepare` | ||
| Prepare an SQL statement for repeated execution: | ||
| ```ts | ||
| const db = useDatabase(); | ||
| const stmt = db.prepare("SELECT * FROM users WHERE id = ?"); | ||
| const result = await stmt.bind("1001").all(); | ||
| ``` | ||
| ## Configuration | ||
| You can configure database connections using `database` config: | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| database: { | ||
| default: { | ||
| connector: "sqlite", | ||
| options: { name: "db" } | ||
| }, | ||
| users: { | ||
| connector: "postgresql", | ||
| options: { | ||
| url: "postgresql://username:password@hostname:port/database_name" | ||
| }, | ||
| }, | ||
| }, | ||
| }); | ||
| ``` | ||
| ### Development Database | ||
| Use the `devDatabase` config to override the database configuration **only for development mode**. This is useful for using a local SQLite database during development while targeting a different database in production. | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| database: { | ||
| default: { | ||
| connector: "postgresql", | ||
| options: { | ||
| url: "postgresql://username:password@hostname:port/database_name" | ||
| } | ||
| } | ||
| }, | ||
| devDatabase: { | ||
| default: { | ||
| connector: "sqlite", | ||
| options: { name: "dev-db" } | ||
| } | ||
| } | ||
| }); | ||
| ``` | ||
| > [!TIP] | ||
| > When `experimental.database` is enabled and no `database` or `devDatabase` config is provided, Nitro automatically configures a default SQLite connection. In development mode, data is stored relative to the project root directory. In Node.js production, it uses the default SQLite path. | ||
| ## Connectors | ||
| Nitro supports all [db0 connectors](https://db0.unjs.io/connectors). The `connector` field in the database config accepts any of the following values: | ||
| | Connector | Description | | ||
| | --- | --- | | ||
| | `sqlite` | Node.js built-in SQLite (alias for `node-sqlite`) | | ||
| | `node-sqlite` | Node.js built-in SQLite | | ||
| | `better-sqlite3` | [better-sqlite3](https://github.com/WiseLibs/better-sqlite3) | | ||
| | `sqlite3` | [sqlite3](https://github.com/TryGhost/node-sqlite3) | | ||
| | `bun` / `bun-sqlite` | Bun built-in SQLite | | ||
| | `libsql` / `libsql-node` | [libSQL](https://github.com/tursodatabase/libsql) (Node.js) | | ||
| | `libsql-http` | libSQL over HTTP | | ||
| | `libsql-web` | libSQL for web environments | | ||
| | `postgresql` | [PostgreSQL](https://github.com/porsager/postgres) | | ||
| | `mysql2` | [MySQL](https://github.com/sidorares/node-mysql2) | | ||
| | `pglite` | [PGlite](https://github.com/electric-sql/pglite) (embedded PostgreSQL) | | ||
| | `planetscale` | [PlanetScale](https://github.com/planetscale/database-js) serverless | | ||
| | `cloudflare-d1` | [Cloudflare D1](https://developers.cloudflare.com/d1/) | | ||
| | `cloudflare-hyperdrive-mysql` | Cloudflare Hyperdrive with MySQL | | ||
| | `cloudflare-hyperdrive-postgresql` | Cloudflare Hyperdrive with PostgreSQL | |
| # Introduction | ||
| > Nitro is a full-stack server framework, compatible with any runtime and any deployment target. | ||
| Nitro gives you a production-ready server with filesystem routing, code-splitting, and built-in support for storage, caching, and databases — all runtime-agnostic and deployable anywhere. | ||
| ## What is Nitro? | ||
| Create server and API routes inside the `routes/` directory. Each file maps directly to a URL path, and Nitro handles the rest — routing, code-splitting, and optimized builds. | ||
| You can also take full control of the server entry by creating a `server.ts` file. Nitro’s high-level, runtime-agnostic approach lets you use any HTTP library, such as [Elysia](https://elysiajs.com/), [h3](https://h3.dev), or [Hono](https://hono.dev). | ||
| ### Performance | ||
| Nitro compiles your routes at build time, removing the need for a runtime router. Only the code required to handle each incoming request is loaded and executed. This makes it ideal for serverless hosting, with near-0ms boot time regardless of project size. | ||
| ### Deploy Anywhere | ||
| Build your server into an optimized `.output/` folder compatible with Node.js, Bun, Deno, and many hosting platforms without any configuration — Cloudflare Workers, Netlify, Vercel, and more. Take advantage of platform features like ESR, ISR, and SWR without changing a single line of code. | ||
| ### Server-Side Rendering | ||
| Render HTML with your favorite templating engine, or use component libraries such as React, Vue, or Svelte directly on the server. Go full universal rendering with client-side hydration. Nitro provides the foundation and a progressive approach to reach your goals. | ||
| ### Storage | ||
| Nitro includes a runtime-agnostic key-value storage layer out of the box. It uses in-memory storage by default, but you can connect more than 20 different drivers (FS, Redis, S3, etc.), attach them to different namespaces, and swap them without changing your code. | ||
| ### Caching | ||
| Nitro supports caching for both server routes and server functions, backed directly by the server storage (via the `cache` namespace). | ||
| ### Database | ||
| Nitro also includes a built-in SQL database. It defaults to SQLite, but you can connect to and query more than 10 databases (Postgres, MySQL, PGLite, etc.) using the same API. | ||
| ### Meta-Framework Foundation | ||
| Nitro can be used as the foundation for building your own meta-framework. Popular frameworks such as Nuxt, SolidStart, and TanStack Start fully or partially leverage Nitro. | ||
| ## Vite Integration | ||
| Nitro integrates seamlessly with [Vite](https://vite.dev) as a plugin. If you’re building a frontend application with Vite, adding Nitro gives you API routes, server-side rendering, and a full production server — all built together with `vite build`. | ||
| ```ts [vite.config.ts] | ||
| import { defineConfig } from "vite"; | ||
| import { nitro } from "nitro/vite"; | ||
| export default defineConfig({ | ||
| plugins: [nitro()], | ||
| }); | ||
| ``` | ||
| With Nitro, `vite build` produces an optimized `.output/` folder containing both your frontend and backend — ready to deploy anywhere. | ||
| Ready to give it a try? Jump into the [quick start](/docs/quick-start). |
| # Lifecycle | ||
| > Understand how Nitro runs and serves incoming requests to your application. | ||
| ## Request lifecycle | ||
| A request can be intercepted and terminated (with or without a response) from any of these layers, in this order: | ||
| <steps> | ||
| ### `request` hook | ||
| The `request` hook is the first code that runs for every incoming request. It is registered via a [server plugin](/docs/plugins): | ||
| ```ts [plugins/request-hook.ts] | ||
| import { definePlugin } from "nitro"; | ||
| export default definePlugin((nitroApp) => { | ||
| nitroApp.hooks.hook("request", (event) => { | ||
| console.log(`Incoming request on ${event.path}`); | ||
| }); | ||
| }); | ||
| ``` | ||
| <note> | ||
| Errors thrown inside the `request` hook are captured by the [`error` hook](#error-handling) and do not terminate the request pipeline. | ||
| </note> | ||
| ### Static assets | ||
| When static asset serving is enabled (the default for most presets), Nitro checks if the request matches a file in the `public/` directory **before** any other middleware or route handler runs. | ||
| If a match is found, the static file is served immediately with appropriate `Content-Type`, `ETag`, `Last-Modified`, and `Cache-Control` headers. The request is terminated and no further middleware or routes are executed. | ||
| Static assets also support content negotiation for pre-compressed files (gzip, brotli, zstd) via the `Accept-Encoding` header. | ||
| ### Route rules | ||
| The matching route rules defined in the Nitro config will execute. Route rules run as middleware so most of them alter the response without terminating it (for instance, adding a header or setting a cache policy). | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| routeRules: { | ||
| '/**': { headers: { 'x-nitro': 'first' } } | ||
| } | ||
| }) | ||
| ``` | ||
| <read-more></read-more> | ||
| ### Global middleware | ||
| Any global middleware defined in the `middleware/` directory will be run: | ||
| ```ts [middleware/info.ts] | ||
| import { defineHandler } from "nitro"; | ||
| export default defineHandler((event) => { | ||
| event.context.info = { name: "Nitro" }; | ||
| }); | ||
| ``` | ||
| <warning> | ||
| Returning from a middleware will close the request and should be avoided when possible. | ||
| </warning> | ||
| <read-more> | ||
| Learn more about Nitro middleware. | ||
| </read-more> | ||
| ### Routed middleware | ||
| Middleware that targets a specific route pattern (defined with a `route` in `middleware/`) runs after global middleware but before the matched route handler. | ||
| ### Routes | ||
| Nitro will look at defined routes in the `routes/` folder to match the incoming request. | ||
| ```ts [routes/api/hello.ts] | ||
| export default (event) => ({ world: true }) | ||
| ``` | ||
| <read-more> | ||
| Learn more about Nitro file-system routing. | ||
| </read-more> | ||
| If serverEntry is defined it will catch all requests not matching any other route acting as `/**` route handler. | ||
| ```ts [server.ts] | ||
| import { defineHandler } from "nitro"; | ||
| export default defineHandler((event) => { | ||
| if (event.path === "/") { | ||
| return "Home page"; | ||
| } | ||
| }); | ||
| ``` | ||
| <read-more> | ||
| Learn more about Nitro server entry. | ||
| </read-more> | ||
| ### Renderer | ||
| If no route is matched, Nitro will look for a renderer handler (defined or auto-detected) to handle the request. | ||
| <read-more> | ||
| Learn more about Nitro renderer. | ||
| </read-more> | ||
| ### `response` hook | ||
| After the response is created (from any of the layers above), the `response` hook runs. This hook receives the final `Response` object and the event, and can be used to inspect or modify response headers: | ||
| ```ts [plugins/response-hook.ts] | ||
| import { definePlugin } from "nitro"; | ||
| export default definePlugin((nitroApp) => { | ||
| nitroApp.hooks.hook("response", (res, event) => { | ||
| console.log(`Response ${res.status} for ${event.path}`); | ||
| }); | ||
| }); | ||
| ``` | ||
| <note> | ||
| The `response` hook runs for every response, including static assets, middleware-terminated requests, and error responses. | ||
| </note> | ||
| </steps> | ||
| ## Error handling | ||
| When an error occurs at any point in the request lifecycle, Nitro: | ||
| 1. Calls the `error` hook with the error and context (including the event and source tags). | ||
| 2. Passes the error to the **error handler** which converts it into an HTTP response. | ||
| ```ts [plugins/errors.ts] | ||
| import { definePlugin } from "nitro"; | ||
| export default definePlugin((nitroApp) => { | ||
| nitroApp.hooks.hook("error", (error, context) => { | ||
| console.error("Captured error:", error); | ||
| // context.event - the H3 event (if available) | ||
| // context.tags - error source tags like "request", "response", "plugin" | ||
| }); | ||
| }); | ||
| ``` | ||
| Errors are also tracked per-request in `event.req.context.nitro.errors` for inspection in later hooks. | ||
| You can provide a custom error handler in the Nitro config to control error response formatting: | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| errorHandler: "~/error", | ||
| }) | ||
| ``` | ||
| Additionally, unhandled promise rejections and uncaught exceptions at the process level are automatically captured into the `error` hook with the tags `"unhandledRejection"` and `"uncaughtException"`. | ||
| ## Server shutdown | ||
| When the Nitro server is shutting down, the `close` hook is called. Use this to clean up resources such as database connections, timers, or external service handles: | ||
| ```ts [plugins/cleanup.ts] | ||
| import { definePlugin } from "nitro"; | ||
| export default definePlugin((nitroApp) => { | ||
| nitroApp.hooks.hook("close", async () => { | ||
| // Clean up resources | ||
| }); | ||
| }); | ||
| ``` | ||
| ## Hooks reference | ||
| All runtime hooks are registered through [server plugins](/docs/plugins) using `nitroApp.hooks.hook()`. | ||
| | Hook | Signature | When it runs | | ||
| | --- | --- | --- | | ||
| | `request` | `(event: HTTPEvent) => void \| Promise<void>` | Start of each request, before routing. | | ||
| | `response` | `(res: Response, event: HTTPEvent) => void \| Promise<void>` | After the response is created, before it is sent. | | ||
| | `error` | `(error: Error, context: { event?, tags? }) => void` | When any error is captured during the lifecycle. | | ||
| | `close` | `() => void` | When the Nitro server is shutting down. | | ||
| <note> | ||
| The `NitroRuntimeHooks` interface is augmentable. Deployment presets (such as Cloudflare) can extend it with platform-specific hooks. | ||
| </note> | ||
| <read-more> | ||
| Learn more about Nitro plugins and hook usage examples. | ||
| </read-more> |
| # Migration Guide | ||
| > [!NOTE] | ||
| > This is a living document for migrating from Nitro 2 to 3. Please check it regularly while using the beta version. | ||
| Nitro v3 introduces intentional backward-incompatible changes. This guide helps you migrate from Nitro v2. | ||
| ## `nitropack` is renamed to `nitro` | ||
| The NPM package [nitropack](https://www.npmjs.com/package/nitropack) (v2) has been renamed to [nitro](https://www.npmjs.com/package/nitro) (v3). | ||
| **Migration:** Update the `nitropack` dependency to `nitro` in `package.json`: | ||
| ```diff [release channel] | ||
| { | ||
| "dependencies": { | ||
| -- "nitropack": "latest" | ||
| ++ "nitro": "latest" | ||
| } | ||
| } | ||
| ``` | ||
| ```diff [nightly channel] | ||
| { | ||
| "dependencies": { | ||
| -- "nitropack": "latest" | ||
| ++ "nitro": "npm:nitro-nightly" | ||
| } | ||
| } | ||
| ``` | ||
| **Migration:** Search your codebase and rename all instances of nitropack to nitro: | ||
| ```diff | ||
| -- import { defineNitroConfig } from "nitropack/config" | ||
| ++ import { defineNitroConfig } from "nitro/config" | ||
| ``` | ||
| ## nitro/runtime | ||
| Runtime utils had been moved to individual `nitro/*` subpath exports. Refer to docs for usage. | ||
| ```diff | ||
| -- import { useStorage } from "nitropack/runtime/storage" | ||
| ++ import { useStorage } from "nitro/storage" | ||
| ``` | ||
| ## Minimum Supported Node.js Version: 20 | ||
| Nitro now requires a minimum Node.js version of 20, as Node.js 18 reaches end-of-life in [April 2025](https://nodejs.org/en/about/previous-releases). | ||
| Please upgrade to the [latest LTS](https://nodejs.org/en/download) version (>= 20). | ||
| **Migration:** | ||
| - Check your local Node.js version using `node --version` and update if necessary. | ||
| - If you use a CI/CD system for deployment, ensure that your pipeline is running Node.js 20 or higher. | ||
| - If your hosting provider manages the Node.js runtime, make sure it's set to version 20, 22, or later. | ||
| ## Type Imports | ||
| Nitro types are now only exported from `nitro/types`. | ||
| **Migration:** Import types from nitro/types instead of nitro: | ||
| ```diff | ||
| -- import { NitroRuntimeConfig } from "nitropack" | ||
| ++ import { NitroRuntimeConfig } from "nitro/types" | ||
| ``` | ||
| ## App Config Support Removed | ||
| Nitro v2 supported a bundled app config that allowed defining configurations in `app.config.ts` and accessing them at runtime via `useAppConfig()`. | ||
| This feature had been removed. | ||
| **Migration:** | ||
| Use a regular `.ts` file in your server directory and import it directly. | ||
| ## Preset updates | ||
| Nitro presets have been updated for the latest compatibility. | ||
| Some (legacy) presets have been removed or renamed. | ||
| | Old Preset | New Preset | | ||
| | --- | --- | | ||
| | `node` | `node_middleware` (export changed to `middleware`) | | ||
| | `cloudflare`, `cloudflare_worker`, `cloudflare_module_legacy` | `cloudflare_module` | | ||
| | `deno-server-legacy` | `deno_server` with Deno v2 | | ||
| | `netlify-builder` | `netlify` or `netlify_edge` | | ||
| | `vercel-edge` | `vercel` with Fluid compute enabled | | ||
| | `azure`, `azure_functions` | `azure_swa` | | ||
| | `firebase` | `firebase_app_hosting` | | ||
| | `iis` | `iis_handler` | | ||
| | `deno` | `deno_deploy` | | ||
| | `edgio` | Discontinued | | ||
| | `cli` | Removed due to lack of use | | ||
| | `service_worker` | Removed due to instability | | ||
| ## Cloudflare Bindings Access | ||
| In Nitro v2, Cloudflare environment variables and bindings were accessible via `event.context.cloudflare.env`. | ||
| In Nitro v3, the Cloudflare preset uses [srvx](https://srvx.unjs.io/) as the underlying server layer. The Cloudflare runtime context is now attached to the request's runtime object instead of the event context. | ||
| **Migration:** | ||
| ```diff | ||
| -- const { cloudflare } = event.context | ||
| -- const binding = cloudflare.env.MY_BINDING | ||
| ++ const { env } = event.req.runtime.cloudflare | ||
| ++ const binding = env.MY_BINDING | ||
| ``` | ||
| :: | ||
| ## Changed nitro subpath imports | ||
| Nitro v2 introduced multiple subpath exports, some of which have been removed or updated: | ||
| - `nitro/rollup`, `nitropack/core` (use `nitro/builder`) | ||
| - `nitropack/runtime/*` (use `nitro/*`) | ||
| - `nitropack/kit` (removed) | ||
| - `nitropack/presets` (removed) | ||
| An experimental `nitropack/kit` was introduced but has now been removed. A standalone Nitro Kit package may be introduced in the future with clearer objectives. | ||
| **Migration:** | ||
| - Use `NitroModule` from `nitro/types` instead of `defineNitroModule` from the kit. | ||
| - Prefer built-in Nitro presets (external presets are only for evaluation purposes). | ||
| ## H3 v2 | ||
| Nitro v3 upgrades to [H3 v2](https://h3.dev), which includes API changes. All H3 utilities are imported from `nitro/h3`. | ||
| ### Web Standards | ||
| H3 v2 is rewritten based on web standard primitives ([`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL), [`Headers`](https://developer.mozilla.org/en-US/docs/Web/API/Headers), [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request), and [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)). | ||
| Access to `event.node.{req,res}` is only available in Node.js runtime. `event.web` is renamed to `event.req` (instance of web [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request)). | ||
| ### Response Handling | ||
| You should always explicitly **return** the response body or **throw** an error: | ||
| ```diff | ||
| -- import { send, sendRedirect, sendStream } from "nitro/h3" | ||
| -- send(event, value) | ||
| -- sendStream(event, stream) | ||
| -- sendRedirect(event, location, code) | ||
| ++ import { redirect } from "nitro/h3" | ||
| ++ return value | ||
| ++ return stream | ||
| ++ return redirect(event, location, code) | ||
| ``` | ||
| Other changes: | ||
| - `sendError(event, error)` → `throw createError(error)` | ||
| - `sendNoContent(event)` → `return noContent(event)` | ||
| - `sendProxy(event, target)` → `return proxy(event, target)` | ||
| ### Request Body | ||
| Most body utilities can be replaced with native `event.req` methods: | ||
| ```diff | ||
| -- import { readBody, readRawBody, readFormData } from "nitro/h3" | ||
| ++ // Use native Request methods | ||
| ++ const json = await event.req.json() | ||
| ++ const text = await event.req.text() | ||
| ++ const formData = await event.req.formData() | ||
| ++ const stream = event.req.body | ||
| ``` | ||
| ### Headers | ||
| H3 now uses standard web [`Headers`](https://developer.mozilla.org/en-US/docs/Web/API/Headers). Header values are always plain `string` (no `null`, `undefined`, or `string[]`). | ||
| ```diff | ||
| -- import { getHeader, setHeader, getResponseStatus } from "nitro/h3" | ||
| -- getHeader(event, "x-foo") | ||
| -- setHeader(event, "x-foo", "bar") | ||
| ++ event.req.headers.get("x-foo") | ||
| ++ event.res.headers.set("x-foo", "bar") | ||
| ++ event.res.status // instead of getResponseStatus(event) | ||
| ``` | ||
| ### Handler Utils | ||
| ```diff | ||
| -- import { eventHandler, defineEventHandler } from "nitro/h3" | ||
| ++ import { defineHandler } from "nitro" | ||
| ``` | ||
| - `lazyEventHandler` → `defineLazyEventHandler` | ||
| - `useBase` → `withBase` | ||
| ### Error Utils | ||
| ```diff | ||
| -- import { createError, isError } from "nitro/h3" | ||
| ++ import { HTTPError } from "nitro" | ||
| ++ throw new HTTPError({ status: 404, message: "Not found" }) | ||
| ++ HTTPError.isError(error) | ||
| ``` | ||
| ### Node.js Utils | ||
| ```diff | ||
| -- import { defineNodeListener, fromNodeMiddleware, toNodeListener } from "nitro/h3" | ||
| ++ import { defineNodeHandler, fromNodeHandler, toNodeHandler } from "nitro/h3" | ||
| ``` | ||
| ## Optional Hooks | ||
| If you were using `useNitroApp().hooks` outside of Nitro plugins before, it might be undefined. Use new `useNitroHooks()` to guarantee having an instance. |
| # Nightly Channel | ||
| > Nitro has a nightly release channel that automatically releases for every commit to `main` branch to try latest changes. | ||
| You can opt-in to the nightly release channel by updating your `package.json`: | ||
| ```json | ||
| { | ||
| "devDependencies": { | ||
| "nitro": "npm:nitro-nightly@latest" | ||
| } | ||
| } | ||
| ``` | ||
| Remove the lockfile (`package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`, `bun.lock`, or `bun.lockb`) and reinstall the dependencies. | ||
| <important> | ||
| When using **Bun as package manager** in a mono-repo, you need to make sure nitro package is properly hoisted. | ||
| ```toml [bunfig.toml] | ||
| [install] | ||
| publicHoistPattern = ["nitro*"] | ||
| ``` | ||
| </important> | ||
| <important> | ||
| Avoid using `<npm|pnpm|yarn|bun|deno> install nitro-nightly`; it does not install correctly. | ||
| If you encounter issues, delete your `node_modules` and lock files, then follow the steps above. | ||
| </important> |
| # Plugins | ||
| > Use plugins to extend Nitro's runtime behavior. | ||
| Nitro plugins are **executed once** during server startup in order to allow extending Nitro's runtime behavior. | ||
| They receive `nitroApp` context, which can be used to hook into lifecycle events. | ||
| Plugins are auto-registered from the `plugins/` directory and run synchronously by file name order on the first Nitro initialization. Plugin functions themselves must be synchronous (return `void`), but the hooks they register can be async. | ||
| **Example:** | ||
| ```ts [plugins/test.ts] | ||
| import { definePlugin } from "nitro"; | ||
| export default definePlugin((nitroApp) => { | ||
| console.log('Nitro plugin', nitroApp) | ||
| }) | ||
| ``` | ||
| If you have plugins in another directory, you can use the `plugins` option: | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| plugins: ['my-plugins/hello.ts'] | ||
| }) | ||
| ``` | ||
| ## The `nitroApp` context | ||
| The plugin function receives a `nitroApp` object with the following properties: | ||
| | Property | Type | Description | | ||
| | --- | --- | --- | | ||
| | `hooks` | [`HookableCore`](https://github.com/unjs/hookable) | Hook system for registering lifecycle callbacks. | | ||
| | `h3` | `H3Core` | The underlying [H3](https://github.com/h3js/h3) application instance. | | ||
| | `fetch` | `(req: Request) => Response \| Promise<Response>` | The app's internal fetch handler. | | ||
| | `captureError` | `(error: Error, context) => void` | Programmatically capture errors into the error hook pipeline. | | ||
| ## Nitro runtime hooks | ||
| You can use Nitro [hooks](https://github.com/unjs/hookable) to extend the default runtime behaviour of Nitro by registering custom functions to the lifecycle events within plugins. | ||
| **Example:** | ||
| ```ts | ||
| import { definePlugin } from "nitro"; | ||
| export default definePlugin((nitroApp) => { | ||
| nitroApp.hooks.hook("close", async () => { | ||
| // Will run when nitro is being closed | ||
| }); | ||
| }) | ||
| ``` | ||
| ### Available hooks | ||
| | Hook | Signature | Description | | ||
| | --- | --- | --- | | ||
| | `request` | `(event: HTTPEvent) => void \| Promise<void>` | Called at the start of each request. | | ||
| | `response` | `(res: Response, event: HTTPEvent) => void \| Promise<void>` | Called after the response is created. | | ||
| | `error` | `(error: Error, context: { event?: HTTPEvent, tags?: string[] }) => void` | Called when an error is captured. | | ||
| | `close` | `() => void` | Called when the Nitro server is shutting down. | | ||
| > [!NOTE] | ||
| > The `NitroRuntimeHooks` interface is augmentable. Deployment presets (such as Cloudflare) can extend it with platform-specific hooks like `cloudflare:scheduled` and `cloudflare:email`. | ||
| ### Unregistering hooks | ||
| The `hook()` method returns an unregister function that can be called to remove the hook: | ||
| ```ts | ||
| import { definePlugin } from "nitro"; | ||
| export default definePlugin((nitroApp) => { | ||
| const unregister = nitroApp.hooks.hook("request", (event) => { | ||
| // ... | ||
| }); | ||
| // Later, remove the hook | ||
| unregister(); | ||
| }); | ||
| ``` | ||
| ## Examples | ||
| ### Capturing errors | ||
| You can use plugins to capture all application errors. | ||
| ```ts | ||
| import { definePlugin } from "nitro"; | ||
| export default definePlugin((nitroApp) => { | ||
| nitroApp.hooks.hook("error", async (error, { event }) => { | ||
| console.error(`${event?.path} Application error:`, error) | ||
| }); | ||
| }) | ||
| ``` | ||
| The `context` object includes an optional `tags` array that identifies the error source (e.g., `"request"`, `"response"`, `"cache"`, `"plugin"`, `"unhandledRejection"`, `"uncaughtException"`). | ||
| ### Programmatic error capture | ||
| You can use `captureError` to manually feed errors into the error hook pipeline: | ||
| ```ts | ||
| import { definePlugin } from "nitro"; | ||
| export default definePlugin((nitroApp) => { | ||
| nitroApp.captureError(new Error("something went wrong"), { | ||
| tags: ["startup"], | ||
| }); | ||
| }); | ||
| ``` | ||
| ### Graceful shutdown | ||
| Server will gracefully shutdown and wait for any background pending tasks initiated by `event.waitUntil`. | ||
| ```ts | ||
| import { definePlugin } from "nitro"; | ||
| export default definePlugin((nitroApp) => { | ||
| nitroApp.hooks.hook("close", async () => { | ||
| // Clean up resources, close connections, etc. | ||
| }); | ||
| }); | ||
| ``` | ||
| ### Request and response lifecycle | ||
| You can use plugins to register hooks that run on the request lifecycle: | ||
| ```ts | ||
| import { definePlugin } from "nitro"; | ||
| export default definePlugin((nitroApp) => { | ||
| nitroApp.hooks.hook("request", (event) => { | ||
| console.log("on request", event.path); | ||
| }); | ||
| nitroApp.hooks.hook("response", (res, event) => { | ||
| // Modify or inspect the response | ||
| console.log("on response", res.status); | ||
| }); | ||
| }); | ||
| ``` | ||
| ### Modifying response headers | ||
| ```ts | ||
| import { definePlugin } from "nitro"; | ||
| export default definePlugin((nitroApp) => { | ||
| nitroApp.hooks.hook("response", (res, event) => { | ||
| const { pathname } = new URL(event.req.url); | ||
| if (pathname.endsWith(".css") || pathname.endsWith(".js")) { | ||
| res.headers.append("Vary", "Origin"); | ||
| } | ||
| }); | ||
| }); | ||
| ``` |
| # Quick Start | ||
| > Start with a fresh Nitro project or adopt it in your current Vite project. | ||
| ## Try Nitro online | ||
| Get a taste of Nitro in your browser using our playground. | ||
| [Play with Nitro in StackBlitz](https://stackblitz.com/github/nitrojs/starter/tree/v3-vite?file=index.html,server.ts) | ||
| ## Create a Nitro project | ||
| The fastest way to create a Nitro application is using the `create-nitro-app`. | ||
| > [!NOTE] | ||
| > Make sure to have installed the latest LTS version of either [Node.js](https://nodejs.org/en), [Bun](https://bun.sh/), or [Deno](https://deno.com/). | ||
| <pm-x></pm-x> | ||
| Follow the instructions from the CLI and you will be ready to start your development server. | ||
| ## Add to a Vite project | ||
| You can add Nitro to any existing Vite project to get API routes, server-side rendering, and more. | ||
| <steps> | ||
| ### Install `nitro` and `vite` | ||
| <pm-install></pm-install> | ||
| ### Add Nitro plugin to Vite | ||
| Add the Nitro plugin to your `vite.config.ts`: | ||
| ```ts [vite.config.ts] {2,6} | ||
| import { defineConfig } from "vite"; | ||
| import { nitro } from "nitro/vite"; | ||
| export default defineConfig({ | ||
| plugins: [ | ||
| nitro() | ||
| ], | ||
| }); | ||
| ``` | ||
| ### Configure Nitro | ||
| Create a `nitro.config.ts` to configure the server directory: | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| serverDir: "./server", | ||
| }); | ||
| ``` | ||
| The `serverDir` option tells Nitro where to look for your server routes. In this example, all routes will be inside the `server/` directory. | ||
| ### Create an API route | ||
| Create your first API route at `server/api/test.ts`: | ||
| <code-tree> | ||
| ```ts [server/api/test.ts] | ||
| import { defineHandler } from "nitro"; | ||
| export default defineHandler(() => { | ||
| return { message: "Hello Nitro!" }; | ||
| }); | ||
| ``` | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| serverDir: "./server", | ||
| }); | ||
| ``` | ||
| ```ts [vite.config.ts] | ||
| import { defineConfig } from "vite"; | ||
| import { nitro } from "nitro/vite"; | ||
| export default defineConfig({ | ||
| plugins: [nitro()], | ||
| }); | ||
| ``` | ||
| </code-tree> | ||
| The file path maps directly to the route URL — `server/api/test.ts` becomes `/api/test`. | ||
| <tip> | ||
| As an alternative to filesystem routing, you can declare routes programmatically using the `routes` config option. See [Programmatic route handlers](/docs/routing#programmatic-route-handlers) for more details. | ||
| </tip> | ||
| <tip> | ||
| You can return strings, JSON objects, `Response` instances, or readable streams from your handlers. See [Routing](/docs/routing) for more about dynamic routes, methods, and middleware. | ||
| </tip> | ||
| ### Start the development server | ||
| <pm-run></pm-run> | ||
| Your API route is now accessible at `http://localhost:3000/api/test` :sparkles: | ||
| </steps> |
| # Nitro Renderer | ||
| > Use a renderer to handle all unmatched routes with custom HTML or a templating system. | ||
| The renderer is a special handler in Nitro that catches all routes that don't match any specific API or route handler. It's commonly used for server-side rendering (SSR), serving single-page applications (SPAs), or creating custom HTML responses. | ||
| ## Configuration | ||
| The renderer is configured using the `renderer` option in your Nitro config: | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| renderer: { | ||
| template: './index.html', // Path to HTML template file | ||
| handler: './renderer.ts', // Path to custom renderer handler | ||
| static: false, // Treat template as static HTML (no rendu processing) | ||
| } | ||
| }) | ||
| ``` | ||
| | Option | Type | Description | | ||
| | --- | --- | --- | | ||
| | `template` | `string` | Path to an HTML file used as the renderer template. | | ||
| | `handler` | `string` | Path to a custom renderer handler module. | | ||
| | `static` | `boolean` | When `true`, skips rendu template processing and serves the HTML as-is. Auto-detected based on template syntax when not set. | | ||
| Set `renderer: false` in the config to explicitly disable the renderer entirely (including auto-detection of `index.html`). | ||
| ## HTML template | ||
| ### Auto-detected `index.html` | ||
| By default, Nitro automatically looks for an `index.html` file in your project src dir. | ||
| If found, Nitro will use it as the renderer template and serve it for all unmatched routes. | ||
| <code-group> | ||
| ```html [index.html] | ||
| <!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <title>My Vite + Nitro App</title> | ||
| </head> | ||
| <body> | ||
| <div id="app"></div> | ||
| <script type="module" src="/src/main.ts"></script> | ||
| </body> | ||
| </html> | ||
| ``` | ||
| ```ts [routes/api/hello.ts] | ||
| import { defineHandler } from "nitro"; | ||
| export default defineHandler((event) => { | ||
| return { hello: "API" }; | ||
| }); | ||
| ``` | ||
| </code-group> | ||
| <tip> | ||
| When `index.html` is detected, Nitro will automatically log in the terminal: `Using index.html as renderer template.` | ||
| </tip> | ||
| With this setup: | ||
| - `/api/hello` → Handled by your API routes | ||
| - `/about`, `/contact`, etc. → Served with `index.html` | ||
| ### Custom HTML file | ||
| You can specify a custom HTML template file using the `renderer.template` option in your Nitro configuration. | ||
| <code-group> | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| renderer: { | ||
| template: './app.html' | ||
| } | ||
| }) | ||
| ``` | ||
| ```html [app.html] | ||
| <!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <title>Custom Template</title> | ||
| </head> | ||
| <body> | ||
| <div id="root">Loading...</div> | ||
| <script type="module" src="/src/main.js"></script> | ||
| </body> | ||
| </html> | ||
| ``` | ||
| </code-group> | ||
| ### Static templates | ||
| By default, Nitro auto-detects whether your HTML template contains [rendu](#hypertext-preprocessor-experimental) syntax. If it does, the template is processed dynamically on each request. If it doesn't, it's served as static HTML. | ||
| You can override this behavior with the `static` option: | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| renderer: { | ||
| template: './index.html', | ||
| static: true // Force static serving, skip template processing | ||
| } | ||
| }) | ||
| ``` | ||
| In production, static templates are inlined into the server bundle and served directly for optimal performance. | ||
| ### Hypertext Preprocessor (experimental) | ||
| Nitro uses [rendu](https://github.com/h3js/rendu) Hypertext Preprocessor, which provides a simple and powerful way to create dynamic HTML templates with JavaScript expressions. | ||
| #### Output expressions | ||
| - `{{ expression }}` — HTML-escaped output | ||
| - `{{{ expression }}}` or `<?= expression ?>` — raw (unescaped) output | ||
| ```html | ||
| <h1>Hello {{ $URL.pathname }}</h1> | ||
| <div>{{{ '<strong>raw html</strong>' }}}</div> | ||
| ``` | ||
| #### Control flow | ||
| Use `<? ... ?>` for JavaScript control flow: | ||
| ```html | ||
| <? if ($METHOD === 'POST') { ?> | ||
| <p>Form submitted!</p> | ||
| <? } else { ?> | ||
| <form method="POST"> | ||
| <button type="submit">Submit</button> | ||
| </form> | ||
| <? } ?> | ||
| <ul> | ||
| <? for (const item of ['a', 'b', 'c']) { ?> | ||
| <li>{{ item }}</li> | ||
| <? } ?> | ||
| </ul> | ||
| ``` | ||
| #### Server scripts | ||
| Use `<script server>` to execute JavaScript on the server: | ||
| ```html | ||
| <script server> | ||
| const data = await fetch('https://api.example.com/data').then(r => r.json()); | ||
| </script> | ||
| <pre>{{ JSON.stringify(data) }}</pre> | ||
| ``` | ||
| #### Streaming content | ||
| Use the `echo()` function for streaming content. It accepts strings, functions, Promises, Response objects, or ReadableStreams: | ||
| ```html | ||
| <script server> | ||
| echo("Loading..."); | ||
| echo(async () => fetch("https://api.example.com/data")); | ||
| </script> | ||
| ``` | ||
| #### Global variables | ||
| Access request context within templates: | ||
| | Variable | Description | | ||
| | --- | --- | | ||
| | `$REQUEST` | The incoming `Request` object | | ||
| | `$METHOD` | HTTP method (`GET`, `POST`, etc.) | | ||
| | `$URL` | Request `URL` object | | ||
| | `$HEADERS` | Request headers | | ||
| | `$RESPONSE` | Response configuration object | | ||
| | `$COOKIES` | Read-only object containing request cookies | | ||
| #### Built-in functions | ||
| | Function | Description | | ||
| | --- | --- | | ||
| | `htmlspecialchars(str)` | Escape HTML characters (automatically applied in `{{ }}` syntax) | | ||
| | `setCookie(name, value, options?)` | Set a cookie in the response | | ||
| | `redirect(url)` | Redirect the user to another URL | | ||
| | `echo(content)` | Stream content to the response | | ||
| ```html [index.html] | ||
| <!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <title>Dynamic template</title> | ||
| </head> | ||
| <body> | ||
| <h1>Hello {{ $REQUEST.url }}</h1> | ||
| <p>Welcome, <?= $COOKIES["user"] || "Guest" ?>!</p> | ||
| <script server> | ||
| setCookie("visited", "true", { maxAge: 3600 }); | ||
| </script> | ||
| </body> | ||
| </html> | ||
| ``` | ||
| <read-more></read-more> | ||
| ## Custom renderer handler | ||
| For more complex scenarios, you can create a custom renderer handler that programmatically generates responses. | ||
| The handler is a default export function that receives an H3 event object. You can access the incoming `Request` via `event.req`: | ||
| ```ts [renderer.ts] | ||
| export default function renderer({ req }: { req: Request }) { | ||
| const url = new URL(req.url); | ||
| return new Response( | ||
| /* html */ `<!DOCTYPE html> | ||
| <html> | ||
| <head> | ||
| <title>Custom Renderer</title> | ||
| </head> | ||
| <body> | ||
| <h1>Hello from custom renderer!</h1> | ||
| <p>Current path: ${url.pathname}</p> | ||
| </body> | ||
| </html>`, | ||
| { headers: { "content-type": "text/html; charset=utf-8" } } | ||
| ); | ||
| } | ||
| ``` | ||
| Then, specify the renderer entry in the Nitro config: | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| renderer: { | ||
| handler: './renderer.ts' | ||
| } | ||
| }) | ||
| ``` | ||
| <note> | ||
| When `renderer.handler` is set, it takes full control of rendering. The `renderer.template` option is ignored. | ||
| </note> | ||
| ## Renderer priority | ||
| The renderer always acts as a catch-all route (`/**`) and has the **lowest priority**. This means: | ||
| 1. Specific API routes are matched first (e.g., `/api/users`) | ||
| 2. Specific server routes are matched next (e.g., `/about`) | ||
| 3. The renderer catches everything else | ||
| ```md | ||
| api/ | ||
| users.ts → /api/users (matched first) | ||
| routes/ | ||
| about.ts → /about (matched second) | ||
| renderer.ts → /** (catches all other routes) | ||
| ``` | ||
| <warning> | ||
| If you define a catch-all route (`[...].ts`) in your routes, Nitro will warn you that the renderer will override it. Use more specific routes or different HTTP methods to avoid conflicts. | ||
| </warning> | ||
| <read-more></read-more> | ||
| ## Vite integration | ||
| When using Nitro with Vite, the renderer integrates with Vite's build pipeline and dev server. | ||
| ### Development mode | ||
| In development, the renderer template is read from disk on each request, so changes to `index.html` are reflected immediately without restarting the server. Vite's `transformIndexHtml` hook is applied to inject HMR client scripts and other dev-time transforms. | ||
| ### SSR with `<!--ssr-outlet-->` | ||
| When using Vite environments with an `ssr` service, you can add an `<!--ssr-outlet-->` comment to your `index.html`. Nitro will replace it with the output from your SSR entry during rendering: | ||
| ```html [index.html] | ||
| <!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <title>SSR App</title> | ||
| </head> | ||
| <body> | ||
| <div id="app"><!--ssr-outlet--></div> | ||
| <script type="module" src="/src/main.ts"></script> | ||
| </body> | ||
| </html> | ||
| ``` | ||
| ### Production build | ||
| During production builds, Vite processes the `index.html` through its build pipeline (resolving scripts, CSS, and other assets), then Nitro inlines the transformed HTML into the server bundle. | ||
| ## Use Cases | ||
| ### Single-Page Application (SPA) | ||
| Serve your SPA's `index.html` for all routes to enable client-side routing: | ||
| > [!TIP] | ||
| > This is the default behavior of Nitro when used with Vite. |
| # Routing | ||
| > Nitro supports filesystem routing to automatically map files to routes. By combining code-splitting with compiled routes, it removes the need for a runtime router, leaving only minimal compiled logic. | ||
| ## Request handler | ||
| Nitro request handler is a function accepting an `event` object, which is a [H3Event](https://h3.dev/guide/api/h3event#h3event-properties) object. | ||
| <code-group> | ||
| ```ts [Single function] | ||
| import type { H3Event } from "nitro"; | ||
| export default (event: H3Event) => { | ||
| return "world"; | ||
| } | ||
| ``` | ||
| ```ts [defineHandler] | ||
| import { defineHandler } from "nitro"; | ||
| // For better type inference | ||
| export default defineHandler((event) => { | ||
| return "world"; | ||
| }); | ||
| ``` | ||
| </code-group> | ||
| ## Filesystem routing | ||
| Nitro supports file-based routing for your API routes (files are automatically mapped to [h3 routes](https://h3.dev/guide/basics/routing)). Defining a route is as simple as creating a file inside the `api/` or `routes/` directory. | ||
| You can only define one handler per files and you can [append the HTTP method](#specific-request-method) to the filename to define a specific request method. | ||
| ``` | ||
| routes/ | ||
| api/ | ||
| test.ts <-- /api/test | ||
| hello.get.ts <-- /hello (GET only) | ||
| hello.post.ts <-- /hello (POST only) | ||
| vite.config.ts | ||
| ``` | ||
| You can nest routes by creating subdirectories. | ||
| ```txt | ||
| routes/ | ||
| api/ | ||
| [org]/ | ||
| [repo]/ | ||
| index.ts <-- /api/:org/:repo | ||
| issues.ts <-- /api/:org/:repo/issues | ||
| index.ts <-- /api/:org | ||
| package.json | ||
| ``` | ||
| #### Route Groups | ||
| In some cases, you may want to group a set of routes together in a way which doesn't affect file-based routing. For this purpose, you can put files in a folder which is wrapped in parentheses `(` and `)`. | ||
| For example: | ||
| ```txt | ||
| routes/ | ||
| api/ | ||
| (admin)/ | ||
| users.ts <-- /api/users | ||
| reports.ts <-- /api/reports | ||
| (public)/ | ||
| index.ts <-- /api | ||
| package.json | ||
| ``` | ||
| > [!NOTE] The route groups are not part of the route definition and are only used for organization purposes. | ||
| ### Static routes | ||
| First, create a file in `routes/` or `routes/api/` directory. The filename will be the route path. | ||
| Then, export a fetch-compatible function: | ||
| ```ts [routes/api/test.ts] | ||
| import { defineHandler } from "nitro"; | ||
| export default defineHandler(() => { | ||
| return { hello: "API" }; | ||
| }); | ||
| ``` | ||
| ### Dynamic routes | ||
| #### Single param | ||
| To define a route with params, use the `[<param>]` syntax where `<param>` is the name of the param. The param will be available in the `event.context.params` object or using the [`getRouterParam`](https://h3.dev/utils/request#getrouterparamevent-name-opts-decode) utility. | ||
| ```ts [routes/hello/[name\].ts] | ||
| import { defineHandler } from "nitro"; | ||
| export default defineHandler((event) => { | ||
| const { name } = event.context.params; | ||
| return `Hello ${name}!`; | ||
| }); | ||
| ``` | ||
| Call the route with the param `/hello/nitro`, you will get: | ||
| ```txt [Response] | ||
| Hello nitro! | ||
| ``` | ||
| #### Multiple params | ||
| You can define multiple params in a route using `[<param1>]/[<param2>]` syntax where each param is a folder. You **cannot** define multiple params in a single filename of folder. | ||
| ```ts [routes/hello/[name\]/[age\].ts] | ||
| import { defineHandler } from "nitro"; | ||
| export default defineHandler((event) => { | ||
| const { name, age } = event.context.params; | ||
| return `Hello ${name}! You are ${age} years old.`; | ||
| }); | ||
| ``` | ||
| #### Catch-all params | ||
| You can capture all the remaining parts of a URL using `[...<param>]` syntax. This will include the `/` in the param. | ||
| ```ts [routes/hello/[...name\].ts] | ||
| import { defineHandler } from "nitro"; | ||
| export default defineHandler((event) => { | ||
| const { name } = event.context.params; | ||
| return `Hello ${name}!`; | ||
| }); | ||
| ``` | ||
| Call the route with the param `/hello/nitro/is/hot`, you will get: | ||
| ```txt [Response] | ||
| Hello nitro/is/hot! | ||
| ``` | ||
| ### Specific request method | ||
| You can append the HTTP method to the filename to force the route to be matched only for a specific HTTP request method, for example `hello.get.ts` will only match for `GET` requests. You can use any HTTP method you want. | ||
| Supported methods: `get`, `post`, `put`, `delete`, `patch`, `head`, `options`, `connect`, `trace`. | ||
| <code-group> | ||
| ```js [GET] | ||
| // routes/users/[id].get.ts | ||
| import { defineHandler } from "nitro"; | ||
| export default defineHandler(async (event) => { | ||
| const { id } = event.context.params; | ||
| // Do something with id | ||
| return `User profile!`; | ||
| }); | ||
| ``` | ||
| ```js [POST] | ||
| // routes/users.post.ts | ||
| import { defineHandler } from "nitro"; | ||
| export default defineHandler(async (event) => { | ||
| const body = await event.req.json(); | ||
| // Do something with body like saving it to a database | ||
| return { updated: true }; | ||
| }); | ||
| ``` | ||
| </code-group> | ||
| ### Catch-all route | ||
| You can create a special route that will match all routes that are not matched by any other route. This is useful for creating a default route. | ||
| To create a catch-all route, create a file named `[...].ts`. | ||
| ```ts [routes/[...\].ts] | ||
| import { defineHandler } from "nitro"; | ||
| export default defineHandler((event) => { | ||
| return `Hello ${event.url}!`; | ||
| }); | ||
| ``` | ||
| ### Environment specific handlers | ||
| You can specify for a route that will only be included in specific builds by adding a `.dev`, `.prod` or `.prerender` suffix to the file name, for example: `routes/test.get.dev.ts` or `routes/test.get.prod.ts`. | ||
| The suffix is placed after the method suffix (if any): | ||
| ```txt | ||
| routes/ | ||
| env/ | ||
| index.dev.ts <-- /env (dev only) | ||
| index.get.prod.ts <-- /env (GET, prod only) | ||
| ``` | ||
| > [!TIP] | ||
| > You can specify multiple environments or specify a preset name as environment using programmatic registration of routes via [`routes`](#routes-config) config. | ||
| ### Ignoring files | ||
| You can use the `ignore` config option to exclude files from route scanning. It accepts an array of glob patterns relative to the server directory. | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| ignore: [ | ||
| "routes/api/**/_*", // Ignore files starting with _ in api/ | ||
| "middleware/_*.ts", // Ignore middleware starting with _ | ||
| "routes/_*.ts", // Ignore root routes starting with _ | ||
| ], | ||
| }); | ||
| ``` | ||
| ## Programmatic route handlers | ||
| In addition to filesystem routing, you can register route handlers programmatically using the `routes` config option. | ||
| ### `routes` config | ||
| The `routes` option allows you to map route patterns to handlers: | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| routes: { | ||
| "/api/hello": "./server/routes/api/hello.ts", | ||
| "/api/custom": { | ||
| handler: "./server/routes/api/hello.ts", | ||
| method: "POST", | ||
| lazy: true, | ||
| }, | ||
| "/virtual": { | ||
| handler: "#virtual-route", | ||
| }, | ||
| }, | ||
| }); | ||
| ``` | ||
| Each route entry can be a simple string (handler path) or an object with the following options: | ||
| | Option | Type | Description | | ||
| | --- | --- | --- | | ||
| | `handler` | `string` | Path to event handler file or virtual module ID | | ||
| | `method` | `string` | HTTP method to match (`get`, `post`, etc.) | | ||
| | `lazy` | `boolean` | Use lazy loading to import handler | | ||
| | `format` | `"web" \| "node"` | Handler type. `"node"` handlers are converted to web-compatible | | ||
| | `env` | `string \| string[]` | Environments to include this handler (`"dev"`, `"prod"`, `"prerender"`, or a preset name) | | ||
| ### `handlers` config | ||
| The `handlers` array is useful for registering middleware with control over route matching: | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| handlers: [ | ||
| { | ||
| route: "/api/**", | ||
| handler: "./server/middleware/api-auth.ts", | ||
| middleware: true, | ||
| }, | ||
| ], | ||
| }); | ||
| ``` | ||
| Each handler entry supports the following options: | ||
| | Option | Type | Description | | ||
| | --- | --- | --- | | ||
| | `route` | `string` | HTTP pathname pattern (e.g., `/test`, `/api/:id`, `/blog/**`) | | ||
| | `handler` | `string` | Path to event handler file or virtual module ID | | ||
| | `method` | `string` | HTTP method to match (`get`, `post`, etc.) | | ||
| | `middleware` | `boolean` | Run handler as middleware before route handlers | | ||
| | `lazy` | `boolean` | Use lazy loading to import handler | | ||
| | `format` | `"web" \| "node"` | Handler type. `"node"` handlers are converted to web-compatible | | ||
| | `env` | `string \| string[]` | Environments to include this handler (`"dev"`, `"prod"`, `"prerender"`, or a preset name) | | ||
| ## Middleware | ||
| Nitro route middleware can hook into the request lifecycle. | ||
| <tip> | ||
| A middleware can modify the request before it is processed, not after. | ||
| </tip> | ||
| Middleware are auto-registered within the `middleware/` directory. | ||
| ```md | ||
| middleware/ | ||
| auth.ts | ||
| logger.ts | ||
| ... | ||
| routes/ | ||
| hello.ts | ||
| ``` | ||
| ### Simple middleware | ||
| Middleware are defined exactly like route handlers with the only exception that they should not return anything. | ||
| Returning from middleware behaves like returning from a request - the value will be returned as a response and further code will not be ran. | ||
| ```ts [middleware/auth.ts] | ||
| import { defineHandler } from "nitro"; | ||
| export default defineHandler((event) => { | ||
| // Extends or modify the event | ||
| event.context.user = { name: "Nitro" }; | ||
| }); | ||
| ``` | ||
| Middleware in `middleware/` directory are automatically registered for all routes. If you want to register a middleware for a specific route, see [Object Syntax Event Handler](https://h3.dev/guide/basics/handler#object-syntax). | ||
| <note> | ||
| Returning anything from a middleware will close the request and should be avoided! Any returned value from middleware will be the response and further code will not be executed however **this is not recommended to do!** | ||
| </note> | ||
| ### Route Meta | ||
| You can define route handler meta at build-time using `defineRouteMeta` macro in the event handler files. | ||
| > [!IMPORTANT] | ||
| > This feature is currently experimental. | ||
| ```ts [routes/api/test.ts] | ||
| import { defineRouteMeta } from "nitro"; | ||
| import { defineHandler } from "nitro"; | ||
| defineRouteMeta({ | ||
| openAPI: { | ||
| tags: ["test"], | ||
| description: "Test route description", | ||
| parameters: [{ in: "query", name: "test", required: true }], | ||
| }, | ||
| }); | ||
| export default defineHandler(() => "OK"); | ||
| ``` | ||
| <read-more> | ||
| This feature is currently usable to specify OpenAPI meta. See swagger specification for available OpenAPI options. | ||
| </read-more> | ||
| ### Execution order | ||
| Middleware are executed in directory listing order. | ||
| ```md | ||
| middleware/ | ||
| auth.ts <-- First | ||
| logger.ts <-- Second | ||
| ... <-- Third | ||
| ``` | ||
| Prefix middleware with a number to control their execution order. | ||
| ```md | ||
| middleware/ | ||
| 1.logger.ts <-- First | ||
| 2.auth.ts <-- Second | ||
| 3.... <-- Third | ||
| ``` | ||
| <note> | ||
| Remember that file names are sorted as strings, thus for example if you have 3 files `1.filename.ts`, `2.filename.ts` and `10.filename.ts`, the `10.filename.ts` will come after the `1.filename.ts`. To avoid this, prefix `1-9` with a `0` like `01`, if you have more than 10 middleware in the same directory. | ||
| </note> | ||
| ### Request filtering | ||
| Middleware are executed on every request. | ||
| Apply custom logic to scope them to specific conditions. | ||
| For example, you can use the URL to apply a middleware to a specific route: | ||
| ```ts [middleware/auth.ts] | ||
| import { defineHandler } from "nitro"; | ||
| export default defineHandler((event) => { | ||
| // Will only execute for /auth route | ||
| if (event.url.pathname.startsWith('/auth')) { | ||
| event.context.user = { name: "Nitro" }; | ||
| } | ||
| }); | ||
| ``` | ||
| ### Route-scoped middleware | ||
| You can register middleware for specific route patterns using the [`handlers`](#handlers-config) config with the `middleware` option and a specific `route`: | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| handlers: [ | ||
| { | ||
| route: "/api/**", | ||
| handler: "./server/middleware/api-auth.ts", | ||
| middleware: true, | ||
| }, | ||
| ], | ||
| }); | ||
| ``` | ||
| Unlike global middleware (registered in the `middleware/` directory which match `/**`), route-scoped middleware only run for requests matching the specified pattern. | ||
| ## Error handling | ||
| You can use the [utilities available in H3](https://h3.dev/guide/basics/error) to handle errors in both routes and middlewares. | ||
| The way errors are sent back to the client depends on the environment. In development, requests with an `Accept` header of `text/html` (such as browsers) will receive a HTML error page. In production, errors are always sent in JSON. | ||
| This behaviour can be overridden by some request properties (e.g.: `Accept` or `User-Agent` headers). | ||
| ## Code splitting | ||
| Nitro creates a separate chunk for each route handler. Chunks load on-demand when first requested, so `/api/users` doesn't load code for `/api/posts`. | ||
| See [`inlineDynamicImports`](/config#inlinedynamicimports) to bundle everything into a single file. | ||
| ## Route rules | ||
| Nitro allows you to add logic at the top-level for each route of your configuration. It can be used for redirecting, proxying, caching, authentication, and adding headers to routes. | ||
| It is a map from route pattern (following [rou3](https://github.com/h3js/rou3)) to route options. | ||
| When `cache` option is set, handlers matching pattern will be automatically wrapped with `defineCachedEventHandler`. See the [cache guide](/docs/cache) to learn more about this function. | ||
| <note> | ||
| `swr: true|number` is shortcut for `cache: { swr: true, maxAge: number }` | ||
| </note> | ||
| You can set route rules in the `nitro.routeRules` options. | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| routeRules: { | ||
| '/blog/**': { swr: true }, | ||
| '/blog/**': { swr: 600 }, | ||
| '/blog/**': { static: true }, | ||
| '/blog/**': { cache: { /* cache options*/ } }, | ||
| '/assets/**': { headers: { 'cache-control': 's-maxage=0' } }, | ||
| '/api/v1/**': { cors: true, headers: { 'access-control-allow-methods': 'GET' } }, | ||
| '/old-page': { redirect: '/new-page' }, | ||
| '/old-page/**': { redirect: '/new-page/**' }, | ||
| '/proxy/example': { proxy: 'https://example.com' }, | ||
| '/proxy/**': { proxy: '/api/**' }, | ||
| '/admin/**': { basicAuth: { username: 'admin', password: 'supersecret' } }, | ||
| } | ||
| }); | ||
| ``` | ||
| ### Rule merging and overrides | ||
| Route rules are matched from least specific to most specific. When multiple rules match a request, their options are merged, with more specific rules taking precedence. | ||
| You can use `false` to disable a rule that was set by a more general pattern: | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| routeRules: { | ||
| '/api/cached/**': { swr: true }, | ||
| '/api/cached/no-cache': { cache: false, swr: false }, | ||
| '/admin/**': { basicAuth: { username: 'admin', password: 'secret' } }, | ||
| '/admin/public/**': { basicAuth: false }, | ||
| } | ||
| }); | ||
| ``` | ||
| ### Headers | ||
| Set custom response headers for matching routes: | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| routeRules: { | ||
| '/api/**': { headers: { 'cache-control': 's-maxage=60' } }, | ||
| '**': { headers: { 'x-powered-by': 'Nitro' } }, | ||
| } | ||
| }); | ||
| ``` | ||
| ### CORS | ||
| Enable CORS headers with the `cors: true` shortcut. This sets `access-control-allow-origin: *`, `access-control-allow-methods: *`, `access-control-allow-headers: *`, and `access-control-max-age: 0`. | ||
| You can override individual CORS headers using `headers`: | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| routeRules: { | ||
| '/api/v1/**': { | ||
| cors: true, | ||
| headers: { 'access-control-allow-methods': 'GET' }, | ||
| }, | ||
| } | ||
| }); | ||
| ``` | ||
| ### Redirect | ||
| Redirect matching routes to another URL. Use a string for a simple redirect (defaults to `307` status), or an object for more control: | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| routeRules: { | ||
| // Simple redirect (307 status) | ||
| '/old-page': { redirect: '/new-page' }, | ||
| // Redirect with custom status | ||
| '/legacy': { redirect: { to: 'https://example.com/', status: 308 } }, | ||
| // Wildcard redirect — preserves the path after the pattern | ||
| '/old-blog/**': { redirect: 'https://blog.example.com/**' }, | ||
| } | ||
| }); | ||
| ``` | ||
| ### Proxy | ||
| Proxy requests to another URL. Supports both internal and external targets: | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| routeRules: { | ||
| // Proxy to exact URL | ||
| '/api/proxy/example': { proxy: 'https://example.com' }, | ||
| // Proxy to internal route | ||
| '/api/proxy/**': { proxy: '/api/echo' }, | ||
| // Wildcard proxy — preserves the path after the pattern | ||
| '/cdn/**': { proxy: 'https://cdn.jsdelivr.net/**' }, | ||
| // Proxy with options | ||
| '/external/**': { | ||
| proxy: { | ||
| to: 'https://api.example.com/**', | ||
| // Additional H3 proxy options... | ||
| }, | ||
| }, | ||
| } | ||
| }); | ||
| ``` | ||
| ### Basic auth | ||
| Protect routes with HTTP Basic Authentication: | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| routeRules: { | ||
| '/admin/**': { | ||
| basicAuth: { | ||
| username: 'admin', | ||
| password: 'supersecret', | ||
| realm: 'Admin Area', // Optional, shown in the browser prompt | ||
| }, | ||
| }, | ||
| // Disable basic auth for a sub-path | ||
| '/admin/public/**': { basicAuth: false }, | ||
| } | ||
| }); | ||
| ``` | ||
| ### Caching (SWR / Static) | ||
| Control caching behavior with `cache`, `swr`, or `static` options: | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| routeRules: { | ||
| // Enable stale-while-revalidate caching | ||
| '/blog/**': { swr: true }, | ||
| // SWR with maxAge in seconds | ||
| '/blog/posts/**': { swr: 600 }, | ||
| // Full cache options | ||
| '/api/data/**': { | ||
| cache: { | ||
| maxAge: 60, | ||
| swr: true, | ||
| // ...other cache options | ||
| }, | ||
| }, | ||
| // Disable caching | ||
| '/api/realtime/**': { cache: false }, | ||
| } | ||
| }); | ||
| ``` | ||
| <tip> | ||
| `swr: true` is a shortcut for `cache: { swr: true }` and `swr: <number>` is a shortcut for `cache: { swr: true, maxAge: <number> }`. | ||
| </tip> | ||
| ### Prerender | ||
| Mark routes for prerendering at build time: | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| routeRules: { | ||
| '/about': { prerender: true }, | ||
| '/dynamic/**': { prerender: false }, | ||
| } | ||
| }); | ||
| ``` | ||
| ### ISR (Vercel) | ||
| Configure Incremental Static Regeneration for Vercel deployments: | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| routeRules: { | ||
| '/isr/**': { isr: true }, | ||
| '/isr-ttl/**': { isr: 60 }, | ||
| '/isr-custom/**': { | ||
| isr: { | ||
| expiration: 60, | ||
| allowQuery: ['q'], | ||
| group: 1, | ||
| }, | ||
| }, | ||
| } | ||
| }); | ||
| ``` | ||
| ### Route rules reference | ||
| | Option | Type | Description | | ||
| | --- | --- | --- | | ||
| | `headers` | `Record<string, string>` | Custom response headers | | ||
| | `redirect` | `string \| { to: string, status?: number }` | Redirect to another URL (default status: `307`) | | ||
| | `proxy` | `string \| { to: string, ...proxyOptions }` | Proxy requests to another URL | | ||
| | `cors` | `boolean` | Enable permissive CORS headers | | ||
| | `cache` | `object \| false` | Cache options (see [cache guide](/docs/cache)) | | ||
| | `swr` | `boolean \| number` | Shortcut for `cache: { swr: true, maxAge: number }` | | ||
| | `static` | `boolean \| number` | Shortcut for static caching | | ||
| | `basicAuth` | `{ username, password, realm? } \| false` | HTTP Basic Authentication | | ||
| | `prerender` | `boolean` | Enable/disable prerendering | | ||
| | `isr` | `boolean \| number \| object` | Incremental Static Regeneration (Vercel) | | ||
| ### Runtime route rules | ||
| Route rules can be provided through `runtimeConfig`, allowing overrides via environment variables without rebuilding: | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| runtimeConfig: { | ||
| nitro: { | ||
| routeRules: { | ||
| '/api/**': { headers: { 'x-env': 'production' } }, | ||
| }, | ||
| }, | ||
| }, | ||
| }); | ||
| ``` | ||
| ## Config reference | ||
| These config options control routing behavior: | ||
| | Option | Type | Default | Description | | ||
| | --- | --- | --- | --- | | ||
| | `baseURL` | `string` | `"/"` | Base URL for all routes | | ||
| | `apiBaseURL` | `string` | `"/api"` | Base URL for routes in the `api/` directory | | ||
| | `apiDir` | `string` | `"api"` | Directory name for API routes | | ||
| | `routesDir` | `string` | `"routes"` | Directory name for file-based routes | | ||
| | `serverDir` | `string \| false` | `false` | Server directory for scanning routes, middleware, plugins, etc. | | ||
| | `scanDirs` | `string[]` | `[]` | Additional directories to scan for routes | | ||
| | `routes` | `Record<string, string \| handler>` | `{}` | Route-to-handler mapping | | ||
| | `handlers` | `NitroEventHandler[]` | `[]` | Programmatic handler registration (mainly for middleware) | | ||
| | `routeRules` | `Record<string, NitroRouteConfig>` | `{}` | Route rules for matching patterns | | ||
| | `ignore` | `string[]` | `[]` | Glob patterns to ignore during file scanning | |
| # Nitro Server Entry | ||
| > Use a server entry to create a global middleware that runs for all routes before they are matched. | ||
| The server entry is a special handler in Nitro that acts as a global middleware, running for every incoming request before routes are matched. It's commonly used for cross-cutting concerns like authentication, logging, request preprocessing, or creating custom routing logic. | ||
| ## Auto-detected `server.ts` | ||
| By default, Nitro automatically looks for a `server.ts` (or `.js`, `.mjs`, `.mts`, `.tsx`, `.jsx`) file in your project root directory. | ||
| If found, Nitro will use it as the server entry and run it for all incoming requests. | ||
| <code-group> | ||
| ```ts [server.ts] | ||
| export default { | ||
| async fetch(req: Request) { | ||
| const url = new URL(req.url); | ||
| // Handle specific routes | ||
| if (url.pathname === "/health") { | ||
| return new Response("OK", { | ||
| status: 200, | ||
| headers: { "content-type": "text/plain" } | ||
| }); | ||
| } | ||
| // Add custom headers to all requests | ||
| // Return nothing to continue to the next handler | ||
| } | ||
| } | ||
| ``` | ||
| ```ts [routes/api/hello.ts] | ||
| import { defineHandler } from "nitro"; | ||
| export default defineHandler((event) => { | ||
| return { hello: "API" }; | ||
| }); | ||
| ``` | ||
| </code-group> | ||
| <tip> | ||
| When `server.ts` is detected, Nitro will log in the terminal: `Detected \`server.ts` as server entry.` | ||
| </tip> | ||
| With this setup: | ||
| - `/health` → Handled by server entry (returns a response) | ||
| - `/api/hello` → Handled by the API route handler directly | ||
| - `/about`, etc. → Server entry runs first, then continues to the renderer if no response is returned | ||
| ## Framework compatibility | ||
| The server entry is a great way to integrate with other frameworks. Any framework that exposes a standard Web `fetch(request: Request): Response` interface can be used as a server entry. | ||
| ### Web-compatible frameworks | ||
| Frameworks that implement the Web `fetch` API work directly with `server.ts`: | ||
| <tabs> | ||
| <tabs-item> | ||
| ```ts [server.ts] | ||
| import { H3 } from "h3"; | ||
| const app = new H3() | ||
| app.get("/", () => "⚡️ Hello from H3!"); | ||
| export default app; | ||
| ``` | ||
| </tabs-item> | ||
| <tabs-item> | ||
| ```ts [server.ts] | ||
| import { Hono } from "hono"; | ||
| const app = new Hono(); | ||
| app.get("/", (c) => c.text("🔥 Hello from Hono!")); | ||
| export default app; | ||
| ``` | ||
| </tabs-item> | ||
| <tabs-item> | ||
| ```ts [server.ts] | ||
| import { Elysia } from "elysia"; | ||
| const app = new Elysia(); | ||
| app.get("/", () => "🦊 Hello from Elysia!"); | ||
| export default app.compile(); | ||
| ``` | ||
| </tabs-item> | ||
| </tabs> | ||
| ### Node.js frameworks | ||
| For Node.js frameworks that use `(req, res)` style handlers (like [Express](https://expressjs.com/) or [Fastify](https://fastify.dev/)), name your server entry file `server.node.ts` instead of `server.ts`. Nitro will automatically detect the `.node.` suffix and convert the Node.js handler to a web-compatible fetch handler using [`srvx`](https://srvx.h3.dev/). | ||
| <tabs> | ||
| <tabs-item> | ||
| ```ts [server.node.ts] | ||
| import Express from "express"; | ||
| const app = Express(); | ||
| app.use("/", (_req, res) => { | ||
| res.send("Hello from Express with Nitro!"); | ||
| }); | ||
| export default app; | ||
| ``` | ||
| </tabs-item> | ||
| <tabs-item> | ||
| ```ts [server.node.ts] | ||
| import Fastify from "fastify"; | ||
| const app = Fastify(); | ||
| app.get("/", () => "Hello, Fastify with Nitro!"); | ||
| await app.ready(); | ||
| export default app.routing; | ||
| ``` | ||
| </tabs-item> | ||
| </tabs> | ||
| ## Configuration | ||
| ### Custom server entry file | ||
| You can specify a custom server entry file using the `serverEntry` option in your Nitro configuration: | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| serverEntry: "./nitro.server.ts" | ||
| }) | ||
| ``` | ||
| You can also provide an object with `handler` and `format` options: | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| serverEntry: { | ||
| handler: "./server.ts", | ||
| format: "node" // "web" (default) or "node" | ||
| } | ||
| }) | ||
| ``` | ||
| ### Handler format | ||
| The `format` option controls how Nitro treats the default export of your server entry: | ||
| - **`"web"`** (default) — Expects a Web-compatible handler with a `fetch(request: Request): Response` method. | ||
| - **`"node"`** — Expects a Node.js-style `(req, res)` handler. Nitro automatically converts it to a web-compatible handler. | ||
| When auto-detecting, the format is determined by the filename: `server.node.ts` uses `"node"` format, while `server.ts` uses `"web"` format. | ||
| ### Disabling server entry | ||
| Set `serverEntry` to `false` to disable auto-detection and prevent Nitro from using any server entry: | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| serverEntry: false | ||
| }) | ||
| ``` | ||
| ## Using event handler | ||
| You can also export an event handler using `defineHandler` for better type inference and access to the h3 event object: | ||
| ```ts [server.ts] | ||
| import { defineHandler } from "nitro"; | ||
| export default defineHandler((event) => { | ||
| // Add custom context | ||
| event.context.requestId = crypto.randomUUID(); | ||
| event.context.timestamp = Date.now(); | ||
| // Log the request | ||
| console.log(`[${event.context.requestId}] ${event.method} ${event.path}`); | ||
| // Continue to the next handler (don't return anything) | ||
| }); | ||
| ``` | ||
| <important> | ||
| If your server entry returns `undefined` or doesn't return anything, the request will continue to be processed by routes and the renderer. If it returns a response, the request lifecycle stops there. | ||
| </important> | ||
| ## Request lifecycle | ||
| The server entry is registered as a catch-all (`/**`) route handler. When a specific route (like `/api/hello`) matches a request, that route handler takes priority. For requests that don't match any specific route, the server entry runs before the renderer: | ||
| ```md | ||
| 1. Server hook: `request` | ||
| 2. Route rules (headers, redirects, etc.) | ||
| 3. Global middleware (middleware/) | ||
| 4. Route matching: | ||
| a. Specific routes (routes/) ← if matched, handles the request | ||
| b. Server entry ← runs for unmatched routes | ||
| c. Renderer (renderer.ts or index.html) | ||
| ``` | ||
| When both a server entry and a renderer exist, they are chained: the server entry runs first, and if it doesn't return a response, the renderer handles the request. | ||
| ## Development mode | ||
| During development, Nitro watches for changes to your server entry file. When the file is created, modified, or deleted, the dev server automatically reloads to pick up the changes. | ||
| ## Best practices | ||
| - Use server entry for cross-cutting concerns that affect **all routes** | ||
| - Return `undefined` to continue processing, return a response to terminate | ||
| - Keep server entry logic lightweight for better performance | ||
| - Use global middleware for modular concerns instead of one large server entry | ||
| - Consider using [Nitro plugins](/docs/plugins) for initialization logic | ||
| - Avoid heavy computation in server entry (it runs for every request) | ||
| - Don't use server entry for route-specific logic (use route handlers instead as they are more performant) |
| # KV Storage | ||
| > Nitro provides a built-in storage layer that can abstract filesystem or database or any other data source. | ||
| Nitro has built-in integration with [unstorage](https://unstorage.unjs.io) to provide a runtime agnostic persistent layer. | ||
| ## Usage | ||
| To use the storage layer, you can use the `useStorage()` utility to access the storage instance. | ||
| ```ts | ||
| import { useStorage } from "nitro/storage"; | ||
| // Default storage (in-memory) | ||
| await useStorage().setItem("test:foo", { hello: "world" }); | ||
| const value = await useStorage().getItem("test:foo"); | ||
| // You can specify a base prefix with useStorage(base) | ||
| const testStorage = useStorage("test"); | ||
| await testStorage.setItem("foo", { hello: "world" }); | ||
| await testStorage.getItem("foo"); // { hello: "world" } | ||
| // You can use generics to type the return value | ||
| await useStorage<{ hello: string }>("test").getItem("foo"); | ||
| await useStorage("test").getItem<{ hello: string }>("foo"); | ||
| ``` | ||
| <read-more></read-more> | ||
| ### Available methods | ||
| The storage instance returned by `useStorage()` provides the following methods: | ||
| | Method | Description | | ||
| | --- | --- | | ||
| | `getItem(key)` | Get the value of a key. Returns `null` if the key does not exist. | | ||
| | `getItems(items)` | Get multiple items at once. Accepts an array of keys or `{ key, options }` objects. | | ||
| | `getItemRaw(key)` | Get the raw value of a key without parsing. Useful for binary data. | | ||
| | `setItem(key, value)` | Set the value of a key. | | ||
| | `setItems(items)` | Set multiple items at once. Accepts an array of `{ key, value }` objects. | | ||
| | `setItemRaw(key, value)` | Set the raw value of a key without serialization. | | ||
| | `hasItem(key)` | Check if a key exists. Returns a boolean. | | ||
| | `removeItem(key)` | Remove a key from storage. | | ||
| | `getKeys(base?)` | Get all keys, optionally filtered by a base prefix. | | ||
| | `clear(base?)` | Clear all keys, optionally filtered by a base prefix. | | ||
| | `getMeta(key)` | Get metadata for a key (e.g., `mtime`, `atime`, `ttl`). | | ||
| | `setMeta(key, meta)` | Set metadata for a key. | | ||
| | `removeMeta(key)` | Remove metadata for a key. | | ||
| | `mount(base, driver)` | Dynamically mount a storage driver at a base path. | | ||
| | `unmount(base)` | Unmount a storage driver from a base path. | | ||
| | `watch(callback)` | Watch for changes. Callback receives `(event, key)` where event is `"update"` or `"remove"`. | | ||
| | `unwatch()` | Stop watching for changes. | | ||
| Shorthand aliases are also available: `get`, `set`, `has`, `del`, `remove`, `keys`. | ||
| ```ts | ||
| import { useStorage } from "nitro/storage"; | ||
| // Get all keys under a prefix | ||
| const keys = await useStorage("test").getKeys(); | ||
| // Check if a key exists | ||
| const exists = await useStorage().hasItem("test:foo"); | ||
| // Remove a key | ||
| await useStorage().removeItem("test:foo"); | ||
| // Get raw binary data | ||
| const raw = await useStorage().getItemRaw("assets/server:image.png"); | ||
| // Get metadata (type, etag, mtime, etc.) | ||
| const meta = await useStorage("assets/server").getMeta("file.txt"); | ||
| ``` | ||
| ## Configuration | ||
| You can mount one or multiple custom storage drivers using the `storage` option. | ||
| The key is the mount point name, and the value is the driver name and configuration. | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| storage: { | ||
| redis: { | ||
| driver: "redis", | ||
| /* redis connector options */ | ||
| } | ||
| } | ||
| }) | ||
| ``` | ||
| Then, you can use the redis storage using the `useStorage("redis")` function. | ||
| <read-more> | ||
| You can find the driver list on [unstorage documentation](https://unstorage.unjs.io/) with their configuration. | ||
| </read-more> | ||
| ### Development storage | ||
| You can use the `devStorage` option to override storage configuration during development and prerendering. | ||
| This is useful when your production driver is not available in development (e.g., a managed Redis instance). | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| storage: { | ||
| db: { | ||
| driver: "redis", | ||
| host: "prod.example.com", | ||
| } | ||
| }, | ||
| devStorage: { | ||
| db: { | ||
| driver: "fs", | ||
| base: "./.data/db" | ||
| } | ||
| } | ||
| }) | ||
| ``` | ||
| When running in development mode, `devStorage` mounts are merged on top of `storage` mounts, allowing you to use a local filesystem driver or an in-memory driver while developing. | ||
| ## Built-in mount points | ||
| Nitro automatically mounts the following storage paths: | ||
| ### `/assets` | ||
| Server assets are mounted at the `/assets` base path. This mount point provides read-only access to bundled server assets (see [Server assets](#server-assets)). | ||
| ```ts | ||
| import { useStorage } from "nitro/storage"; | ||
| // Access server assets via the /assets mount | ||
| const content = await useStorage("assets/server").getItem("my-file.txt"); | ||
| ``` | ||
| ### Default (in-memory) | ||
| The root storage (without a base path) uses an in-memory driver by default. Data stored here is not persisted across restarts. | ||
| ```ts | ||
| import { useStorage } from "nitro/storage"; | ||
| // In-memory by default, not persisted | ||
| await useStorage().setItem("counter", 1); | ||
| ``` | ||
| To persist data, mount a driver with a persistent backend (e.g., `fs`, `redis`, etc.) using the `storage` configuration option. | ||
| ## Server assets | ||
| Nitro allows you to bundle files from an `assets/` directory at the root of your project. These files are accessible at runtime via the `assets/server` storage mount. | ||
| ``` | ||
| my-project/ | ||
| assets/ | ||
| data.json | ||
| templates/ | ||
| welcome.html | ||
| server/ | ||
| routes/ | ||
| index.ts | ||
| ``` | ||
| ```ts [server/routes/index.ts] | ||
| import { useStorage } from "nitro/storage"; | ||
| export default defineHandler(async () => { | ||
| const serverAssets = useStorage("assets/server"); | ||
| const keys = await serverAssets.getKeys(); | ||
| const data = await serverAssets.getItem("data.json"); | ||
| const template = await serverAssets.getItem("templates/welcome.html"); | ||
| return { keys, data, template }; | ||
| }); | ||
| ``` | ||
| ### Custom asset directories | ||
| You can register additional asset directories using the `serverAssets` config option: | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| serverAssets: [ | ||
| { | ||
| baseName: "templates", | ||
| dir: "./templates", | ||
| } | ||
| ] | ||
| }) | ||
| ``` | ||
| Custom asset directories are accessible under `assets/<baseName>`: | ||
| ```ts | ||
| import { useStorage } from "nitro/storage"; | ||
| const templates = useStorage("assets/templates"); | ||
| const keys = await templates.getKeys(); | ||
| const html = await templates.getItem("email.html"); | ||
| ``` | ||
| ### Asset metadata | ||
| Server assets include metadata such as content type, ETag, and modification time: | ||
| ```ts | ||
| import { useStorage } from "nitro/storage"; | ||
| const serverAssets = useStorage("assets/server"); | ||
| const meta = await serverAssets.getMeta("image.png"); | ||
| // { type: "image/png", etag: "\"...\"", mtime: "2024-01-01T00:00:00.000Z" } | ||
| // Useful for setting response headers | ||
| const raw = await serverAssets.getItemRaw("image.png"); | ||
| ``` | ||
| <note> | ||
| In development, server assets are read directly from the filesystem. In production, they are bundled and inlined into the build output. | ||
| </note> | ||
| ## Runtime configuration | ||
| In scenarios where the mount point configuration is not known until runtime, Nitro can dynamically add mount points during startup using [plugins](/docs/plugins). | ||
| ```ts [plugins/storage.ts] | ||
| import { useStorage } from "nitro/storage"; | ||
| import { definePlugin } from "nitro"; | ||
| import redisDriver from "unstorage/drivers/redis"; | ||
| export default definePlugin(() => { | ||
| const storage = useStorage() | ||
| // Dynamically pass in credentials from runtime configuration, or other sources | ||
| const driver = redisDriver({ | ||
| base: "redis", | ||
| host: process.env.REDIS_HOST, | ||
| port: process.env.REDIS_PORT, | ||
| /* other redis connector options */ | ||
| }) | ||
| // Mount driver | ||
| storage.mount("redis", driver) | ||
| }) | ||
| ``` | ||
| <warning> | ||
| This is a temporary workaround, with a better solution coming in the future! Keep a lookout on the GitHub issue [here](https://github.com/nitrojs/nitro/issues/1161#issuecomment-1511444675). | ||
| </warning> |
| # Tasks | ||
| > Nitro tasks allow on-off operations in runtime. | ||
| ## Opt-in to the experimental feature | ||
| > [!IMPORTANT] | ||
| > Tasks support is currently experimental. | ||
| > See [nitrojs/nitro#1974](https://github.com/nitrojs/nitro/issues/1974) for the relevant discussion. | ||
| In order to use the tasks API you need to enable experimental feature flag. | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| experimental: { | ||
| tasks: true | ||
| } | ||
| }) | ||
| ``` | ||
| ## Define tasks | ||
| Tasks can be defined in `tasks/[name].ts` files. | ||
| Nested directories are supported. The task name will be joined with `:`. (Example: `tasks/db/migrate.ts` task name will be `db:migrate`) | ||
| **Example:** | ||
| ```ts [tasks/db/migrate.ts] | ||
| export default defineTask({ | ||
| meta: { | ||
| name: "db:migrate", | ||
| description: "Run database migrations", | ||
| }, | ||
| run({ payload, context }) { | ||
| console.log("Running DB migration task..."); | ||
| return { result: "Success" }; | ||
| }, | ||
| }); | ||
| ``` | ||
| ### Task interface | ||
| The `defineTask` helper accepts an object with the following properties: | ||
| - **`meta`** (optional): An object with optional `name` and `description` string fields used for display in the dev server and CLI. | ||
| - **`run`** (required): A function that receives a [`TaskEvent`](#taskevent) and returns (or resolves to) an object with an optional `result` property. | ||
| ```ts | ||
| interface Task<RT = unknown> { | ||
| meta?: { name?: string; description?: string }; | ||
| run(event: TaskEvent): { result?: RT } | Promise<{ result?: RT }>; | ||
| } | ||
| ``` | ||
| ### `TaskEvent` | ||
| The `run` function receives a `TaskEvent` object with the following properties: | ||
| - **`name`**: The name of the task being executed. | ||
| - **`payload`**: An object (`Record<string, unknown>`) containing any data passed to the task. | ||
| - **`context`**: A `TaskContext` object (may include `waitUntil` depending on the runtime). | ||
| ```ts | ||
| interface TaskEvent { | ||
| name: string; | ||
| payload: TaskPayload; | ||
| context: TaskContext; | ||
| } | ||
| ``` | ||
| ### Registering tasks via config | ||
| In addition to file-based scanning, tasks can be registered directly in the Nitro config. This is useful for tasks provided by modules or pointing to custom handler paths. | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| experimental: { | ||
| tasks: true | ||
| }, | ||
| tasks: { | ||
| "db:migrate": { | ||
| handler: "./tasks/custom-migrate.ts", | ||
| description: "Run database migrations" | ||
| } | ||
| } | ||
| }) | ||
| ``` | ||
| If a task is both scanned from the `tasks/` directory and defined in the config, the config-defined `handler` takes precedence. | ||
| ## Scheduled tasks | ||
| You can define scheduled tasks using Nitro configuration to automatically run after each period of time. | ||
| ```ts [nitro.config.ts] | ||
| import { defineNitroConfig } from "nitro/config"; | ||
| export default defineNitroConfig({ | ||
| scheduledTasks: { | ||
| // Run `cms:update` task every minute | ||
| '* * * * *': ['cms:update'], | ||
| // Run a single task (string shorthand) | ||
| '0 * * * *': 'db:cleanup' | ||
| } | ||
| }) | ||
| ``` | ||
| The `scheduledTasks` config maps cron expressions to either a single task name (string) or an array of task names. When multiple tasks are assigned to the same cron expression, they run in parallel. | ||
| > [!TIP] | ||
| > You can use [crontab.guru](https://crontab.guru/) to easily generate and understand cron tab patterns. | ||
| When a scheduled task runs, it automatically receives a `payload` with `scheduledTime` set to the current timestamp (`Date.now()`). | ||
| ### Platform support | ||
| - **`dev`**, **`node_server`**, **`node_cluster`**, **`node_middleware`**, **`bun`** and **`deno_server`** presets are supported with the [croner](https://croner.56k.guru/) engine. | ||
| - **`cloudflare_module`** and **`cloudflare_pages`** presets have native integration with [Cron Triggers](https://developers.cloudflare.com/workers/configuration/cron-triggers/). Nitro automatically generates the cron triggers in the wrangler config at build time - no manual wrangler setup required. | ||
| - **`vercel`** preset has native integration with [Vercel Cron Jobs](https://vercel.com/docs/cron-jobs). Nitro automatically generates the cron job configuration at build time - no manual `vercel.json` setup required. You can secure cron endpoints by setting the `CRON_SECRET` environment variable. | ||
| - More presets (with native primitives support) are planned to be supported! | ||
| ## `waitUntil` | ||
| When running background tasks, you might want to make sure the server or worker waits until the task is done. | ||
| An optional `context.waitUntil` function <u>might</u> be available depending on the runtime. | ||
| ```ts | ||
| export default defineTask({ | ||
| run({ context }) { | ||
| const promise = fetch(...) | ||
| context.waitUntil?.(promise); | ||
| await promise; | ||
| return { result: "Success" }; | ||
| }, | ||
| }); | ||
| ``` | ||
| ## Programmatically run tasks | ||
| To manually run tasks, you can use `runTask(name, { payload?, context? })` utility from `nitro/task`. | ||
| **Example:** | ||
| ```ts [api/migrate.ts] | ||
| import { defineHandler } from "nitro"; | ||
| export default defineHandler(async (event) => { | ||
| // IMPORTANT: Authenticate user and validate payload! | ||
| const payload = Object.fromEntries(event.url.searchParams); | ||
| const { result } = await runTask("db:migrate", { payload }); | ||
| return { result }; | ||
| }); | ||
| ``` | ||
| ### Error handling | ||
| `runTask` throws an HTTP error if: | ||
| - The task does not exist (status `404`). | ||
| - The task has no handler implementation (status `501`). | ||
| Any errors thrown inside the task's `run` function will propagate to the caller. | ||
| ## Run tasks with dev server | ||
| Nitro's built-in dev server exposes tasks to be easily executed without programmatic usage. | ||
| ### Using API routes | ||
| #### `/_nitro/tasks` | ||
| This endpoint returns a list of available task names and their meta. | ||
| ```json | ||
| // [GET] /_nitro/tasks | ||
| { | ||
| "tasks": { | ||
| "db:migrate": { | ||
| "description": "Run database migrations" | ||
| }, | ||
| "cms:update": { | ||
| "description": "Update CMS content" | ||
| } | ||
| }, | ||
| "scheduledTasks": [ | ||
| { | ||
| "cron": "* * * * *", | ||
| "tasks": [ | ||
| "cms:update" | ||
| ] | ||
| } | ||
| ] | ||
| } | ||
| ``` | ||
| #### `/_nitro/tasks/:name` | ||
| This endpoint executes a task. You can provide a payload using both query parameters and body JSON payload. The payload sent in the JSON body payload must be under the `"payload"` property. | ||
| <code-group> | ||
| ```ts [tasks/echo/payload.ts] | ||
| export default defineTask({ | ||
| meta: { | ||
| name: "echo:payload", | ||
| description: "Returns the provided payload", | ||
| }, | ||
| run({ payload, context }) { | ||
| console.log("Running echo task..."); | ||
| return { result: payload }; | ||
| }, | ||
| }); | ||
| ``` | ||
| ```json [GET] | ||
| // [GET] /_nitro/tasks/echo:payload?field=value&array=1&array=2 | ||
| { | ||
| "field": "value", | ||
| "array": ["1", "2"] | ||
| } | ||
| ``` | ||
| ```json [POST] | ||
| /** | ||
| * [POST] /_nitro/tasks/echo:payload?field=value | ||
| * body: { | ||
| * "payload": { | ||
| * "answer": 42, | ||
| * "nested": { | ||
| * "value": true | ||
| * } | ||
| * } | ||
| * } | ||
| */ | ||
| { | ||
| "field": "value", | ||
| "answer": 42, | ||
| "nested": { | ||
| "value": true | ||
| } | ||
| } | ||
| ``` | ||
| </code-group> | ||
| > [!NOTE] | ||
| > The JSON payload included in the body will overwrite the keys present in the query params. | ||
| ### Using CLI | ||
| > [!IMPORTANT] | ||
| > It is only possible to run these commands while the **dev server is running**. You should run them in a second terminal. | ||
| #### List tasks | ||
| ```sh | ||
| nitro task list | ||
| ``` | ||
| #### Run a task | ||
| ```sh | ||
| nitro task run db:migrate --payload "{}" | ||
| ``` | ||
| The `--payload` flag accepts a JSON string that will be parsed and passed to the task. If the value is not a valid JSON object, the task runs without a payload. | ||
| ## Notes | ||
| ### Concurrency | ||
| Each task can have **one running instance**. Calling a task of same name multiple times in parallel, results in calling it once and all callers will get the same return value. | ||
| > [!NOTE] | ||
| > Nitro tasks can be running multiple times and in parallel. |
| # API Routes | ||
| > File-based API routing with HTTP method support and dynamic parameters. | ||
| <code-tree> | ||
| ```html [index.html] | ||
| <!doctype html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="utf-8" /> | ||
| <meta name="viewport" content="width=device-width,initial-scale=1" /> | ||
| <title>API Routes</title> | ||
| </head> | ||
| <body> | ||
| <h2>API Routes:</h2> | ||
| <ul> | ||
| <li><a href="/api/hello">/api/hello</a></li> | ||
| <li><a href="/api/hello/world">/api/hello/world</a></li> | ||
| <li><a href="/api/test">/api/test</a></li> | ||
| </ul> | ||
| </body> | ||
| </html> | ||
| ``` | ||
| ```ts [nitro.config.ts] | ||
| import { defineConfig } from "nitro"; | ||
| export default defineConfig({ | ||
| serverDir: "./", | ||
| }); | ||
| ``` | ||
| ```json [package.json] | ||
| { | ||
| "type": "module", | ||
| "scripts": { | ||
| "dev": "nitro dev", | ||
| "build": "nitro build" | ||
| }, | ||
| "devDependencies": { | ||
| "nitro": "latest" | ||
| } | ||
| } | ||
| ``` | ||
| ```json [tsconfig.json] | ||
| { | ||
| "extends": "nitro/tsconfig" | ||
| } | ||
| ``` | ||
| ```ts [vite.config.ts] | ||
| import { defineConfig } from "vite"; | ||
| import { nitro } from "nitro/vite"; | ||
| export default defineConfig({ plugins: [nitro()] }); | ||
| ``` | ||
| ```ts [api/hello.ts] | ||
| import { defineHandler } from "nitro"; | ||
| export default defineHandler(() => "Nitro is amazing!"); | ||
| ``` | ||
| ```ts [api/test.get.ts] | ||
| import { defineHandler } from "nitro"; | ||
| export default defineHandler(() => "Test get handler"); | ||
| ``` | ||
| ```ts [api/test.post.ts] | ||
| import { defineHandler } from "nitro"; | ||
| export default defineHandler(async (event) => { | ||
| const body = await event.req.json(); | ||
| return { | ||
| message: "Test post handler", | ||
| body, | ||
| }; | ||
| }); | ||
| ``` | ||
| ```ts [api/hello/[name].ts] | ||
| import { defineHandler } from "nitro"; | ||
| export default defineHandler((event) => `Hello (param: ${event.context.params!.name})!`); | ||
| ``` | ||
| </code-tree> | ||
| Nitro supports file-based routing in the `api/` or `routes/` directory. Each file becomes an API endpoint based on its path. | ||
| ## Basic Route | ||
| Create a file in the `api/` directory to define a route. The file path becomes the URL path: | ||
| ```ts [api/hello.ts] | ||
| import { defineHandler } from "nitro"; | ||
| export default defineHandler(() => "Nitro is amazing!"); | ||
| ``` | ||
| This creates a `GET /api/hello` endpoint. | ||
| ## Dynamic Routes | ||
| Use square brackets `[param]` for dynamic URL segments. Access params via `event.context.params`: | ||
| ```ts [api/hello/[name].ts] | ||
| import { defineHandler } from "nitro"; | ||
| export default defineHandler((event) => `Hello (param: ${event.context.params!.name})!`); | ||
| ``` | ||
| This creates a `GET /api/hello/:name` endpoint (e.g., `/api/hello/world`). | ||
| ## HTTP Methods | ||
| Suffix your file with the HTTP method (`.get.ts`, `.post.ts`, `.put.ts`, `.delete.ts`, etc.): | ||
| ### GET Handler | ||
| ```ts [api/test.get.ts] | ||
| import { defineHandler } from "nitro"; | ||
| export default defineHandler(() => "Test get handler"); | ||
| ``` | ||
| ### POST Handler | ||
| ```ts [api/test.post.ts] | ||
| import { defineHandler } from "nitro"; | ||
| export default defineHandler(async (event) => { | ||
| const body = await event.req.json(); | ||
| return { | ||
| message: "Test post handler", | ||
| body, | ||
| }; | ||
| }); | ||
| ``` | ||
| ## Learn More | ||
| - [Routing](/docs/routing) |
| # Auto Imports | ||
| > Automatic imports for utilities and composables. | ||
| <code-tree> | ||
| ```ts [nitro.config.ts] | ||
| import { defineConfig } from "nitro"; | ||
| export default defineConfig({ | ||
| serverDir: true, | ||
| imports: {}, | ||
| }); | ||
| ``` | ||
| ```json [package.json] | ||
| { | ||
| "type": "module", | ||
| "scripts": { | ||
| "dev": "nitro dev", | ||
| "build": "nitro build" | ||
| }, | ||
| "devDependencies": { | ||
| "nitro": "latest" | ||
| } | ||
| } | ||
| ``` | ||
| ```ts [server.ts] | ||
| import { defineHandler } from "nitro"; | ||
| import { makeGreeting } from "./server/utils/hello.ts"; | ||
| export default defineHandler(() => `<h1>${makeGreeting("Nitro")}</h1>`); | ||
| ``` | ||
| ```json [tsconfig.json] | ||
| { | ||
| "include": [".nitro/types/nitro-imports.d.ts", "src"] | ||
| } | ||
| ``` | ||
| ```ts [vite.config.ts] | ||
| import { defineConfig } from "vite"; | ||
| import { nitro } from "nitro/vite"; | ||
| export default defineConfig({ plugins: [nitro()] }); | ||
| ``` | ||
| ```ts [server/utils/hello.ts] | ||
| export function makeGreeting(name: string) { | ||
| return `Hello, ${name}!`; | ||
| } | ||
| ``` | ||
| </code-tree> | ||
| Functions exported from `server/utils/` are automatically available without explicit imports when auto-imports are enabled. Define a utility once and use it anywhere in your server code. | ||
| ## Configuration | ||
| Enable auto-imports by setting `imports` in your config: | ||
| ```ts [nitro.config.ts] | ||
| import { defineConfig } from "nitro"; | ||
| export default defineConfig({ | ||
| serverDir: true, | ||
| imports: {}, | ||
| }); | ||
| ``` | ||
| ## Using Auto Imports | ||
| 1. Create a utility file in `server/utils/`: | ||
| ```ts [server/utils/hello.ts] | ||
| export function makeGreeting(name: string) { | ||
| return `Hello, ${name}!`; | ||
| } | ||
| ``` | ||
| 2. The function is available without importing it: | ||
| ```ts [server.ts] | ||
| import { defineHandler } from "nitro"; | ||
| import { makeGreeting } from "./server/utils/hello.ts"; | ||
| export default defineHandler(() => `<h1>${makeGreeting("Nitro")}</h1>`); | ||
| ``` | ||
| With this setup, any function exported from `server/utils/` becomes globally available. Nitro scans the directory and generates the necessary imports automatically. | ||
| ## Learn More | ||
| - [Configuration](/docs/configuration) |
| # Cached Handler | ||
| > Cache route responses with configurable bypass logic. | ||
| <code-tree> | ||
| ```ts [nitro.config.ts] | ||
| import { defineConfig } from "nitro"; | ||
| export default defineConfig({}); | ||
| ``` | ||
| ```json [package.json] | ||
| { | ||
| "type": "module", | ||
| "scripts": { | ||
| "dev": "nitro dev", | ||
| "build": "nitro build" | ||
| }, | ||
| "devDependencies": { | ||
| "nitro": "latest" | ||
| } | ||
| } | ||
| ``` | ||
| ```ts [server.ts] | ||
| import { html } from "nitro"; | ||
| import { defineCachedHandler } from "nitro/cache"; | ||
| export default defineCachedHandler( | ||
| async () => { | ||
| await new Promise((resolve) => setTimeout(resolve, 500)); | ||
| return html` | ||
| Response generated at ${new Date().toISOString()} (took 500ms) | ||
| <br />(<a href="?skipCache=true">skip cache</a>) | ||
| `; | ||
| }, | ||
| { shouldBypassCache: ({ req }) => req.url.includes("skipCache=true") } | ||
| ); | ||
| ``` | ||
| ```json [tsconfig.json] | ||
| { | ||
| "extends": "nitro/tsconfig" | ||
| } | ||
| ``` | ||
| ```ts [vite.config.ts] | ||
| import { defineConfig } from "vite"; | ||
| import { nitro } from "nitro/vite"; | ||
| export default defineConfig({ plugins: [nitro()] }); | ||
| ``` | ||
| </code-tree> | ||
| This example shows how to cache an expensive operation (a 500 ms delay) and conditionally bypass the cache using a query parameter. On first request, the handler executes and caches the result. Subsequent requests return the cached response instantly until the cache expires or is bypassed. | ||
| ## How It Works | ||
| ```ts [server.ts] | ||
| import { html } from "nitro"; | ||
| import { defineCachedHandler } from "nitro/cache"; | ||
| export default defineCachedHandler( | ||
| async () => { | ||
| await new Promise((resolve) => setTimeout(resolve, 500)); | ||
| return html` | ||
| Response generated at ${new Date().toISOString()} (took 500ms) | ||
| <br />(<a href="?skipCache=true">skip cache</a>) | ||
| `; | ||
| }, | ||
| { shouldBypassCache: ({ req }) => req.url.includes("skipCache=true") } | ||
| ); | ||
| ``` | ||
| The handler simulates a slow operation with a 500ms delay. As `defineCachedHandler` wraps it, the response is cached after the first execution. The `shouldBypassCache` option checks for `?skipCache=true` in the URL and when present the cache is skipped and the handler runs fresh. | ||
| ## Learn More | ||
| - [Cache](/docs/cache) | ||
| - [Storage](/docs/storage) |
| # Custom Error Handler | ||
| > Customize error responses with a global error handler. | ||
| <code-tree> | ||
| ```ts [error.ts] | ||
| import { defineErrorHandler } from "nitro"; | ||
| export default defineErrorHandler((error, _event) => { | ||
| return new Response(`Custom Error Handler: ${error.message}`, { | ||
| status: 500, | ||
| headers: { "Content-Type": "text/plain" }, | ||
| }); | ||
| }); | ||
| ``` | ||
| ```ts [nitro.config.ts] | ||
| import { defineConfig } from "nitro"; | ||
| // import errorHandler from "./error"; | ||
| export default defineConfig({ | ||
| errorHandler: "./error.ts", | ||
| // devErrorHandler: errorHandler, | ||
| }); | ||
| ``` | ||
| ```json [package.json] | ||
| { | ||
| "type": "module", | ||
| "scripts": { | ||
| "dev": "nitro dev", | ||
| "build": "nitro build" | ||
| }, | ||
| "devDependencies": { | ||
| "nitro": "latest" | ||
| } | ||
| } | ||
| ``` | ||
| ```ts [server.ts] | ||
| import { defineHandler, HTTPError } from "nitro"; | ||
| export default defineHandler(() => { | ||
| throw new HTTPError("Example Error!", { status: 500 }); | ||
| }); | ||
| ``` | ||
| ```json [tsconfig.json] | ||
| { | ||
| "extends": "nitro/tsconfig" | ||
| } | ||
| ``` | ||
| ```ts [vite.config.ts] | ||
| import { defineConfig } from "vite"; | ||
| import { nitro } from "nitro/vite"; | ||
| export default defineConfig({ plugins: [nitro()] }); | ||
| ``` | ||
| </code-tree> | ||
| This example shows how to intercept all errors and return a custom response format. When any route throws an error, Nitro calls your error handler instead of returning the default error page. | ||
| ## Error Handler | ||
| Create an `error.ts` file in your project root to define the global error handler: | ||
| ```ts [error.ts] | ||
| import { defineErrorHandler } from "nitro"; | ||
| export default defineErrorHandler((error, _event) => { | ||
| return new Response(`Custom Error Handler: ${error.message}`, { | ||
| status: 500, | ||
| headers: { "Content-Type": "text/plain" }, | ||
| }); | ||
| }); | ||
| ``` | ||
| The handler receives the thrown error and the H3 event object. You can use the event to access request details like headers, cookies, or the URL path to customize responses per route. | ||
| ## Triggering an Error | ||
| The main handler throws an error to demonstrate the custom error handler: | ||
| ```ts [server.ts] | ||
| import { defineHandler, HTTPError } from "nitro"; | ||
| export default defineHandler(() => { | ||
| throw new HTTPError("Example Error!", { status: 500 }); | ||
| }); | ||
| ``` | ||
| When you visit the page, instead of seeing a generic error page, you'll see "Custom Error Handler: Example Error!" because the error handler intercepts the thrown error. | ||
| ## Learn More | ||
| - [Server Entry](/docs/server-entry) |
| # Database | ||
| > Built-in database support with SQL template literals. | ||
| <code-tree> | ||
| ```ts [nitro.config.ts] | ||
| import { defineConfig } from "nitro"; | ||
| export default defineConfig({ | ||
| experimental: { | ||
| database: true, | ||
| tasks: true, | ||
| }, | ||
| database: { | ||
| default: { connector: "sqlite" }, | ||
| }, | ||
| }); | ||
| ``` | ||
| ```json [package.json] | ||
| { | ||
| "type": "module", | ||
| "scripts": { | ||
| "dev": "nitro dev", | ||
| "build": "nitro build" | ||
| }, | ||
| "devDependencies": { | ||
| "nitro": "latest" | ||
| } | ||
| } | ||
| ``` | ||
| ```ts [server.ts] | ||
| import { defineHandler } from "nitro"; | ||
| import { useDatabase } from "nitro/database"; | ||
| export default defineHandler(async () => { | ||
| const db = useDatabase(); | ||
| // Create users table | ||
| await db.sql`DROP TABLE IF EXISTS users`; | ||
| await db.sql`CREATE TABLE IF NOT EXISTS users ("id" TEXT PRIMARY KEY, "firstName" TEXT, "lastName" TEXT, "email" TEXT)`; | ||
| // Add a new user | ||
| const userId = String(Math.round(Math.random() * 10_000)); | ||
| await db.sql`INSERT INTO users VALUES (${userId}, 'John', 'Doe', '')`; | ||
| // Query for users | ||
| const { rows } = await db.sql`SELECT * FROM users WHERE id = ${userId}`; | ||
| return { | ||
| rows, | ||
| }; | ||
| }); | ||
| ``` | ||
| ```json [tsconfig.json] | ||
| { | ||
| "extends": "nitro/tsconfig" | ||
| } | ||
| ``` | ||
| ```ts [vite.config.ts] | ||
| import { defineConfig } from "vite"; | ||
| import { nitro } from "nitro/vite"; | ||
| export default defineConfig({ plugins: [nitro()] }); | ||
| ``` | ||
| ```ts [tasks/db/migrate.ts] | ||
| import { defineTask } from "nitro/task"; | ||
| import { useDatabase } from "nitro/database"; | ||
| export default defineTask({ | ||
| meta: { | ||
| description: "Run database migrations", | ||
| }, | ||
| async run() { | ||
| const db = useDatabase(); | ||
| console.log("Running database migrations..."); | ||
| // Create users table | ||
| await db.sql`DROP TABLE IF EXISTS users`; | ||
| await db.sql`CREATE TABLE IF NOT EXISTS users ("id" TEXT PRIMARY KEY, "firstName" TEXT, "lastName" TEXT, "email" TEXT)`; | ||
| return { | ||
| result: "Database migrations complete!", | ||
| }; | ||
| }, | ||
| }); | ||
| ``` | ||
| </code-tree> | ||
| Nitro provides a built-in database layer that uses SQL template literals for safe, parameterized queries. This example creates a users table, inserts a record, and queries it back. | ||
| ## Querying the Database | ||
| ```ts [server.ts] | ||
| import { defineHandler } from "nitro"; | ||
| import { useDatabase } from "nitro/database"; | ||
| export default defineHandler(async () => { | ||
| const db = useDatabase(); | ||
| // Create users table | ||
| await db.sql`DROP TABLE IF EXISTS users`; | ||
| await db.sql`CREATE TABLE IF NOT EXISTS users ("id" TEXT PRIMARY KEY, "firstName" TEXT, "lastName" TEXT, "email" TEXT)`; | ||
| // Add a new user | ||
| const userId = String(Math.round(Math.random() * 10_000)); | ||
| await db.sql`INSERT INTO users VALUES (${userId}, 'John', 'Doe', '')`; | ||
| // Query for users | ||
| const { rows } = await db.sql`SELECT * FROM users WHERE id = ${userId}`; | ||
| return { | ||
| rows, | ||
| }; | ||
| }); | ||
| ``` | ||
| Retrieve the database instance using `useDatabase()`. The database can be queried using `db.sql`, and variables like `${userId}` are automatically escaped to prevent SQL injection. | ||
| ## Running Migrations with Tasks | ||
| Nitro tasks let you run operations outside of request handlers. For database migrations, create a task file in `tasks/` and run it via the CLI. This keeps schema changes separate from your application code. | ||
| ```ts [tasks/db/migrate.ts] | ||
| import { defineTask } from "nitro/task"; | ||
| import { useDatabase } from "nitro/database"; | ||
| export default defineTask({ | ||
| meta: { | ||
| description: "Run database migrations", | ||
| }, | ||
| async run() { | ||
| const db = useDatabase(); | ||
| console.log("Running database migrations..."); | ||
| // Create users table | ||
| await db.sql`DROP TABLE IF EXISTS users`; | ||
| await db.sql`CREATE TABLE IF NOT EXISTS users ("id" TEXT PRIMARY KEY, "firstName" TEXT, "lastName" TEXT, "email" TEXT)`; | ||
| return { | ||
| result: "Database migrations complete!", | ||
| }; | ||
| }, | ||
| }); | ||
| ``` | ||
| ## Learn More | ||
| - [Database](/docs/database) | ||
| - [Tasks](/docs/tasks) |
| # Elysia | ||
| > Integrate Elysia with Nitro using the server entry. | ||
| <code-tree> | ||
| ```ts [nitro.config.ts] | ||
| import { defineConfig } from "nitro"; | ||
| export default defineConfig({}); | ||
| ``` | ||
| ```json [package.json] | ||
| { | ||
| "type": "module", | ||
| "scripts": { | ||
| "build": "nitro build", | ||
| "dev": "nitro dev" | ||
| }, | ||
| "devDependencies": { | ||
| "elysia": "^1.4.22", | ||
| "nitro": "latest" | ||
| } | ||
| } | ||
| ``` | ||
| ```ts [server.ts] | ||
| import { Elysia } from "elysia"; | ||
| const app = new Elysia(); | ||
| app.get("/", () => "Hello, Elysia with Nitro!"); | ||
| export default app.compile(); | ||
| ``` | ||
| ```json [tsconfig.json] | ||
| { | ||
| "extends": "nitro/tsconfig" | ||
| } | ||
| ``` | ||
| ```ts [vite.config.ts] | ||
| import { defineConfig } from "vite"; | ||
| import { nitro } from "nitro/vite"; | ||
| export default defineConfig({ plugins: [nitro()] }); | ||
| ``` | ||
| </code-tree> | ||
| ## Server Entry | ||
| ```ts [server.ts] | ||
| import { Elysia } from "elysia"; | ||
| const app = new Elysia(); | ||
| app.get("/", () => "Hello, Elysia with Nitro!"); | ||
| export default app.compile(); | ||
| ``` | ||
| Nitro auto-detects `server.ts` in your project root and uses it as the server entry. The Elysia app handles all incoming requests, giving you full control over routing and middleware. | ||
| Call `app.compile()` before exporting to optimize the router for production. | ||
| ## Learn More | ||
| - [Server Entry](/docs/server-entry) | ||
| - [Elysia Documentation](https://elysiajs.com/) |
| # Express | ||
| > Integrate Express with Nitro using the server entry. | ||
| <code-tree> | ||
| ```ts [nitro.config.ts] | ||
| import { defineConfig } from "nitro"; | ||
| export default defineConfig({}); | ||
| ``` | ||
| ```json [package.json] | ||
| { | ||
| "type": "module", | ||
| "scripts": { | ||
| "build": "nitro build", | ||
| "dev": "nitro dev" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/express": "^5.0.6", | ||
| "express": "^5.2.1", | ||
| "nitro": "latest" | ||
| } | ||
| } | ||
| ``` | ||
| ```ts [server.node.ts] | ||
| import Express from "express"; | ||
| const app = Express(); | ||
| app.use("/", (_req, res) => { | ||
| res.send("Hello from Express with Nitro!"); | ||
| }); | ||
| export default app; | ||
| ``` | ||
| ```json [tsconfig.json] | ||
| { | ||
| "extends": "nitro/tsconfig" | ||
| } | ||
| ``` | ||
| ```ts [vite.config.ts] | ||
| import { defineConfig } from "vite"; | ||
| import { nitro } from "nitro/vite"; | ||
| export default defineConfig({ plugins: [nitro()] }); | ||
| ``` | ||
| </code-tree> | ||
| ## Server Entry | ||
| ```ts [server.node.ts] | ||
| import Express from "express"; | ||
| const app = Express(); | ||
| app.use("/", (_req, res) => { | ||
| res.send("Hello from Express with Nitro!"); | ||
| }); | ||
| export default app; | ||
| ``` | ||
| Nitro auto-detects `server.node.ts` in your project root and uses it as the server entry. The Express app handles all incoming requests, giving you full control over routing and middleware. | ||
| <note> | ||
| The `.node.ts` suffix indicates this entry is Node.js specific and won't work in other runtimes like Cloudflare Workers or Deno. | ||
| </note> | ||
| ## Learn More | ||
| - [Server Entry](/docs/server-entry) | ||
| - [Express Documentation](https://expressjs.com/) |
| # Fastify | ||
| > Integrate Fastify with Nitro using the server entry. | ||
| <code-tree> | ||
| ```ts [nitro.config.ts] | ||
| import { defineConfig } from "nitro"; | ||
| export default defineConfig({}); | ||
| ``` | ||
| ```json [package.json] | ||
| { | ||
| "type": "module", | ||
| "scripts": { | ||
| "build": "nitro build", | ||
| "dev": "nitro dev" | ||
| }, | ||
| "devDependencies": { | ||
| "fastify": "^5.7.4", | ||
| "nitro": "latest" | ||
| } | ||
| } | ||
| ``` | ||
| ```ts [server.node.ts] | ||
| import Fastify from "fastify"; | ||
| const app = Fastify(); | ||
| app.get("/", () => "Hello, Fastify with Nitro!"); | ||
| await app.ready(); | ||
| export default app.routing; | ||
| ``` | ||
| ```json [tsconfig.json] | ||
| { | ||
| "extends": "nitro/tsconfig" | ||
| } | ||
| ``` | ||
| ```ts [vite.config.ts] | ||
| import { defineConfig } from "vite"; | ||
| import { nitro } from "nitro/vite"; | ||
| export default defineConfig({ plugins: [nitro()] }); | ||
| ``` | ||
| </code-tree> | ||
| ## Server Entry | ||
| ```ts [server.node.ts] | ||
| import Fastify from "fastify"; | ||
| const app = Fastify(); | ||
| app.get("/", () => "Hello, Fastify with Nitro!"); | ||
| await app.ready(); | ||
| export default app.routing; | ||
| ``` | ||
| Nitro auto-detects `server.node.ts` in your project root and uses it as the server entry. | ||
| Call `await app.ready()` to initialize all registered plugins before exporting. Export `app.routing` (not `app`) to provide Nitro with the request handler function. | ||
| <note> | ||
| The `.node.ts` suffix indicates this entry is Node.js specific and won't work in other runtimes like Cloudflare Workers or Deno. | ||
| </note> | ||
| ## Learn More | ||
| - [Server Entry](/docs/server-entry) | ||
| - [Fastify Documentation](https://fastify.dev/) |
| # Hello World | ||
| > Minimal Nitro server using the web standard fetch handler. | ||
| <code-tree> | ||
| ```ts [nitro.config.ts] | ||
| import { defineConfig } from "nitro"; | ||
| export default defineConfig({}); | ||
| ``` | ||
| ```json [package.json] | ||
| { | ||
| "type": "module", | ||
| "scripts": { | ||
| "build": "nitro build", | ||
| "dev": "nitro dev", | ||
| "preview": "node .output/server/index.mjs" | ||
| }, | ||
| "devDependencies": { | ||
| "nitro": "latest" | ||
| } | ||
| } | ||
| ``` | ||
| ```ts [server.ts] | ||
| export default { | ||
| fetch(req: Request) { | ||
| return new Response("Nitro Works!"); | ||
| }, | ||
| }; | ||
| ``` | ||
| ```json [tsconfig.json] | ||
| { | ||
| "extends": "nitro/tsconfig" | ||
| } | ||
| ``` | ||
| ```ts [vite.config.ts] | ||
| import { defineConfig } from "vite"; | ||
| import { nitro } from "nitro/vite"; | ||
| export default defineConfig({ plugins: [nitro()] }); | ||
| ``` | ||
| </code-tree> | ||
| The simplest Nitro server. Export an object with a `fetch` method that receives a standard `Request` and returns a `Response`. No frameworks, no abstractions, just the web platform. | ||
| ## Server Entry | ||
| ```ts [server.ts] | ||
| export default { | ||
| fetch(req: Request) { | ||
| return new Response("Nitro Works!"); | ||
| }, | ||
| }; | ||
| ``` | ||
| The `fetch` method follows the same signature as Service Workers and Cloudflare Workers. This pattern works across all deployment targets because it uses web standards. | ||
| Add the Nitro plugin to Vite and it handles the rest: dev server, hot reloading, and production builds. | ||
| ## Learn More | ||
| - [Server Entry](/docs/server-entry) | ||
| - [Configuration](/docs/configuration) |
| # Hono | ||
| > Integrate Hono with Nitro using the server entry. | ||
| <code-tree> | ||
| ```ts [nitro.config.ts] | ||
| import { defineConfig } from "nitro"; | ||
| export default defineConfig({}); | ||
| ``` | ||
| ```json [package.json] | ||
| { | ||
| "type": "module", | ||
| "scripts": { | ||
| "build": "nitro build", | ||
| "dev": "nitro dev" | ||
| }, | ||
| "devDependencies": { | ||
| "hono": "^4.11.8", | ||
| "nitro": "latest" | ||
| } | ||
| } | ||
| ``` | ||
| ```ts [server.ts] | ||
| import { Hono } from "hono"; | ||
| const app = new Hono(); | ||
| app.get("/", (c) => { | ||
| return c.text("Hello, Hono with Nitro!"); | ||
| }); | ||
| export default app; | ||
| ``` | ||
| ```json [tsconfig.json] | ||
| { | ||
| "extends": "nitro/tsconfig" | ||
| } | ||
| ``` | ||
| ```ts [vite.config.ts] | ||
| import { defineConfig } from "vite"; | ||
| import { nitro } from "nitro/vite"; | ||
| export default defineConfig({ plugins: [nitro()] }); | ||
| ``` | ||
| </code-tree> | ||
| ## Server Entry | ||
| ```ts [server.ts] | ||
| import { Hono } from "hono"; | ||
| const app = new Hono(); | ||
| app.get("/", (c) => { | ||
| return c.text("Hello, Hono with Nitro!"); | ||
| }); | ||
| export default app; | ||
| ``` | ||
| Nitro auto-detects `server.ts` in your project root and uses it as the server entry. The Hono app handles all incoming requests, giving you full control over routing and middleware. | ||
| Hono is cross-runtime compatible, so this server entry works across all Nitro deployment targets including Node.js, Deno, Bun, and Cloudflare Workers. | ||
| ## Learn More | ||
| - [Server Entry](/docs/server-entry) | ||
| - [Hono Documentation](https://hono.dev/) |
| # Import Alias | ||
| > Custom import aliases for cleaner module paths. | ||
| <code-tree> | ||
| ```ts [nitro.config.ts] | ||
| import { defineConfig } from "nitro"; | ||
| export default defineConfig({ | ||
| serverDir: true, | ||
| experimental: { | ||
| tsconfigPaths: true, | ||
| }, | ||
| }); | ||
| ``` | ||
| ```json [package.json] | ||
| { | ||
| "type": "module", | ||
| "imports": { | ||
| "#server/*": "./server/*" | ||
| }, | ||
| "scripts": { | ||
| "build": "nitro build", | ||
| "dev": "nitro dev", | ||
| "preview": "node .output/server/index.mjs" | ||
| }, | ||
| "devDependencies": { | ||
| "nitro": "latest" | ||
| } | ||
| } | ||
| ``` | ||
| ```json [tsconfig.json] | ||
| { | ||
| "extends": "nitro/tsconfig", | ||
| "compilerOptions": { | ||
| "paths": { | ||
| "~server/*": ["./server/*"] | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
| ```ts [vite.config.ts] | ||
| import { defineConfig } from "vite"; | ||
| import { nitro } from "nitro/vite"; | ||
| export default defineConfig({ plugins: [nitro()] }); | ||
| ``` | ||
| ```ts [server/routes/index.ts] | ||
| import { sum } from "~server/utils/math.ts"; | ||
| import { rand } from "#server/utils/math.ts"; | ||
| export default () => { | ||
| const [a, b] = [rand(1, 10), rand(1, 10)]; | ||
| const result = sum(a, b); | ||
| return `The sum of ${a} + ${b} = ${result}`; | ||
| }; | ||
| ``` | ||
| ```ts [server/utils/math.ts] | ||
| export function rand(min: number, max: number): number { | ||
| return Math.floor(Math.random() * (max - min + 1)) + min; | ||
| } | ||
| export function sum(a: number, b: number): number { | ||
| return a + b; | ||
| } | ||
| ``` | ||
| </code-tree> | ||
| Import aliases like `~` and `#` let you reference modules with shorter paths instead of relative imports. | ||
| ## Importing Using Aliases | ||
| ```ts [server/routes/index.ts] | ||
| import { sum } from "~server/utils/math.ts"; | ||
| import { rand } from "#server/utils/math.ts"; | ||
| export default () => { | ||
| const [a, b] = [rand(1, 10), rand(1, 10)]; | ||
| const result = sum(a, b); | ||
| return `The sum of ${a} + ${b} = ${result}`; | ||
| }; | ||
| ``` | ||
| The route imports the `sum` function using `~server/` and `rand` using `#server/`. Both resolve to the same `server/utils/math.ts` file. The handler generates two random numbers and returns their sum. | ||
| ## Configuration | ||
| Aliases can be configured in `package.json` imports field or `nitro.config.ts`. | ||
| ## Learn More | ||
| - [Configuration](/docs/configuration) |
| # Examples | ||
| > Explore Nitro examples to learn how to build full-stack applications |
| # Middleware | ||
| > Request middleware for authentication, logging, and request modification. | ||
| <code-tree> | ||
| ```ts [nitro.config.ts] | ||
| import { defineConfig } from "nitro"; | ||
| export default defineConfig({ | ||
| serverDir: true, | ||
| }); | ||
| ``` | ||
| ```json [package.json] | ||
| { | ||
| "type": "module", | ||
| "scripts": { | ||
| "dev": "nitro dev", | ||
| "build": "nitro build" | ||
| }, | ||
| "devDependencies": { | ||
| "nitro": "latest" | ||
| } | ||
| } | ||
| ``` | ||
| ```ts [server.ts] | ||
| import { defineHandler } from "nitro"; | ||
| export default defineHandler((event) => ({ | ||
| auth: event.context.auth, | ||
| })); | ||
| ``` | ||
| ```json [tsconfig.json] | ||
| { | ||
| "extends": "nitro/tsconfig" | ||
| } | ||
| ``` | ||
| ```ts [vite.config.ts] | ||
| import { defineConfig } from "vite"; | ||
| import { nitro } from "nitro/vite"; | ||
| export default defineConfig({ plugins: [nitro()] }); | ||
| ``` | ||
| ```ts [server/middleware/auth.ts] | ||
| import { defineMiddleware } from "nitro"; | ||
| export default defineMiddleware((event) => { | ||
| event.context.auth = { name: "User " + Math.round(Math.random() * 100) }; | ||
| }); | ||
| ``` | ||
| </code-tree> | ||
| Middleware functions run before route handlers on every request. They can modify the request, add context, or return early responses. | ||
| ## Defining Middleware | ||
| Create files in `server/middleware/`. They run in alphabetical order: | ||
| ```ts [server/middleware/auth.ts] | ||
| import { defineMiddleware } from "nitro"; | ||
| export default defineMiddleware((event) => { | ||
| event.context.auth = { name: "User " + Math.round(Math.random() * 100) }; | ||
| }); | ||
| ``` | ||
| Middleware can: | ||
| - Add data to `event.context` for use in handlers | ||
| - Return a response early to short-circuit the request | ||
| - Modify request headers or other properties | ||
| ## Accessing Context in Handlers | ||
| Data added to `event.context` in middleware is available in all subsequent handlers: | ||
| ```ts [server.ts] | ||
| import { defineHandler } from "nitro"; | ||
| export default defineHandler((event) => ({ | ||
| auth: event.context.auth, | ||
| })); | ||
| ``` | ||
| ## Learn More | ||
| - [Routing](/docs/routing) |
| # Mono JSX | ||
| > Server-side JSX rendering in Nitro with mono-jsx. | ||
| <code-tree> | ||
| ```ts [nitro.config.ts] | ||
| import { defineConfig } from "nitro"; | ||
| export default defineConfig({}); | ||
| ``` | ||
| ```json [package.json] | ||
| { | ||
| "type": "module", | ||
| "scripts": { | ||
| "dev": "nitro dev", | ||
| "build": "nitro build" | ||
| }, | ||
| "devDependencies": { | ||
| "mono-jsx": "latest", | ||
| "nitro": "latest" | ||
| } | ||
| } | ||
| ``` | ||
| ```tsx [server.tsx] | ||
| export default () => ( | ||
| <html> | ||
| <h1>Nitro + mongo-jsx works!</h1> | ||
| </html> | ||
| ); | ||
| ``` | ||
| ```json [tsconfig.json] | ||
| { | ||
| "extends": "nitro/tsconfig", | ||
| "compilerOptions": { | ||
| "jsx": "react-jsx", | ||
| "jsxImportSource": "mono-jsx" | ||
| } | ||
| } | ||
| ``` | ||
| ```ts [vite.config.ts] | ||
| import { defineConfig } from "vite"; | ||
| import { nitro } from "nitro/vite"; | ||
| export default defineConfig({ plugins: [nitro()] }); | ||
| ``` | ||
| </code-tree> | ||
| ## Server Entry | ||
| ```tsx [server.tsx] | ||
| export default () => ( | ||
| <html> | ||
| <h1>Nitro + mongo-jsx works!</h1> | ||
| </html> | ||
| ); | ||
| ``` | ||
| Nitro auto-detects `server.tsx` and uses mono-jsx to transform JSX into HTML. Export a function that returns JSX, and Nitro sends the rendered HTML as the response. | ||
| ## Learn More | ||
| - [Renderer](/docs/renderer) | ||
| - [mono-jsx](https://github.com/aspect-dev/mono-jsx) |
| # Nano JSX | ||
| > Server-side JSX rendering in Nitro with nano-jsx. | ||
| <code-tree> | ||
| ```ts [nitro.config.ts] | ||
| import { defineConfig } from "nitro"; | ||
| export default defineConfig({}); | ||
| ``` | ||
| ```json [package.json] | ||
| { | ||
| "type": "module", | ||
| "scripts": { | ||
| "dev": "nitro dev", | ||
| "build": "nitro build" | ||
| }, | ||
| "devDependencies": { | ||
| "nano-jsx": "^0.2.1", | ||
| "nitro": "latest" | ||
| } | ||
| } | ||
| ``` | ||
| ```tsx [server.tsx] | ||
| import { defineHandler, html } from "nitro"; | ||
| import { renderSSR } from "nano-jsx"; | ||
| export default defineHandler(() => { | ||
| return html(renderSSR(() => <h1>Nitro + nano-jsx works!</h1>)); | ||
| }); | ||
| ``` | ||
| ```json [tsconfig.json] | ||
| { | ||
| "extends": "nitro/tsconfig", | ||
| "compilerOptions": { | ||
| "jsx": "react-jsx", | ||
| "jsxImportSource": "nano-jsx/esm" | ||
| } | ||
| } | ||
| ``` | ||
| ```ts [vite.config.ts] | ||
| import { defineConfig } from "vite"; | ||
| import { nitro } from "nitro/vite"; | ||
| export default defineConfig({ plugins: [nitro()] }); | ||
| ``` | ||
| </code-tree> | ||
| ## Server Entry | ||
| ```tsx [server.tsx] | ||
| import { defineHandler, html } from "nitro"; | ||
| import { renderSSR } from "nano-jsx"; | ||
| export default defineHandler(() => { | ||
| return html(renderSSR(() => <h1>Nitro + nano-jsx works!</h1>)); | ||
| }); | ||
| ``` | ||
| Nitro auto-detects `server.tsx` and uses it as the server entry. Use `renderSSR` from nano-jsx to convert JSX into an HTML string. The `html` helper from H3 sets the correct content type header. | ||
| ## Learn More | ||
| - [Renderer](/docs/renderer) | ||
| - [nano-jsx](https://nanojsx.io/) |
| # Plugins | ||
| > Extend Nitro with custom plugins for hooks and lifecycle events. | ||
| <code-tree> | ||
| ```ts [nitro.config.ts] | ||
| import { defineConfig } from "nitro"; | ||
| export default defineConfig({ | ||
| serverDir: true, | ||
| }); | ||
| ``` | ||
| ```json [package.json] | ||
| { | ||
| "type": "module", | ||
| "scripts": { | ||
| "dev": "nitro dev", | ||
| "build": "nitro build" | ||
| }, | ||
| "devDependencies": { | ||
| "nitro": "latest" | ||
| } | ||
| } | ||
| ``` | ||
| ```ts [server.ts] | ||
| import { eventHandler } from "h3"; | ||
| export default eventHandler(() => "<h1>Hello Nitro!</h1>"); | ||
| ``` | ||
| ```json [tsconfig.json] | ||
| { | ||
| "extends": "nitro/tsconfig" | ||
| } | ||
| ``` | ||
| ```ts [vite.config.ts] | ||
| import { defineConfig } from "vite"; | ||
| import { nitro } from "nitro/vite"; | ||
| export default defineConfig({ plugins: [nitro()] }); | ||
| ``` | ||
| ```ts [server/plugins/test.ts] | ||
| import { definePlugin } from "nitro"; | ||
| import { useNitroHooks } from "nitro/app"; | ||
| export default definePlugin((nitroApp) => { | ||
| const hooks = useNitroHooks(); | ||
| hooks.hook("response", (event) => { | ||
| event.headers.set("content-type", "html; charset=utf-8"); | ||
| }); | ||
| }); | ||
| ``` | ||
| </code-tree> | ||
| Plugins let you hook into Nitro's runtime lifecycle. This example shows a plugin that modifies the `Content-Type` header on every response. Create files in `server/plugins/` and they're automatically loaded at startup. | ||
| ## Defining a Plugin | ||
| ```ts [server/plugins/test.ts] | ||
| import { definePlugin } from "nitro"; | ||
| import { useNitroHooks } from "nitro/app"; | ||
| export default definePlugin((nitroApp) => { | ||
| const hooks = useNitroHooks(); | ||
| hooks.hook("response", (event) => { | ||
| event.headers.set("content-type", "html; charset=utf-8"); | ||
| }); | ||
| }); | ||
| ``` | ||
| The plugin uses `useNitroHooks()` to access the hooks system, then registers a `response` hook that runs after every request. Here it sets the content type to HTML, but you could log requests, add security headers, or modify responses in any way. | ||
| ## Main Handler | ||
| ```ts [server.ts] | ||
| import { eventHandler } from "h3"; | ||
| export default eventHandler(() => "<h1>Hello Nitro!</h1>"); | ||
| ``` | ||
| The handler returns HTML without setting a content type. The plugin automatically adds the correct `Content-Type: html; charset=utf-8` header to the response. | ||
| ## Learn More | ||
| - [Plugins](/docs/plugins) | ||
| - [Lifecycle](/docs/lifecycle) |
| # Custom Renderer | ||
| > Build a custom HTML renderer in Nitro with server-side data fetching. | ||
| <code-tree> | ||
| ```ts [nitro.config.ts] | ||
| import { defineConfig } from "nitro"; | ||
| export default defineConfig({ | ||
| serverDir: "./", | ||
| renderer: { handler: "./renderer" }, | ||
| }); | ||
| ``` | ||
| ```json [package.json] | ||
| { | ||
| "type": "module", | ||
| "scripts": { | ||
| "dev": "nitro dev", | ||
| "build": "nitro build" | ||
| }, | ||
| "devDependencies": { | ||
| "nitro": "latest" | ||
| } | ||
| } | ||
| ``` | ||
| ```ts [renderer.ts] | ||
| import { fetch } from "nitro"; | ||
| export default async function renderer({ url }: { req: Request; url: URL }) { | ||
| const apiRes = await fetch("/api/hello").then((res) => res.text()); | ||
| return new Response( | ||
| /* html */ `<!DOCTYPE html> | ||
| <html> | ||
| <head> | ||
| <title>Custom Renderer</title> | ||
| </head> | ||
| <body> | ||
| <h1>Hello from custom renderer!</h1> | ||
| <p>Current path: ${url.pathname}</p> | ||
| <p>API says: ${apiRes}</p> | ||
| </body> | ||
| </html>`, | ||
| { headers: { "content-type": "text/html; charset=utf-8" } } | ||
| ); | ||
| } | ||
| ``` | ||
| ```json [tsconfig.json] | ||
| { | ||
| "extends": "nitro/tsconfig" | ||
| } | ||
| ``` | ||
| ```ts [vite.config.ts] | ||
| import { defineConfig } from "vite"; | ||
| import { nitro } from "nitro/vite"; | ||
| export default defineConfig({ plugins: [nitro()] }); | ||
| ``` | ||
| ```ts [api/hello.ts] | ||
| import { defineHandler } from "nitro"; | ||
| export default defineHandler(() => "Nitro is amazing!"); | ||
| ``` | ||
| </code-tree> | ||
| Create a custom renderer that generates HTML responses with data from API routes. Use Nitro's internal `fetch` to call routes without network overhead. | ||
| ## Renderer | ||
| ```ts [renderer.ts] | ||
| import { fetch } from "nitro"; | ||
| export default async function renderer({ url }: { req: Request; url: URL }) { | ||
| const apiRes = await fetch("/api/hello").then((res) => res.text()); | ||
| return new Response( | ||
| /* html */ `<!DOCTYPE html> | ||
| <html> | ||
| <head> | ||
| <title>Custom Renderer</title> | ||
| </head> | ||
| <body> | ||
| <h1>Hello from custom renderer!</h1> | ||
| <p>Current path: ${url.pathname}</p> | ||
| <p>API says: ${apiRes}</p> | ||
| </body> | ||
| </html>`, | ||
| { headers: { "content-type": "text/html; charset=utf-8" } } | ||
| ); | ||
| } | ||
| ``` | ||
| Nitro auto-detects `renderer.ts` in your project root and uses it for all non-API routes. The renderer function receives the request URL and returns a `Response`. | ||
| Use `fetch` from `nitro` to call API routes without network overhead—these requests stay in-process. | ||
| ## API Route | ||
| ```ts [api/hello.ts] | ||
| import { defineHandler } from "nitro"; | ||
| export default defineHandler(() => "Nitro is amazing!"); | ||
| ``` | ||
| Define API routes in the `api/` directory. When the renderer calls `fetch("/api/hello")`, this handler runs and returns its response. | ||
| ## Learn More | ||
| - [Renderer](/docs/renderer) |
| # Runtime Config | ||
| > Environment-aware configuration with runtime access. | ||
| <code-tree> | ||
| ```ts [nitro.config.ts] | ||
| import { defineConfig } from "nitro"; | ||
| export default defineConfig({ | ||
| serverDir: "./", | ||
| runtimeConfig: { | ||
| apiKey: "", | ||
| }, | ||
| }); | ||
| ``` | ||
| ```json [package.json] | ||
| { | ||
| "type": "module", | ||
| "scripts": { | ||
| "dev": "nitro dev", | ||
| "build": "nitro build" | ||
| }, | ||
| "devDependencies": { | ||
| "nitro": "latest" | ||
| } | ||
| } | ||
| ``` | ||
| ```ts [server.ts] | ||
| import { defineHandler } from "nitro"; | ||
| import { useRuntimeConfig } from "nitro/runtime-config"; | ||
| export default defineHandler((event) => { | ||
| const runtimeConfig = useRuntimeConfig(); | ||
| return { runtimeConfig }; | ||
| }); | ||
| ``` | ||
| ```json [tsconfig.json] | ||
| { | ||
| "extends": "nitro/tsconfig" | ||
| } | ||
| ``` | ||
| ```ts [vite.config.ts] | ||
| import { defineConfig } from "vite"; | ||
| import { nitro } from "nitro/vite"; | ||
| export default defineConfig({ plugins: [nitro()] }); | ||
| ``` | ||
| </code-tree> | ||
| Runtime config lets you define configuration values that can be overridden by environment variables at runtime. | ||
| ## Define Config Schema | ||
| Declare your runtime config with default values in `nitro.config.ts`: | ||
| ```ts [nitro.config.ts] | ||
| import { defineConfig } from "nitro"; | ||
| export default defineConfig({ | ||
| serverDir: "./", | ||
| runtimeConfig: { | ||
| apiKey: "", | ||
| }, | ||
| }); | ||
| ``` | ||
| ## Access at Runtime | ||
| Use `useRuntimeConfig` to access configuration values in your handlers: | ||
| ```ts [server.ts] | ||
| import { defineHandler } from "nitro"; | ||
| import { useRuntimeConfig } from "nitro/runtime-config"; | ||
| export default defineHandler((event) => { | ||
| const runtimeConfig = useRuntimeConfig(); | ||
| return { runtimeConfig }; | ||
| }); | ||
| ``` | ||
| ## Environment Variables | ||
| Override config values via environment variables prefixed with `NITRO_`: | ||
| ```sh [.env] | ||
| # NEVER COMMIT SENSITIVE DATA. THIS IS ONLY FOR DEMO PURPOSES. | ||
| NITRO_API_KEY=secret-api-key | ||
| ``` | ||
| ## Learn More | ||
| - [Configuration](/docs/configuration) |
| # Server Fetch | ||
| > Internal server-to-server requests without network overhead. | ||
| <code-tree> | ||
| ```ts [nitro.config.ts] | ||
| import { defineConfig, serverFetch } from "nitro"; | ||
| export default defineConfig({ | ||
| serverDir: "./", | ||
| hooks: { | ||
| "dev:start": async () => { | ||
| const res = await serverFetch("/hello"); | ||
| const text = await res.text(); | ||
| console.log("Fetched /hello in nitro module:", res.status, text); | ||
| }, | ||
| }, | ||
| }); | ||
| ``` | ||
| ```json [package.json] | ||
| { | ||
| "type": "module", | ||
| "scripts": { | ||
| "dev": "nitro dev", | ||
| "build": "nitro build" | ||
| }, | ||
| "devDependencies": { | ||
| "nitro": "latest" | ||
| } | ||
| } | ||
| ``` | ||
| ```json [tsconfig.json] | ||
| { | ||
| "extends": "nitro/tsconfig" | ||
| } | ||
| ``` | ||
| ```ts [vite.config.ts] | ||
| import { defineConfig } from "vite"; | ||
| import { nitro } from "nitro/vite"; | ||
| export default defineConfig({ plugins: [nitro()] }); | ||
| ``` | ||
| ```ts [routes/hello.ts] | ||
| import { defineHandler } from "nitro"; | ||
| export default defineHandler(() => "Hello!"); | ||
| ``` | ||
| ```ts [routes/index.ts] | ||
| import { defineHandler } from "nitro"; | ||
| import { fetch } from "nitro"; | ||
| export default defineHandler(() => fetch("/hello")); | ||
| ``` | ||
| </code-tree> | ||
| When you need one route to call another, use Nitro's `fetch` function instead of the global fetch. It makes internal requests that stay in-process, avoiding network round-trips. The request never leaves the server. | ||
| ## Main Route | ||
| ```ts [routes/index.ts] | ||
| import { defineHandler } from "nitro"; | ||
| import { fetch } from "nitro"; | ||
| export default defineHandler(() => fetch("/hello")); | ||
| ``` | ||
| The index route imports `fetch` from `nitro` (not the global fetch) and calls the `/hello` route. This request is handled internally without going through the network stack. | ||
| ## Internal API Route | ||
| ```ts [routes/hello.ts] | ||
| import { defineHandler } from "nitro"; | ||
| export default defineHandler(() => "Hello!"); | ||
| ``` | ||
| A simple route that returns "Hello!". When the index route calls `fetch("/hello")`, this handler runs and its response is returned directly. | ||
| ## Learn More | ||
| - [Routing](/docs/routing) |
| # Shiki | ||
| > Server-side syntax highlighting in Nitro with Shiki. | ||
| <code-tree> | ||
| ```html [index.html] | ||
| <!doctype html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="utf-8" /> | ||
| <meta name="viewport" content="width=device-width,initial-scale=1" /> | ||
| <title>Hello World Snippet</title> | ||
| <link rel="stylesheet" href="styles.css" /> | ||
| </head> | ||
| <body> | ||
| <div class="card" role="region" aria-label="Code snippet"> | ||
| <div class="label">JavaScript</div> | ||
| <script server> | ||
| const hl = (code) => | ||
| serverFetch("/api/highlight", { | ||
| method: "POST", | ||
| body: code, | ||
| }); | ||
| </script> | ||
| <pre><code>{{{ hl(`console.log("💚 Simple is beautiful!");`) }}}</code></pre> | ||
| </div> | ||
| </body> | ||
| </html> | ||
| ``` | ||
| ```ts [nitro.config.ts] | ||
| import { defineConfig } from "nitro"; | ||
| export default defineConfig({ | ||
| serverDir: "./", | ||
| }); | ||
| ``` | ||
| ```json [package.json] | ||
| { | ||
| "type": "module", | ||
| "scripts": { | ||
| "dev": "vite dev", | ||
| "build": "vite build" | ||
| }, | ||
| "devDependencies": { | ||
| "nitro": "latest", | ||
| "shiki": "^3.22.0" | ||
| } | ||
| } | ||
| ``` | ||
| ```css [styles.css] | ||
| html, | ||
| body { | ||
| height: 100%; | ||
| margin: 0; | ||
| } | ||
| body { | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| background: #f6f8fa; | ||
| font-family: | ||
| system-ui, | ||
| -apple-system, | ||
| "Segoe UI", | ||
| Roboto, | ||
| "Helvetica Neue", | ||
| Arial, | ||
| "Noto Sans", | ||
| "Liberation Sans", | ||
| sans-serif; | ||
| } | ||
| .card { | ||
| text-align: left; | ||
| background: #0b1220; | ||
| color: #e6edf3; | ||
| padding: 1rem; | ||
| border-radius: 8px; | ||
| box-shadow: 0 8px 24px rgba(2, 6, 23, 0.2); | ||
| max-width: 90%; | ||
| width: 520px; | ||
| } | ||
| .label { | ||
| font-size: 12px; | ||
| color: #9aa7b2; | ||
| margin-bottom: 8px; | ||
| } | ||
| pre { | ||
| margin: 0; | ||
| font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, "Courier New", monospace; | ||
| font-size: 14px; | ||
| background: transparent; | ||
| white-space: pre; | ||
| overflow: auto; | ||
| } | ||
| ``` | ||
| ```json [tsconfig.json] | ||
| { | ||
| "extends": "nitro/tsconfig" | ||
| } | ||
| ``` | ||
| ```ts [vite.config.ts] | ||
| import { defineConfig } from "vite"; | ||
| import { nitro } from "nitro/vite"; | ||
| export default defineConfig({ | ||
| plugins: [nitro()], | ||
| }); | ||
| ``` | ||
| ```ts [api/highlight.ts] | ||
| import { createHighlighterCore } from "shiki/core"; | ||
| import { createOnigurumaEngine } from "shiki/engine/oniguruma"; | ||
| const highlighter = await createHighlighterCore({ | ||
| engine: createOnigurumaEngine(import("shiki/wasm")), | ||
| themes: [await import("shiki/themes/vitesse-dark.mjs")], | ||
| langs: [await import("shiki/langs/ts.mjs")], | ||
| }); | ||
| export default async ({ req }: { req: Request }) => { | ||
| const code = await req.text(); | ||
| const html = await highlighter.codeToHtml(code, { | ||
| lang: "ts", | ||
| theme: "vitesse-dark", | ||
| }); | ||
| return new Response(html, { | ||
| headers: { "Content-Type": "text/html; charset=utf-8" }, | ||
| }); | ||
| }; | ||
| ``` | ||
| </code-tree> | ||
| Use Shiki for syntax highlighting with TextMate grammars. This example highlights code on the server using Nitro's server scripts feature, which runs JavaScript inside HTML files before sending the response. | ||
| ## API Route | ||
| ```ts [api/highlight.ts] | ||
| import { createHighlighterCore } from "shiki/core"; | ||
| import { createOnigurumaEngine } from "shiki/engine/oniguruma"; | ||
| const highlighter = await createHighlighterCore({ | ||
| engine: createOnigurumaEngine(import("shiki/wasm")), | ||
| themes: [await import("shiki/themes/vitesse-dark.mjs")], | ||
| langs: [await import("shiki/langs/ts.mjs")], | ||
| }); | ||
| export default async ({ req }: { req: Request }) => { | ||
| const code = await req.text(); | ||
| const html = await highlighter.codeToHtml(code, { | ||
| lang: "ts", | ||
| theme: "vitesse-dark", | ||
| }); | ||
| return new Response(html, { | ||
| headers: { "Content-Type": "text/html; charset=utf-8" }, | ||
| }); | ||
| }; | ||
| ``` | ||
| Create a Shiki highlighter with the Vitesse Dark theme and TypeScript language support. When the API receives a POST request, it reads the code from the request body and returns highlighted HTML. | ||
| ## Server-Side Rendering | ||
| ```html [index.html] | ||
| <!doctype html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="utf-8" /> | ||
| <meta name="viewport" content="width=device-width,initial-scale=1" /> | ||
| <title>Hello World Snippet</title> | ||
| <link rel="stylesheet" href="styles.css" /> | ||
| </head> | ||
| <body> | ||
| <div class="card" role="region" aria-label="Code snippet"> | ||
| <div class="label">JavaScript</div> | ||
| <script server> | ||
| const hl = (code) => | ||
| serverFetch("/api/highlight", { | ||
| method: "POST", | ||
| body: code, | ||
| }); | ||
| </script> | ||
| <pre><code>{{{ hl(`console.log("💚 Simple is beautiful!");`) }}}</code></pre> | ||
| </div> | ||
| </body> | ||
| </html> | ||
| ``` | ||
| The `<script server>` tag runs on the server before the HTML is sent. It defines a helper function that calls the highlight API using `serverFetch`. The triple-brace syntax `{{{ }}}` outputs the result without escaping, so the highlighted HTML renders correctly. | ||
| ## Learn More | ||
| - [Shiki](https://shiki.style/) |
| # Virtual Routes | ||
| > Define routes programmatically using Nitro's virtual module system. | ||
| <code-tree> | ||
| ```ts [nitro.config.ts] | ||
| import { defineConfig } from "nitro"; | ||
| export default defineConfig({ | ||
| routes: { | ||
| "/": "#virtual-route", | ||
| }, | ||
| virtual: { | ||
| "#virtual-route": () => | ||
| /* js */ `export default () => new Response("Hello from virtual entry!")`, | ||
| }, | ||
| }); | ||
| ``` | ||
| ```json [package.json] | ||
| { | ||
| "type": "module", | ||
| "scripts": { | ||
| "build": "nitro build", | ||
| "dev": "nitro dev", | ||
| "preview": "node .output/server/index.mjs" | ||
| }, | ||
| "devDependencies": { | ||
| "nitro": "latest" | ||
| } | ||
| } | ||
| ``` | ||
| ```json [tsconfig.json] | ||
| { | ||
| "extends": "nitro/tsconfig" | ||
| } | ||
| ``` | ||
| ```ts [vite.config.ts] | ||
| import { defineConfig } from "vite"; | ||
| import { nitro } from "nitro/vite"; | ||
| export default defineConfig({ plugins: [nitro()] }); | ||
| ``` | ||
| </code-tree> | ||
| Virtual routes let you define handlers as strings in your config instead of creating separate files. This is useful when generating routes dynamically, building plugins, or keeping simple routes inline. | ||
| ## Configuration | ||
| ```ts [nitro.config.ts] | ||
| import { defineConfig } from "nitro"; | ||
| export default defineConfig({ | ||
| routes: { | ||
| "/": "#virtual-route", | ||
| }, | ||
| virtual: { | ||
| "#virtual-route": () => | ||
| /* js */ `export default () => new Response("Hello from virtual entry!")`, | ||
| }, | ||
| }); | ||
| ``` | ||
| The `routes` option maps URL paths to virtual module identifiers (prefixed with `#`). The `virtual` option defines the module content as a string or function returning a string. At build time, Nitro resolves these virtual modules to actual handlers. | ||
| There are no route files in this project. The entire handler is defined inline in the config, and Nitro generates the route at build time. | ||
| ## Learn More | ||
| - [Routing](/docs/routing) | ||
| - [Configuration](/docs/configuration) |
| # Vite Nitro Plugin | ||
| > Use Nitro as a Vite plugin for programmatic configuration. | ||
| <code-tree> | ||
| ```json [package.json] | ||
| { | ||
| "type": "module", | ||
| "scripts": { | ||
| "build": "vite build", | ||
| "preview": "vite preview", | ||
| "dev": "vite dev" | ||
| }, | ||
| "devDependencies": { | ||
| "nitro": "latest", | ||
| "vite": "beta" | ||
| } | ||
| } | ||
| ``` | ||
| ```json [tsconfig.json] | ||
| { | ||
| "extends": "nitro/tsconfig" | ||
| } | ||
| ``` | ||
| ```js [vite.config.mjs] | ||
| import { defineConfig } from "vite"; | ||
| import { nitro } from "nitro/vite"; | ||
| export default defineConfig({ | ||
| plugins: [ | ||
| nitro(), | ||
| { | ||
| name: "my-nitro-plugin", | ||
| nitro: { | ||
| setup: (nitro) => { | ||
| nitro.options.routes["/"] = "#virtual-by-plugin"; | ||
| nitro.options.virtual["#virtual-by-plugin"] = | ||
| `export default () => new Response("Hello from virtual entry!")`; | ||
| }, | ||
| }, | ||
| }, | ||
| ], | ||
| }); | ||
| ``` | ||
| </code-tree> | ||
| Instead of using a separate `nitro.config.ts`, you can configure Nitro directly in your Vite config. This gives you access to Nitro's setup hook where you can register routes and virtual modules programmatically. | ||
| ## Vite Configuration | ||
| ```js [vite.config.mjs] | ||
| import { defineConfig } from "vite"; | ||
| import { nitro } from "nitro/vite"; | ||
| export default defineConfig({ | ||
| plugins: [ | ||
| nitro(), | ||
| { | ||
| name: "my-nitro-plugin", | ||
| nitro: { | ||
| setup: (nitro) => { | ||
| nitro.options.routes["/"] = "#virtual-by-plugin"; | ||
| nitro.options.virtual["#virtual-by-plugin"] = | ||
| `export default () => new Response("Hello from virtual entry!")`; | ||
| }, | ||
| }, | ||
| }, | ||
| ], | ||
| }); | ||
| ``` | ||
| The config adds two plugins: the `nitro()` plugin and a custom plugin that uses the `nitro.setup` hook. Inside the setup function, you have access to Nitro's options object. This example registers a virtual route at `/` that maps to a virtual module `#virtual-by-plugin`, then defines that module inline. | ||
| ## Learn More | ||
| - [Configuration](/docs/configuration) |
| # Vite RSC | ||
| > React Server Components with Vite and Nitro. | ||
| <code-tree> | ||
| ```json [package.json] | ||
| { | ||
| "name": "@vitejs/plugin-rsc-examples-starter", | ||
| "version": "0.0.0", | ||
| "private": true, | ||
| "license": "MIT", | ||
| "type": "module", | ||
| "scripts": { | ||
| "dev": "vite", | ||
| "build": "vite build", | ||
| "preview": "vite preview" | ||
| }, | ||
| "dependencies": { | ||
| "react": "^19.2.4", | ||
| "react-dom": "^19.2.4" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/react": "^19.2.13", | ||
| "@types/react-dom": "^19.2.3", | ||
| "@vitejs/plugin-react": "^5.1.3", | ||
| "@vitejs/plugin-rsc": "^0.5.19", | ||
| "nitro": "latest", | ||
| "rsc-html-stream": "^0.0.7", | ||
| "vite": "beta" | ||
| } | ||
| } | ||
| ``` | ||
| ```json [tsconfig.json] | ||
| { | ||
| "extends": "nitro/tsconfig", | ||
| "compilerOptions": { | ||
| "lib": ["ESNext", "DOM", "DOM.Iterable"], | ||
| "types": ["vite/client", "@vitejs/plugin-rsc/types"], | ||
| "jsx": "react-jsx" | ||
| } | ||
| } | ||
| ``` | ||
| ```ts [vite.config.ts] | ||
| import { defineConfig } from "vite"; | ||
| import { nitro } from "nitro/vite"; | ||
| import rsc from "@vitejs/plugin-rsc"; | ||
| import react from "@vitejs/plugin-react"; | ||
| export default defineConfig({ | ||
| plugins: [ | ||
| nitro(), | ||
| rsc({ | ||
| serverHandler: false, | ||
| entries: { | ||
| ssr: "./app/framework/entry.ssr.tsx", | ||
| rsc: "./app/framework/entry.rsc.tsx", | ||
| }, | ||
| }), | ||
| react(), | ||
| ], | ||
| environments: { | ||
| client: { | ||
| build: { | ||
| rollupOptions: { | ||
| input: { index: "./app/framework/entry.browser.tsx" }, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }); | ||
| ``` | ||
| ```tsx [app/action.tsx] | ||
| "use server"; | ||
| let serverCounter = 0; | ||
| export async function getServerCounter() { | ||
| return serverCounter; | ||
| } | ||
| export async function updateServerCounter(change: number) { | ||
| serverCounter += change; | ||
| } | ||
| ``` | ||
| ```tsx [app/client.tsx] | ||
| "use client"; | ||
| import React from "react"; | ||
| export function ClientCounter() { | ||
| const [count, setCount] = React.useState(0); | ||
| return <button onClick={() => setCount((count) => count + 1)}>Client Counter: {count}</button>; | ||
| } | ||
| ``` | ||
| ```css [app/index.css] | ||
| :root { | ||
| font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; | ||
| line-height: 1.5; | ||
| font-weight: 400; | ||
| color-scheme: light dark; | ||
| color: rgba(255, 255, 255, 0.87); | ||
| background-color: #242424; | ||
| font-synthesis: none; | ||
| text-rendering: optimizeLegibility; | ||
| -webkit-font-smoothing: antialiased; | ||
| -moz-osx-font-smoothing: grayscale; | ||
| } | ||
| a { | ||
| font-weight: 500; | ||
| color: #646cff; | ||
| text-decoration: inherit; | ||
| } | ||
| a:hover { | ||
| color: #535bf2; | ||
| } | ||
| body { | ||
| margin: 0; | ||
| display: flex; | ||
| place-items: center; | ||
| min-width: 320px; | ||
| min-height: 100vh; | ||
| } | ||
| h1 { | ||
| font-size: 3.2em; | ||
| line-height: 1.1; | ||
| } | ||
| button { | ||
| border-radius: 8px; | ||
| border: 1px solid transparent; | ||
| padding: 0.6em 1.2em; | ||
| font-size: 1em; | ||
| font-weight: 500; | ||
| font-family: inherit; | ||
| background-color: #1a1a1a; | ||
| cursor: pointer; | ||
| transition: border-color 0.25s; | ||
| } | ||
| button:hover { | ||
| border-color: #646cff; | ||
| } | ||
| button:focus, | ||
| button:focus-visible { | ||
| outline: 4px auto -webkit-focus-ring-color; | ||
| } | ||
| @media (prefers-color-scheme: light) { | ||
| :root { | ||
| color: #213547; | ||
| background-color: #ffffff; | ||
| } | ||
| a:hover { | ||
| color: #747bff; | ||
| } | ||
| button { | ||
| background-color: #f9f9f9; | ||
| } | ||
| } | ||
| #root { | ||
| max-width: 1280px; | ||
| margin: 0 auto; | ||
| padding: 2rem; | ||
| text-align: center; | ||
| } | ||
| .logo { | ||
| height: 6em; | ||
| padding: 1.5em; | ||
| will-change: filter; | ||
| transition: filter 300ms; | ||
| } | ||
| .logo:hover { | ||
| filter: drop-shadow(0 0 2em #646cffaa); | ||
| } | ||
| .logo.react:hover { | ||
| filter: drop-shadow(0 0 2em #61dafbaa); | ||
| } | ||
| @keyframes logo-spin { | ||
| from { | ||
| transform: rotate(0deg); | ||
| } | ||
| to { | ||
| transform: rotate(360deg); | ||
| } | ||
| } | ||
| @media (prefers-reduced-motion: no-preference) { | ||
| a:nth-of-type(2) .logo { | ||
| animation: logo-spin infinite 20s linear; | ||
| } | ||
| } | ||
| .card { | ||
| padding: 1rem; | ||
| } | ||
| .read-the-docs { | ||
| color: #888; | ||
| text-align: left; | ||
| } | ||
| ``` | ||
| ```tsx [app/root.tsx] | ||
| import "./index.css"; // css import is automatically injected in exported server components | ||
| import viteLogo from "./assets/vite.svg"; | ||
| import { getServerCounter, updateServerCounter } from "./action.tsx"; | ||
| import reactLogo from "./assets/react.svg"; | ||
| import nitroLogo from "./assets/nitro.svg"; | ||
| import { ClientCounter } from "./client.tsx"; | ||
| export function Root(props: { url: URL }) { | ||
| return ( | ||
| <html lang="en"> | ||
| <head> | ||
| {/* eslint-disable-next-line unicorn/text-encoding-identifier-case */} | ||
| <meta charSet="UTF-8" /> | ||
| <link rel="icon" type="image/svg+xml" href="/vite.svg" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <title>Nitro + Vite + RSC</title> | ||
| </head> | ||
| <body> | ||
| <App {...props} /> | ||
| </body> | ||
| </html> | ||
| ); | ||
| } | ||
| function App(props: { url: URL }) { | ||
| return ( | ||
| <div id="root"> | ||
| <div> | ||
| <a href="https://vite.dev" target="_blank"> | ||
| <img src={viteLogo} className="logo" alt="Vite logo" /> | ||
| </a> | ||
| <a href="https://react.dev/reference/rsc/server-components" target="_blank"> | ||
| <img src={reactLogo} className="logo react" alt="React logo" /> | ||
| </a> | ||
| <a href="https://nitro.build" target="_blank"> | ||
| <img src={nitroLogo} className="logo" alt="Nitro logo" /> | ||
| </a> | ||
| </div> | ||
| <h1>Vite + RSC + Nitro</h1> | ||
| <div className="card"> | ||
| <ClientCounter /> | ||
| </div> | ||
| <div className="card"> | ||
| <form action={updateServerCounter.bind(null, 1)}> | ||
| <button>Server Counter: {getServerCounter()}</button> | ||
| </form> | ||
| </div> | ||
| <div className="card">Request URL: {props.url?.href}</div> | ||
| <ul className="read-the-docs"> | ||
| <li> | ||
| Edit <code>src/client.tsx</code> to test client HMR. | ||
| </li> | ||
| <li> | ||
| Edit <code>src/root.tsx</code> to test server HMR. | ||
| </li> | ||
| <li> | ||
| Visit{" "} | ||
| <a href="./_.rsc" target="_blank"> | ||
| <code>_.rsc</code> | ||
| </a>{" "} | ||
| to view RSC stream payload. | ||
| </li> | ||
| <li> | ||
| Visit{" "} | ||
| <a href="?__nojs" target="_blank"> | ||
| <code>?__nojs</code> | ||
| </a>{" "} | ||
| to test server action without js enabled. | ||
| </li> | ||
| </ul> | ||
| </div> | ||
| ); | ||
| } | ||
| ``` | ||
| ```text [app/assets/nitro.svg] | ||
| <!-- nitro logo --> | ||
| <svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg"> | ||
| <g clip-path="url(#clip0_115_108)"> | ||
| <path fill-rule="evenodd" clip-rule="evenodd" | ||
| d="M35.2166 7.02016C28.0478 -1.38317 15.4241 -2.38397 7.02077 4.78481C-1.38256 11.9536 -2.38336 24.5773 4.78542 32.9806C11.9542 41.3839 24.5779 42.3847 32.9812 35.216C41.3846 28.0472 42.3854 15.4235 35.2166 7.02016ZM25.2525 17.5175C26.0233 17.5175 26.5155 18.3527 26.1287 19.0194L26.0175 19.2111L18.4696 31.6294C18.3293 31.8602 18.0788 32.001 17.8088 32.001H17.0883C16.5946 32.001 16.2336 31.5349 16.3573 31.0569L18.4054 23.1384C18.5691 22.5053 18.0912 21.888 17.4373 21.888H14.2914C13.6375 21.888 13.1596 21.2708 13.3232 20.6377L16.4137 8.68289C16.5261 8.28056 16.8904 7.99734 17.3081 8.00208C17.3587 8.00266 17.4046 8.0035 17.4427 8.0047L20.6109 8.00465C21.217 8.00436 21.684 8.53896 21.6023 9.13949L21.5828 9.28246L20.3746 16.349C20.2702 16.9598 20.7406 17.5175 21.3603 17.5175H25.2525Z" | ||
| fill="url(#paint0_diamond_115_108)" /> | ||
| <mask id="mask0_115_108" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" | ||
| width="40" height="41"> | ||
| <circle cx="20" cy="20.001" r="20" fill="url(#paint1_diamond_115_108)" /> | ||
| </mask> | ||
| <g mask="url(#mask0_115_108)"> | ||
| <g filter="url(#filter0_f_115_108)"> | ||
| <path | ||
| d="M1.11145 13.4267C0.0703174 16.4179 -0.245523 19.6136 0.189923 22.7507C0.62537 25.8879 1.79965 28.8768 3.61611 31.4713C5.43256 34.0659 7.83925 36.192 10.6381 37.6746C13.4369 39.1572 16.5478 39.9538 19.7147 39.999C22.8816 40.0442 26.0139 39.3366 28.8539 37.9345C31.6939 36.5324 34.1602 34.4758 36.05 31.9341C37.9397 29.3924 39.1988 26.4383 39.7236 23.3148C40.2483 20.1914 40.0238 16.9879 39.0684 13.9682L33.2532 15.808C33.9172 17.9068 34.0732 20.1333 33.7085 22.3042C33.3438 24.4751 32.4687 26.5283 31.1552 28.2949C29.8418 30.0615 28.1276 31.4908 26.1537 32.4653C24.1799 33.4399 22.0028 33.9316 19.8017 33.9002C17.6006 33.8688 15.4384 33.3151 13.4932 32.2847C11.5479 31.2543 9.87518 29.7766 8.61269 27.9733C7.35019 26.1699 6.53403 24.0926 6.23138 21.9122C5.92873 19.7317 6.14825 17.5106 6.87187 15.4316L1.11145 13.4267Z" | ||
| fill="white" /> | ||
| </g> | ||
| </g> | ||
| </g> | ||
| <defs> | ||
| <filter id="filter0_f_115_108" x="-10" y="3.42667" width="60" height="46.5744" | ||
| filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> | ||
| <feFlood flood-opacity="0" result="BackgroundImageFix" /> | ||
| <feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" /> | ||
| <feGaussianBlur stdDeviation="5" result="effect1_foregroundBlur_115_108" /> | ||
| </filter> | ||
| <radialGradient id="paint0_diamond_115_108" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" | ||
| gradientTransform="translate(4.00069 20.0004) scale(39.0007 397.71)"> | ||
| <stop stop-color="#31B2F3" /> | ||
| <stop offset="0.473958" stop-color="#F27CEC" /> | ||
| <stop offset="1" stop-color="#FD6641" /> | ||
| </radialGradient> | ||
| <radialGradient id="paint1_diamond_115_108" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" | ||
| gradientTransform="translate(4 20.0011) scale(39 397.703)"> | ||
| <stop stop-color="#F27CEC" /> | ||
| <stop offset="0.484375" stop-color="#31B2F3" /> | ||
| <stop offset="1" stop-color="#7D7573" /> | ||
| </radialGradient> | ||
| <clipPath id="clip0_115_108"> | ||
| <rect width="146" height="40.001" fill="white" /> | ||
| </clipPath> | ||
| </defs> | ||
| </svg> | ||
| ``` | ||
| ```text [app/assets/react.svg] | ||
| <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg> | ||
| ``` | ||
| ```text [app/assets/vite.svg] | ||
| <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg> | ||
| ``` | ||
| ```tsx [app/framework/entry.browser.tsx] | ||
| import { | ||
| createFromReadableStream, | ||
| createFromFetch, | ||
| setServerCallback, | ||
| createTemporaryReferenceSet, | ||
| encodeReply, | ||
| } from "@vitejs/plugin-rsc/browser"; | ||
| import React from "react"; | ||
| import { createRoot, hydrateRoot } from "react-dom/client"; | ||
| import { rscStream } from "rsc-html-stream/client"; | ||
| import { GlobalErrorBoundary } from "./error-boundary"; | ||
| import type { RscPayload } from "./entry.rsc"; | ||
| import { createRscRenderRequest } from "./request"; | ||
| async function main() { | ||
| // Stash `setPayload` function to trigger re-rendering | ||
| // from outside of `BrowserRoot` component (e.g. server function call, navigation, hmr) | ||
| let setPayload: (v: RscPayload) => void; | ||
| // Deserialize RSC stream back to React VDOM for CSR | ||
| const initialPayload = await createFromReadableStream<RscPayload>( | ||
| // Initial RSC stream is injected in SSR stream as <script>...FLIGHT_DATA...</script> | ||
| rscStream | ||
| ); | ||
| // Browser root component to (re-)render RSC payload as state | ||
| function BrowserRoot() { | ||
| const [payload, setPayload_] = React.useState(initialPayload); | ||
| React.useEffect(() => { | ||
| setPayload = (v) => React.startTransition(() => setPayload_(v)); | ||
| }, [setPayload_]); | ||
| // Re-fetch/render on client side navigation | ||
| React.useEffect(() => { | ||
| return listenNavigation(() => fetchRscPayload()); | ||
| }, []); | ||
| return payload.root; | ||
| } | ||
| // Re-fetch RSC and trigger re-rendering | ||
| async function fetchRscPayload() { | ||
| const renderRequest = createRscRenderRequest(globalThis.location.href); | ||
| const payload = await createFromFetch<RscPayload>(fetch(renderRequest)); | ||
| setPayload(payload); | ||
| } | ||
| // Register a handler which will be internally called by React | ||
| // on server function request after hydration. | ||
| setServerCallback(async (id, args) => { | ||
| const temporaryReferences = createTemporaryReferenceSet(); | ||
| const renderRequest = createRscRenderRequest(globalThis.location.href, { | ||
| id, | ||
| body: await encodeReply(args, { temporaryReferences }), | ||
| }); | ||
| const payload = await createFromFetch<RscPayload>(fetch(renderRequest), { | ||
| temporaryReferences, | ||
| }); | ||
| setPayload(payload); | ||
| const { ok, data } = payload.returnValue!; | ||
| if (!ok) throw data; | ||
| return data; | ||
| }); | ||
| // Hydration | ||
| const browserRoot = ( | ||
| <React.StrictMode> | ||
| <GlobalErrorBoundary> | ||
| <BrowserRoot /> | ||
| </GlobalErrorBoundary> | ||
| </React.StrictMode> | ||
| ); | ||
| if ("__NO_HYDRATE" in globalThis) { | ||
| createRoot(document).render(browserRoot); | ||
| } else { | ||
| hydrateRoot(document, browserRoot, { | ||
| formState: initialPayload.formState, | ||
| }); | ||
| } | ||
| // Implement server HMR by triggering re-fetch/render of RSC upon server code change | ||
| if (import.meta.hot) { | ||
| import.meta.hot.on("rsc:update", () => { | ||
| fetchRscPayload(); | ||
| }); | ||
| } | ||
| } | ||
| // A little helper to setup events interception for client side navigation | ||
| function listenNavigation(onNavigation: () => void) { | ||
| globalThis.addEventListener("popstate", onNavigation); | ||
| const oldPushState = globalThis.history.pushState; | ||
| globalThis.history.pushState = function (...args) { | ||
| const res = oldPushState.apply(this, args); | ||
| onNavigation(); | ||
| return res; | ||
| }; | ||
| const oldReplaceState = globalThis.history.replaceState; | ||
| globalThis.history.replaceState = function (...args) { | ||
| const res = oldReplaceState.apply(this, args); | ||
| onNavigation(); | ||
| return res; | ||
| }; | ||
| function onClick(e: MouseEvent) { | ||
| const link = (e.target as Element).closest("a"); | ||
| if ( | ||
| link && | ||
| link instanceof HTMLAnchorElement && | ||
| link.href && | ||
| (!link.target || link.target === "_self") && | ||
| link.origin === location.origin && | ||
| !link.hasAttribute("download") && | ||
| e.button === 0 && // left clicks only | ||
| !e.metaKey && // open in new tab (mac) | ||
| !e.ctrlKey && // open in new tab (windows) | ||
| !e.altKey && // download | ||
| !e.shiftKey && | ||
| !e.defaultPrevented | ||
| ) { | ||
| e.preventDefault(); | ||
| history.pushState(null, "", link.href); | ||
| } | ||
| } | ||
| document.addEventListener("click", onClick); | ||
| return () => { | ||
| document.removeEventListener("click", onClick); | ||
| globalThis.removeEventListener("popstate", onNavigation); | ||
| globalThis.history.pushState = oldPushState; | ||
| globalThis.history.replaceState = oldReplaceState; | ||
| }; | ||
| } | ||
| // eslint-disable-next-line unicorn/prefer-top-level-await | ||
| main(); | ||
| ``` | ||
| ```tsx [app/framework/entry.rsc.tsx] | ||
| import { | ||
| renderToReadableStream, | ||
| createTemporaryReferenceSet, | ||
| decodeReply, | ||
| loadServerAction, | ||
| decodeAction, | ||
| decodeFormState, | ||
| } from "@vitejs/plugin-rsc/rsc"; | ||
| import type { ReactFormState } from "react-dom/client"; | ||
| import { Root } from "../root.tsx"; | ||
| import { parseRenderRequest } from "./request.tsx"; | ||
| // The schema of payload which is serialized into RSC stream on rsc environment | ||
| // and deserialized on ssr/client environments. | ||
| export type RscPayload = { | ||
| // this demo renders/serializes/deserializes entire root html element | ||
| // but this mechanism can be changed to render/fetch different parts of components | ||
| // based on your own route conventions. | ||
| root: React.ReactNode; | ||
| // Server action return value of non-progressive enhancement case | ||
| returnValue?: { ok: boolean; data: unknown }; | ||
| // Server action form state (e.g. useActionState) of progressive enhancement case | ||
| formState?: ReactFormState; | ||
| }; | ||
| // The plugin by default assumes `rsc` entry having default export of request handler. | ||
| // however, how server entries are executed can be customized by registering own server handler. | ||
| export default async function handler(request: Request): Promise<Response> { | ||
| // Differentiate RSC, SSR, action, etc. | ||
| const renderRequest = parseRenderRequest(request); | ||
| request = renderRequest.request; | ||
| // Handle server function request | ||
| let returnValue: RscPayload["returnValue"] | undefined; | ||
| let formState: ReactFormState | undefined; | ||
| let temporaryReferences: unknown | undefined; | ||
| let actionStatus: number | undefined; | ||
| if (renderRequest.isAction === true) { | ||
| if (renderRequest.actionId) { | ||
| // Action is called via `ReactClient.setServerCallback`. | ||
| const contentType = request.headers.get("content-type"); | ||
| const body = contentType?.startsWith("multipart/form-data") | ||
| ? await request.formData() | ||
| : await request.text(); | ||
| temporaryReferences = createTemporaryReferenceSet(); | ||
| const args = await decodeReply(body, { temporaryReferences }); | ||
| const action = await loadServerAction(renderRequest.actionId); | ||
| try { | ||
| // eslint-disable-next-line prefer-spread | ||
| const data = await action.apply(null, args); | ||
| returnValue = { ok: true, data }; | ||
| } catch (error_) { | ||
| returnValue = { ok: false, data: error_ }; | ||
| actionStatus = 500; | ||
| } | ||
| } else { | ||
| // Otherwise server function is called via `<form action={...}>` | ||
| // before hydration (e.g. when JavaScript is disabled). | ||
| // aka progressive enhancement. | ||
| const formData = await request.formData(); | ||
| const decodedAction = await decodeAction(formData); | ||
| try { | ||
| const result = await decodedAction(); | ||
| formState = await decodeFormState(result, formData); | ||
| } catch { | ||
| // there's no single general obvious way to surface this error, | ||
| // so explicitly return classic 500 response. | ||
| return new Response("Internal Server Error: server action failed", { | ||
| status: 500, | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| // Serialization from React VDOM tree to RSC stream. | ||
| // We render RSC stream after handling server function request | ||
| // so that new render reflects updated state from server function call | ||
| // to achieve single round trip to mutate and fetch from server. | ||
| const rscPayload: RscPayload = { | ||
| root: <Root url={renderRequest.url} />, | ||
| formState, | ||
| returnValue, | ||
| }; | ||
| const rscOptions = { temporaryReferences }; | ||
| const rscStream = renderToReadableStream<RscPayload>(rscPayload, rscOptions); | ||
| // Respond RSC stream without HTML rendering as decided by `RenderRequest` | ||
| if (renderRequest.isRsc) { | ||
| return new Response(rscStream, { | ||
| status: actionStatus, | ||
| headers: { | ||
| "content-type": "text/x-component;charset=utf-8", | ||
| }, | ||
| }); | ||
| } | ||
| // Delegate to SSR environment for HTML rendering. | ||
| // The plugin provides `loadModule` helper to allow loading SSR environment entry module | ||
| // in RSC environment. however this can be customized by implementing own runtime communication | ||
| // e.g. `@cloudflare/vite-plugin`'s service binding. | ||
| const ssrEntryModule = await import.meta.viteRsc.loadModule<typeof import("./entry.ssr.tsx")>( | ||
| "ssr", | ||
| "index" | ||
| ); | ||
| const ssrResult = await ssrEntryModule.renderHTML(rscStream, { | ||
| formState, | ||
| // Allow quick simulation of JavaScript disabled browser | ||
| debugNoJS: renderRequest.url.searchParams.has("__nojs"), | ||
| }); | ||
| // Respond HTML | ||
| return new Response(ssrResult.stream, { | ||
| status: ssrResult.status, | ||
| headers: { | ||
| "Content-Type": "text/html", | ||
| }, | ||
| }); | ||
| } | ||
| if (import.meta.hot) { | ||
| import.meta.hot.accept(); | ||
| } | ||
| ``` | ||
| ```tsx [app/framework/entry.ssr.tsx] | ||
| import { createFromReadableStream } from "@vitejs/plugin-rsc/ssr"; | ||
| import React from "react"; | ||
| import type { ReactFormState } from "react-dom/client"; | ||
| import { renderToReadableStream } from "react-dom/server.edge"; | ||
| import { injectRSCPayload } from "rsc-html-stream/server"; | ||
| import type { RscPayload } from "./entry.rsc"; | ||
| export default { | ||
| fetch: async (request: Request) => { | ||
| const rscEntryModule = await import.meta.viteRsc.loadModule<typeof import("./entry.rsc")>( | ||
| "rsc", | ||
| "index" | ||
| ); | ||
| return rscEntryModule.default(request); | ||
| }, | ||
| }; | ||
| export async function renderHTML( | ||
| rscStream: ReadableStream<Uint8Array>, | ||
| options: { | ||
| formState?: ReactFormState; | ||
| nonce?: string; | ||
| debugNoJS?: boolean; | ||
| } | ||
| ): Promise<{ stream: ReadableStream<Uint8Array>; status?: number }> { | ||
| // Duplicate one RSC stream into two. | ||
| // - one for SSR (ReactClient.createFromReadableStream below) | ||
| // - another for browser hydration payload by injecting <script>...FLIGHT_DATA...</script>. | ||
| const [rscStream1, rscStream2] = rscStream.tee(); | ||
| // Deserialize RSC stream back to React VDOM | ||
| let payload: Promise<RscPayload> | undefined; | ||
| function SsrRoot() { | ||
| // Deserialization needs to be kicked off inside ReactDOMServer context | ||
| // for ReactDOMServer preinit/preloading to work | ||
| payload ??= createFromReadableStream<RscPayload>(rscStream1); | ||
| return React.use(payload).root; | ||
| } | ||
| // Render HTML (traditional SSR) | ||
| const bootstrapScriptContent = await import.meta.viteRsc.loadBootstrapScriptContent("index"); | ||
| let htmlStream: ReadableStream<Uint8Array>; | ||
| let status: number | undefined; | ||
| try { | ||
| htmlStream = await renderToReadableStream(<SsrRoot />, { | ||
| bootstrapScriptContent: options?.debugNoJS ? undefined : bootstrapScriptContent, | ||
| nonce: options?.nonce, | ||
| formState: options?.formState, | ||
| }); | ||
| } catch { | ||
| // fallback to render an empty shell and run pure CSR on browser, | ||
| // which can replay server component error and trigger error boundary. | ||
| status = 500; | ||
| htmlStream = await renderToReadableStream( | ||
| <html> | ||
| <body> | ||
| <noscript>Internal Server Error: SSR failed</noscript> | ||
| </body> | ||
| </html>, | ||
| { | ||
| bootstrapScriptContent: | ||
| `self.__NO_HYDRATE=1;` + (options?.debugNoJS ? "" : bootstrapScriptContent), | ||
| nonce: options?.nonce, | ||
| } | ||
| ); | ||
| } | ||
| let responseStream: ReadableStream<Uint8Array> = htmlStream; | ||
| if (!options?.debugNoJS) { | ||
| // Initial RSC stream is injected in HTML stream as <script>...FLIGHT_DATA...</script> | ||
| // using utility made by devongovett https://github.com/devongovett/rsc-html-stream | ||
| responseStream = responseStream.pipeThrough( | ||
| injectRSCPayload(rscStream2, { | ||
| nonce: options?.nonce, | ||
| }) | ||
| ); | ||
| } | ||
| return { stream: responseStream, status }; | ||
| } | ||
| ``` | ||
| ```tsx [app/framework/error-boundary.tsx] | ||
| "use client"; | ||
| import React from "react"; | ||
| // Minimal ErrorBoundary example to handle errors globally on browser | ||
| export function GlobalErrorBoundary(props: { children?: React.ReactNode }) { | ||
| return <ErrorBoundary errorComponent={DefaultGlobalErrorPage}>{props.children}</ErrorBoundary>; | ||
| } | ||
| // https://github.com/vercel/next.js/blob/33f8428f7066bf8b2ec61f025427ceb2a54c4bdf/packages/next/src/client/components/error-boundary.tsx | ||
| // https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary | ||
| class ErrorBoundary extends React.Component<{ | ||
| children?: React.ReactNode; | ||
| errorComponent: React.FC<{ | ||
| error: Error; | ||
| reset: () => void; | ||
| }>; | ||
| }> { | ||
| override state: { error?: Error } = {}; | ||
| static getDerivedStateFromError(error: Error) { | ||
| return { error }; | ||
| } | ||
| reset = () => { | ||
| this.setState({ error: null }); | ||
| }; | ||
| override render() { | ||
| const error = this.state.error; | ||
| if (error) { | ||
| return <this.props.errorComponent error={error} reset={this.reset} />; | ||
| } | ||
| return this.props.children; | ||
| } | ||
| } | ||
| // https://github.com/vercel/next.js/blob/677c9b372faef680d17e9ba224743f44e1107661/packages/next/src/build/webpack/loaders/next-app-loader.ts#L73 | ||
| // https://github.com/vercel/next.js/blob/677c9b372faef680d17e9ba224743f44e1107661/packages/next/src/client/components/error-boundary.tsx#L145 | ||
| function DefaultGlobalErrorPage(props: { error: Error; reset: () => void }) { | ||
| return ( | ||
| <html> | ||
| <head> | ||
| <title>Unexpected Error</title> | ||
| </head> | ||
| <body | ||
| style={{ | ||
| height: "100vh", | ||
| display: "flex", | ||
| flexDirection: "column", | ||
| placeContent: "center", | ||
| placeItems: "center", | ||
| fontSize: "16px", | ||
| fontWeight: 400, | ||
| lineHeight: "24px", | ||
| }} | ||
| > | ||
| <p>Caught an unexpected error</p> | ||
| <pre> | ||
| Error:{" "} | ||
| {import.meta.env.DEV && "message" in props.error ? props.error.message : "(Unknown)"} | ||
| </pre> | ||
| <button | ||
| onClick={() => { | ||
| React.startTransition(() => { | ||
| props.reset(); | ||
| }); | ||
| }} | ||
| > | ||
| Reset | ||
| </button> | ||
| </body> | ||
| </html> | ||
| ); | ||
| } | ||
| ``` | ||
| ```tsx [app/framework/request.tsx] | ||
| // Framework conventions (arbitrary choices for this demo): | ||
| // - Use `_.rsc` URL suffix to differentiate RSC requests from SSR requests | ||
| // - Use `x-rsc-action` header to pass server action ID | ||
| const URL_POSTFIX = "_.rsc"; | ||
| const HEADER_ACTION_ID = "x-rsc-action"; | ||
| // Parsed request information used to route between RSC/SSR rendering and action handling. | ||
| // Created by parseRenderRequest() from incoming HTTP requests. | ||
| type RenderRequest = { | ||
| isRsc: boolean; // true if request should return RSC payload (via _.rsc suffix) | ||
| isAction: boolean; // true if this is a server action call (POST request) | ||
| actionId?: string; // server action ID from x-rsc-action header | ||
| request: Request; // normalized Request with _.rsc suffix removed from URL | ||
| url: URL; // normalized URL with _.rsc suffix removed | ||
| }; | ||
| export function createRscRenderRequest( | ||
| urlString: string, | ||
| action?: { id: string; body: BodyInit } | ||
| ): Request { | ||
| const url = new URL(urlString); | ||
| url.pathname += URL_POSTFIX; | ||
| const headers = new Headers(); | ||
| if (action) { | ||
| headers.set(HEADER_ACTION_ID, action.id); | ||
| } | ||
| return new Request(url.toString(), { | ||
| method: action ? "POST" : "GET", | ||
| headers, | ||
| body: action?.body, | ||
| }); | ||
| } | ||
| export function parseRenderRequest(request: Request): RenderRequest { | ||
| const url = new URL(request.url); | ||
| const isAction = request.method === "POST"; | ||
| if (url.pathname.endsWith(URL_POSTFIX)) { | ||
| url.pathname = url.pathname.slice(0, -URL_POSTFIX.length); | ||
| const actionId = request.headers.get(HEADER_ACTION_ID) || undefined; | ||
| if (request.method === "POST" && !actionId) { | ||
| throw new Error("Missing action id header for RSC action request"); | ||
| } | ||
| return { | ||
| isRsc: true, | ||
| isAction, | ||
| actionId, | ||
| request: new Request(url, request), | ||
| url, | ||
| }; | ||
| } else { | ||
| return { | ||
| isRsc: false, | ||
| isAction, | ||
| request, | ||
| url, | ||
| }; | ||
| } | ||
| } | ||
| ``` | ||
| </code-tree> | ||
| This example demonstrates React Server Components (RSC) using Vite's experimental RSC plugin with Nitro. It includes server components, client components, server actions, and streaming SSR. | ||
| ## Overview | ||
| 1. **SSR Entry** handles incoming requests and renders React components to HTML | ||
| 2. **Root Component** defines the page structure as a server component | ||
| 3. **Client Components** use the `"use client"` directive for interactive parts | ||
| ## 1. SSR Entry | ||
| ```tsx [app/framework/entry.ssr.tsx] | ||
| import { createFromReadableStream } from "@vitejs/plugin-rsc/ssr"; | ||
| import React from "react"; | ||
| import type { ReactFormState } from "react-dom/client"; | ||
| import { renderToReadableStream } from "react-dom/server.edge"; | ||
| import { injectRSCPayload } from "rsc-html-stream/server"; | ||
| import type { RscPayload } from "./entry.rsc"; | ||
| export default { | ||
| fetch: async (request: Request) => { | ||
| const rscEntryModule = await import.meta.viteRsc.loadModule<typeof import("./entry.rsc")>( | ||
| "rsc", | ||
| "index" | ||
| ); | ||
| return rscEntryModule.default(request); | ||
| }, | ||
| }; | ||
| export async function renderHTML( | ||
| rscStream: ReadableStream<Uint8Array>, | ||
| options: { | ||
| formState?: ReactFormState; | ||
| nonce?: string; | ||
| debugNoJS?: boolean; | ||
| } | ||
| ): Promise<{ stream: ReadableStream<Uint8Array>; status?: number }> { | ||
| // Duplicate one RSC stream into two. | ||
| // - one for SSR (ReactClient.createFromReadableStream below) | ||
| // - another for browser hydration payload by injecting <script>...FLIGHT_DATA...</script>. | ||
| const [rscStream1, rscStream2] = rscStream.tee(); | ||
| // Deserialize RSC stream back to React VDOM | ||
| let payload: Promise<RscPayload> | undefined; | ||
| function SsrRoot() { | ||
| // Deserialization needs to be kicked off inside ReactDOMServer context | ||
| // for ReactDOMServer preinit/preloading to work | ||
| payload ??= createFromReadableStream<RscPayload>(rscStream1); | ||
| return React.use(payload).root; | ||
| } | ||
| // Render HTML (traditional SSR) | ||
| const bootstrapScriptContent = await import.meta.viteRsc.loadBootstrapScriptContent("index"); | ||
| let htmlStream: ReadableStream<Uint8Array>; | ||
| let status: number | undefined; | ||
| try { | ||
| htmlStream = await renderToReadableStream(<SsrRoot />, { | ||
| bootstrapScriptContent: options?.debugNoJS ? undefined : bootstrapScriptContent, | ||
| nonce: options?.nonce, | ||
| formState: options?.formState, | ||
| }); | ||
| } catch { | ||
| // fallback to render an empty shell and run pure CSR on browser, | ||
| // which can replay server component error and trigger error boundary. | ||
| status = 500; | ||
| htmlStream = await renderToReadableStream( | ||
| <html> | ||
| <body> | ||
| <noscript>Internal Server Error: SSR failed</noscript> | ||
| </body> | ||
| </html>, | ||
| { | ||
| bootstrapScriptContent: | ||
| `self.__NO_HYDRATE=1;` + (options?.debugNoJS ? "" : bootstrapScriptContent), | ||
| nonce: options?.nonce, | ||
| } | ||
| ); | ||
| } | ||
| let responseStream: ReadableStream<Uint8Array> = htmlStream; | ||
| if (!options?.debugNoJS) { | ||
| // Initial RSC stream is injected in HTML stream as <script>...FLIGHT_DATA...</script> | ||
| // using utility made by devongovett https://github.com/devongovett/rsc-html-stream | ||
| responseStream = responseStream.pipeThrough( | ||
| injectRSCPayload(rscStream2, { | ||
| nonce: options?.nonce, | ||
| }) | ||
| ); | ||
| } | ||
| return { stream: responseStream, status }; | ||
| } | ||
| ``` | ||
| The SSR entry handles the rendering pipeline. It loads the RSC entry module, duplicates the RSC stream (one for SSR, one for hydration), deserializes the stream back to React VDOM, and renders it to HTML. The RSC payload is injected into the HTML for client hydration. | ||
| ## 2. Root Server Component | ||
| ```tsx [app/root.tsx] | ||
| import "./index.css"; // css import is automatically injected in exported server components | ||
| import viteLogo from "./assets/vite.svg"; | ||
| import { getServerCounter, updateServerCounter } from "./action.tsx"; | ||
| import reactLogo from "./assets/react.svg"; | ||
| import nitroLogo from "./assets/nitro.svg"; | ||
| import { ClientCounter } from "./client.tsx"; | ||
| export function Root(props: { url: URL }) { | ||
| return ( | ||
| <html lang="en"> | ||
| <head> | ||
| {/* eslint-disable-next-line unicorn/text-encoding-identifier-case */} | ||
| <meta charSet="UTF-8" /> | ||
| <link rel="icon" type="image/svg+xml" href="/vite.svg" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <title>Nitro + Vite + RSC</title> | ||
| </head> | ||
| <body> | ||
| <App {...props} /> | ||
| </body> | ||
| </html> | ||
| ); | ||
| } | ||
| function App(props: { url: URL }) { | ||
| return ( | ||
| <div id="root"> | ||
| <div> | ||
| <a href="https://vite.dev" target="_blank"> | ||
| <img src={viteLogo} className="logo" alt="Vite logo" /> | ||
| </a> | ||
| <a href="https://react.dev/reference/rsc/server-components" target="_blank"> | ||
| <img src={reactLogo} className="logo react" alt="React logo" /> | ||
| </a> | ||
| <a href="https://nitro.build" target="_blank"> | ||
| <img src={nitroLogo} className="logo" alt="Nitro logo" /> | ||
| </a> | ||
| </div> | ||
| <h1>Vite + RSC + Nitro</h1> | ||
| <div className="card"> | ||
| <ClientCounter /> | ||
| </div> | ||
| <div className="card"> | ||
| <form action={updateServerCounter.bind(null, 1)}> | ||
| <button>Server Counter: {getServerCounter()}</button> | ||
| </form> | ||
| </div> | ||
| <div className="card">Request URL: {props.url?.href}</div> | ||
| <ul className="read-the-docs"> | ||
| <li> | ||
| Edit <code>src/client.tsx</code> to test client HMR. | ||
| </li> | ||
| <li> | ||
| Edit <code>src/root.tsx</code> to test server HMR. | ||
| </li> | ||
| <li> | ||
| Visit{" "} | ||
| <a href="./_.rsc" target="_blank"> | ||
| <code>_.rsc</code> | ||
| </a>{" "} | ||
| to view RSC stream payload. | ||
| </li> | ||
| <li> | ||
| Visit{" "} | ||
| <a href="?__nojs" target="_blank"> | ||
| <code>?__nojs</code> | ||
| </a>{" "} | ||
| to test server action without js enabled. | ||
| </li> | ||
| </ul> | ||
| </div> | ||
| ); | ||
| } | ||
| ``` | ||
| Server components run only on the server. They can import CSS directly, use server-side data, and call server actions. The `ClientCounter` component is imported but runs on the client because it has the `"use client"` directive. | ||
| ## 3. Client Component | ||
| ```tsx [app/client.tsx] | ||
| "use client"; | ||
| import React from "react"; | ||
| export function ClientCounter() { | ||
| const [count, setCount] = React.useState(0); | ||
| return <button onClick={() => setCount((count) => count + 1)}>Client Counter: {count}</button>; | ||
| } | ||
| ``` | ||
| The `"use client"` directive marks this as a client component. It hydrates on the browser and handles interactive state. Server components can import and render client components, but client components cannot import server components. | ||
| ## Learn More | ||
| - [React Server Components](https://react.dev/reference/rsc/server-components) |
| # Vite SSR HTML | ||
| > Server-side rendering with vanilla HTML, Vite, and Nitro. | ||
| <code-tree> | ||
| ```html [index.html] | ||
| <!doctype html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <title>Nitro Quotes</title> | ||
| <style> | ||
| @import "tailwindcss"; | ||
| </style> | ||
| </head> | ||
| <body | ||
| class="min-h-screen flex items-center justify-center p-5 bg-gradient-to-br from-indigo-500 to-purple-600 font-sans" | ||
| > | ||
| <div class="max-w-xl w-full text-center text-white"> | ||
| <div class="bg-white/10 backdrop-blur-md rounded-2xl p-10 shadow-xl border border-white/20"> | ||
| <div | ||
| id="quote" | ||
| class="text-[clamp(1.2rem,4vw,1.8rem)] leading-relaxed mb-5 font-light opacity-70 transition-opacity duration-500" | ||
| > | ||
| <!--ssr-outlet--> | ||
| </div> | ||
| <div | ||
| id="author" | ||
| class="text-[clamp(1rem,3vw,1.2rem)] opacity-0 font-normal transition-opacity duration-500" | ||
| ></div> | ||
| <button | ||
| id="refresh-btn" | ||
| class="mt-5 bg-white/20 border border-white/30 text-white px-6 py-3 rounded-full cursor-pointer text-sm transition hover:bg-white/30 hover:-translate-y-0.5" | ||
| onclick="fetchQuote()" | ||
| > | ||
| New Quote | ||
| </button> | ||
| </div> | ||
| <div class="mt-8 text-sm opacity-60"> | ||
| Powered by | ||
| <a | ||
| class="text-white no-underline border-b border-white/30 hover:border-white transition-colors" | ||
| href="https://vitejs.dev/" | ||
| >Vite</a | ||
| > | ||
| and | ||
| <a | ||
| class="text-white no-underline border-b border-white/30 hover:border-white transition-colors" | ||
| href="https://github.com/nitrojs/nitro" | ||
| >Nitro v3</a | ||
| >. | ||
| </div> | ||
| </div> | ||
| <script> | ||
| const quoteElement = document.getElementById("quote"); | ||
| const authorElement = document.getElementById("author"); | ||
| const refreshBtn = document.getElementById("refresh-btn"); | ||
| const baseQuoteClasses = | ||
| "text-[clamp(1.2rem,4vw,1.8rem)] leading-relaxed mb-5 font-light transition-opacity duration-500"; | ||
| const loadingQuoteClasses = baseQuoteClasses + " opacity-70"; | ||
| const normalQuoteClasses = baseQuoteClasses + " opacity-100"; | ||
| const errorQuoteClasses = baseQuoteClasses + " text-red-400 opacity-100 text-sm"; | ||
| const baseAuthorClasses = | ||
| "text-[clamp(1rem,3vw,1.2rem)] font-normal transition-opacity duration-500"; | ||
| const hiddenAuthorClasses = baseAuthorClasses + " opacity-0"; | ||
| const visibleAuthorClasses = baseAuthorClasses + " opacity-80"; | ||
| async function fetchQuote() { | ||
| try { | ||
| quoteElement.textContent = "Loading..."; | ||
| quoteElement.className = loadingQuoteClasses; | ||
| authorElement.textContent = ""; | ||
| authorElement.className = hiddenAuthorClasses; | ||
| refreshBtn.style.display = "none"; | ||
| const response = await fetch("/quote"); | ||
| if (!response.ok) { | ||
| throw new Error(`HTTP error! status: ${response.status}`); | ||
| } | ||
| const { text, author } = await response.json(); | ||
| quoteElement.textContent = `"${text}"`; | ||
| quoteElement.className = normalQuoteClasses; | ||
| authorElement.textContent = `— ${author}`; | ||
| authorElement.className = visibleAuthorClasses; | ||
| } catch (error) { | ||
| console.error("Error fetching quote:", error); | ||
| quoteElement.textContent = "Failed to load quote. Please try again."; | ||
| quoteElement.className = errorQuoteClasses; | ||
| authorElement.textContent = ""; | ||
| authorElement.className = hiddenAuthorClasses; | ||
| } finally { | ||
| refreshBtn.style.display = "inline-block"; | ||
| } | ||
| } | ||
| </script> | ||
| </body> | ||
| </html> | ||
| ``` | ||
| ```json [package.json] | ||
| { | ||
| "type": "module", | ||
| "scripts": { | ||
| "build": "vite build", | ||
| "dev": "vite dev", | ||
| "preview": "vite preview" | ||
| }, | ||
| "devDependencies": { | ||
| "@tailwindcss/vite": "^4.1.18", | ||
| "nitro": "latest", | ||
| "tailwindcss": "^4.1.18", | ||
| "vite": "beta" | ||
| } | ||
| } | ||
| ``` | ||
| ```json [tsconfig.json] | ||
| { | ||
| "extends": "nitro/tsconfig" | ||
| } | ||
| ``` | ||
| ```ts [vite.config.ts] | ||
| import { defineConfig } from "vite"; | ||
| import { nitro } from "nitro/vite"; | ||
| import tailwindcss from "@tailwindcss/vite"; | ||
| export default defineConfig({ | ||
| plugins: [ | ||
| nitro({ | ||
| serverDir: "./", | ||
| }), | ||
| tailwindcss(), | ||
| ], | ||
| }); | ||
| ``` | ||
| ```ts [app/entry-server.ts] | ||
| import { fetch } from "nitro"; | ||
| export default { | ||
| async fetch() { | ||
| const quote = (await fetch("/quote").then((res) => res.json())) as { | ||
| text: string; | ||
| }; | ||
| return tokenizedStream(quote.text, 50); | ||
| }, | ||
| }; | ||
| function tokenizedStream(text: string, delay: number): ReadableStream<Uint8Array> { | ||
| const tokens = text.split(" "); | ||
| return new ReadableStream({ | ||
| start(controller) { | ||
| let index = 0; | ||
| function push() { | ||
| if (index < tokens.length) { | ||
| const word = tokens[index++] + (index < tokens.length ? " " : ""); | ||
| controller.enqueue(new TextEncoder().encode(word)); | ||
| setTimeout(push, delay); | ||
| } else { | ||
| controller.close(); | ||
| } | ||
| } | ||
| push(); | ||
| }, | ||
| }); | ||
| } | ||
| ``` | ||
| ```ts [routes/quote.ts] | ||
| const QUOTES_URL = | ||
| "https://github.com/JamesFT/Database-Quotes-JSON/raw/refs/heads/master/quotes.json"; | ||
| let _quotes: Promise<unknown> | undefined; | ||
| function getQuotes() { | ||
| return (_quotes ??= fetch(QUOTES_URL).then((res) => res.json())) as Promise< | ||
| { quoteText: string; quoteAuthor: string }[] | ||
| >; | ||
| } | ||
| export default async function quotesHandler() { | ||
| const quotes = await getQuotes(); | ||
| const randomQuote = quotes[Math.floor(Math.random() * quotes.length)]; | ||
| return Response.json({ | ||
| text: randomQuote.quoteText, | ||
| author: randomQuote.quoteAuthor, | ||
| }); | ||
| } | ||
| ``` | ||
| </code-tree> | ||
| This example renders an HTML template with server-side data and streams the response word by word. It demonstrates how to use Nitro's Vite SSR integration without a framework. | ||
| ## Overview | ||
| 1. **Add the Nitro Vite plugin** to enable SSR | ||
| 2. **Create an HTML template** with a `<!--ssr-outlet-->` comment where server content goes | ||
| 3. **Create a server entry** that fetches data and returns a stream | ||
| 4. **Add API routes** for server-side data | ||
| ## How It Works | ||
| The `index.html` file contains an `<!--ssr-outlet-->` comment that marks where server-rendered content will be inserted. Nitro replaces this comment with the output from your server entry. | ||
| The server entry exports an object with a `fetch` method. It calls the `/quote` API route using Nitro's internal fetch, then returns a `ReadableStream` that emits the quote text word by word with a 50ms delay between each word. | ||
| The quote route fetches a JSON file of quotes from GitHub, caches the result, and returns a random quote. The server entry calls this route to get content for the page. | ||
| ## Learn More | ||
| - [Renderer](/docs/renderer) | ||
| - [Server Entry](/docs/server-entry) |
| # SSR with Preact | ||
| > Server-side rendering with Preact in Nitro using Vite. | ||
| <code-tree> | ||
| ```json [package.json] | ||
| { | ||
| "type": "module", | ||
| "scripts": { | ||
| "build": "vite build", | ||
| "preview": "vite preview", | ||
| "dev": "vite dev" | ||
| }, | ||
| "devDependencies": { | ||
| "@preact/preset-vite": "^2.10.3", | ||
| "@tailwindcss/vite": "^4.1.18", | ||
| "nitro": "latest", | ||
| "preact": "^10.28.3", | ||
| "preact-render-to-string": "^6.6.5", | ||
| "tailwindcss": "^4.1.18", | ||
| "vite": "beta" | ||
| } | ||
| } | ||
| ``` | ||
| ```json [tsconfig.json] | ||
| { | ||
| "extends": "nitro/tsconfig", | ||
| "compilerOptions": { | ||
| "jsx": "react-jsx", | ||
| "jsxImportSource": "preact" | ||
| } | ||
| } | ||
| ``` | ||
| ```js [vite.config.mjs] | ||
| import { defineConfig } from "vite"; | ||
| import { nitro } from "nitro/vite"; | ||
| import preact from "@preact/preset-vite"; | ||
| export default defineConfig({ | ||
| plugins: [nitro(), preact()], | ||
| environments: { | ||
| client: { | ||
| build: { | ||
| rollupOptions: { | ||
| input: "./src/entry-client.tsx", | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }); | ||
| ``` | ||
| ```tsx [src/app.tsx] | ||
| import { useState } from "preact/hooks"; | ||
| export function App() { | ||
| const [count, setCount] = useState(0); | ||
| return <button onClick={() => setCount((c) => c + 1)}>Count is {count}</button>; | ||
| } | ||
| ``` | ||
| ```tsx [src/entry-client.tsx] | ||
| import { hydrate } from "preact"; | ||
| import { App } from "./app.tsx"; | ||
| function main() { | ||
| hydrate(<App />, document.querySelector("#app")!); | ||
| } | ||
| main(); | ||
| ``` | ||
| ```tsx [src/entry-server.tsx] | ||
| import "./styles.css"; | ||
| import { renderToReadableStream } from "preact-render-to-string/stream"; | ||
| import { App } from "./app.jsx"; | ||
| import clientAssets from "./entry-client?assets=client"; | ||
| import serverAssets from "./entry-server?assets=ssr"; | ||
| export default { | ||
| async fetch(request: Request) { | ||
| const url = new URL(request.url); | ||
| const htmlStream = renderToReadableStream(<Root url={url} />); | ||
| return new Response(htmlStream, { | ||
| headers: { "Content-Type": "text/html;charset=utf-8" }, | ||
| }); | ||
| }, | ||
| }; | ||
| function Root(props: { url: URL }) { | ||
| const assets = clientAssets.merge(serverAssets); | ||
| return ( | ||
| <html lang="en"> | ||
| <head> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| {assets.css.map((attr: any) => ( | ||
| <link key={attr.href} rel="stylesheet" {...attr} /> | ||
| ))} | ||
| {assets.js.map((attr: any) => ( | ||
| <link key={attr.href} type="modulepreload" {...attr} /> | ||
| ))} | ||
| <script type="module" src={assets.entry} /> | ||
| </head> | ||
| <body> | ||
| <h1 className="hero">Nitro + Vite + Preact</h1> | ||
| <p>URL: {props.url.href}</p> | ||
| <div id="app"> | ||
| <App /> | ||
| </div> | ||
| </body> | ||
| </html> | ||
| ); | ||
| } | ||
| ``` | ||
| ```css [src/styles.css] | ||
| .hero { | ||
| color: orange; | ||
| } | ||
| button { | ||
| background-color: lightskyblue; | ||
| } | ||
| ``` | ||
| </code-tree> | ||
| Set up server-side rendering (SSR) with Preact, Vite, and Nitro. This setup enables streaming HTML responses, automatic asset management, and client hydration. | ||
| ## Overview | ||
| 1. Add the Nitro Vite plugin to your Vite config | ||
| 2. Configure client and server entry points | ||
| 3. Create a server entry that renders your app to HTML | ||
| 4. Create a client entry that hydrates the server-rendered HTML | ||
| ## 1. Configure Vite | ||
| Add the Nitro and Preact plugins to your Vite config. Define the `client` environment with your client entry point: | ||
| ```js [vite.config.mjs] | ||
| import { defineConfig } from "vite"; | ||
| import { nitro } from "nitro/vite"; | ||
| import preact from "@preact/preset-vite"; | ||
| export default defineConfig({ | ||
| plugins: [nitro(), preact()], | ||
| environments: { | ||
| client: { | ||
| build: { | ||
| rollupOptions: { | ||
| input: "./src/entry-client.tsx", | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }); | ||
| ``` | ||
| The `environments.client` configuration tells Vite which file to use as the browser entry point. Nitro automatically detects the server entry from files named `entry-server` or `server` in common directories. | ||
| ## 2. Create the App Component | ||
| Create a shared Preact component that runs on both server and client: | ||
| ```tsx [src/app.tsx] | ||
| import { useState } from "preact/hooks"; | ||
| export function App() { | ||
| const [count, setCount] = useState(0); | ||
| return <button onClick={() => setCount((c) => c + 1)}>Count is {count}</button>; | ||
| } | ||
| ``` | ||
| ## 3. Create the Server Entry | ||
| The server entry renders your Preact app to a streaming HTML response using `preact-render-to-string/stream`: | ||
| ```tsx [src/entry-server.tsx] | ||
| import "./styles.css"; | ||
| import { renderToReadableStream } from "preact-render-to-string/stream"; | ||
| import { App } from "./app.jsx"; | ||
| import clientAssets from "./entry-client?assets=client"; | ||
| import serverAssets from "./entry-server?assets=ssr"; | ||
| export default { | ||
| async fetch(request: Request) { | ||
| const url = new URL(request.url); | ||
| const htmlStream = renderToReadableStream(<Root url={url} />); | ||
| return new Response(htmlStream, { | ||
| headers: { "Content-Type": "text/html;charset=utf-8" }, | ||
| }); | ||
| }, | ||
| }; | ||
| function Root(props: { url: URL }) { | ||
| const assets = clientAssets.merge(serverAssets); | ||
| return ( | ||
| <html lang="en"> | ||
| <head> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| {assets.css.map((attr: any) => ( | ||
| <link key={attr.href} rel="stylesheet" {...attr} /> | ||
| ))} | ||
| {assets.js.map((attr: any) => ( | ||
| <link key={attr.href} type="modulepreload" {...attr} /> | ||
| ))} | ||
| <script type="module" src={assets.entry} /> | ||
| </head> | ||
| <body> | ||
| <h1 className="hero">Nitro + Vite + Preact</h1> | ||
| <p>URL: {props.url.href}</p> | ||
| <div id="app"> | ||
| <App /> | ||
| </div> | ||
| </body> | ||
| </html> | ||
| ); | ||
| } | ||
| ``` | ||
| Import assets using the `?assets=client` and `?assets=ssr` query parameters. Nitro collects CSS and JS assets from each entry point, and `merge()` combines them into a single manifest. The `assets` object provides arrays of stylesheet and script attributes, plus the client entry URL. Use `renderToReadableStream` to stream HTML as Preact renders, improving time-to-first-byte. | ||
| ## 4. Create the Client Entry | ||
| The client entry hydrates the server-rendered HTML, attaching Preact's event handlers: | ||
| ```tsx [src/entry-client.tsx] | ||
| import { hydrate } from "preact"; | ||
| import { App } from "./app.tsx"; | ||
| function main() { | ||
| hydrate(<App />, document.querySelector("#app")!); | ||
| } | ||
| main(); | ||
| ``` | ||
| The `hydrate` function attaches Preact to the existing server-rendered DOM inside `#app` without re-rendering it. | ||
| ## Learn More | ||
| - [Renderer](/docs/renderer) | ||
| - [Server Entry](/docs/server-entry) |
| # SSR with React | ||
| > Server-side rendering with React in Nitro using Vite. | ||
| <code-tree> | ||
| ```json [package.json] | ||
| { | ||
| "type": "module", | ||
| "scripts": { | ||
| "build": "vite build", | ||
| "preview": "vite preview", | ||
| "dev": "vite dev" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/react": "^19.2.13", | ||
| "@types/react-dom": "^19.2.3", | ||
| "@vitejs/plugin-react": "^5.1.3", | ||
| "nitro": "latest", | ||
| "react": "^19.2.4", | ||
| "react-dom": "^19.2.4", | ||
| "react-refresh": "^0.18.0", | ||
| "vite": "beta" | ||
| } | ||
| } | ||
| ``` | ||
| ```json [tsconfig.json] | ||
| { | ||
| "extends": "nitro/tsconfig", | ||
| "compilerOptions": { | ||
| "jsx": "react-jsx", | ||
| "jsxImportSource": "react" | ||
| } | ||
| } | ||
| ``` | ||
| ```js [vite.config.mjs] | ||
| import { defineConfig } from "vite"; | ||
| import { nitro } from "nitro/vite"; | ||
| import react from "@vitejs/plugin-react"; | ||
| export default defineConfig({ | ||
| plugins: [nitro(), react()], | ||
| environments: { | ||
| client: { | ||
| build: { rollupOptions: { input: "./src/entry-client.tsx" } }, | ||
| }, | ||
| }, | ||
| }); | ||
| ``` | ||
| ```tsx [src/app.tsx] | ||
| import { useState } from "react"; | ||
| export function App() { | ||
| const [count, setCount] = useState(0); | ||
| return ( | ||
| <> | ||
| <h1 className="hero">Nitro + Vite + React</h1> | ||
| <button onClick={() => setCount((c) => c + 1)}>Count is {count}</button> | ||
| </> | ||
| ); | ||
| } | ||
| ``` | ||
| ```tsx [src/entry-client.tsx] | ||
| import "@vitejs/plugin-react/preamble"; | ||
| import { hydrateRoot } from "react-dom/client"; | ||
| import { App } from "./app.tsx"; | ||
| hydrateRoot(document.querySelector("#app")!, <App />); | ||
| ``` | ||
| ```tsx [src/entry-server.tsx] | ||
| import "./styles.css"; | ||
| import { renderToReadableStream } from "react-dom/server.edge"; | ||
| import { App } from "./app.tsx"; | ||
| import clientAssets from "./entry-client?assets=client"; | ||
| import serverAssets from "./entry-server?assets=ssr"; | ||
| export default { | ||
| async fetch(_req: Request) { | ||
| const assets = clientAssets.merge(serverAssets); | ||
| return new Response( | ||
| await renderToReadableStream( | ||
| <html lang="en"> | ||
| <head> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| {assets.css.map((attr: any) => ( | ||
| <link key={attr.href} rel="stylesheet" {...attr} /> | ||
| ))} | ||
| {assets.js.map((attr: any) => ( | ||
| <link key={attr.href} type="modulepreload" {...attr} /> | ||
| ))} | ||
| <script type="module" src={assets.entry} /> | ||
| </head> | ||
| <body id="app"> | ||
| <App /> | ||
| </body> | ||
| </html> | ||
| ), | ||
| { headers: { "Content-Type": "text/html;charset=utf-8" } } | ||
| ); | ||
| }, | ||
| }; | ||
| ``` | ||
| ```css [src/styles.css] | ||
| .hero { | ||
| color: orange; | ||
| } | ||
| button { | ||
| background-color: lightskyblue; | ||
| } | ||
| ``` | ||
| </code-tree> | ||
| Set up server-side rendering (SSR) with React, Vite, and Nitro. This setup enables streaming HTML responses, automatic asset management, and client hydration. | ||
| ## Overview | ||
| 1. Add the Nitro Vite plugin to your Vite config | ||
| 2. Configure client and server entry points | ||
| 3. Create a server entry that renders your app to HTML | ||
| 4. Create a client entry that hydrates the server-rendered HTML | ||
| ## 1. Configure Vite | ||
| Add the Nitro and React plugins to your Vite config. Define the `client` environment with your client entry point: | ||
| ```js [vite.config.mjs] | ||
| import { defineConfig } from "vite"; | ||
| import { nitro } from "nitro/vite"; | ||
| import react from "@vitejs/plugin-react"; | ||
| export default defineConfig({ | ||
| plugins: [nitro(), react()], | ||
| environments: { | ||
| client: { | ||
| build: { rollupOptions: { input: "./src/entry-client.tsx" } }, | ||
| }, | ||
| }, | ||
| }); | ||
| ``` | ||
| The `environments.client` configuration tells Vite which file to use as the browser entry point. Nitro automatically detects the server entry from files named `entry-server` or `server` in common directories. | ||
| ## 2. Create the App Component | ||
| Create a shared React component that runs on both server and client: | ||
| ```tsx [src/app.tsx] | ||
| import { useState } from "react"; | ||
| export function App() { | ||
| const [count, setCount] = useState(0); | ||
| return ( | ||
| <> | ||
| <h1 className="hero">Nitro + Vite + React</h1> | ||
| <button onClick={() => setCount((c) => c + 1)}>Count is {count}</button> | ||
| </> | ||
| ); | ||
| } | ||
| ``` | ||
| ## 3. Create the Server Entry | ||
| The server entry renders your React app to a streaming HTML response. It uses `react-dom/server.edge` for edge-compatible streaming: | ||
| ```tsx [src/entry-server.tsx] | ||
| import "./styles.css"; | ||
| import { renderToReadableStream } from "react-dom/server.edge"; | ||
| import { App } from "./app.tsx"; | ||
| import clientAssets from "./entry-client?assets=client"; | ||
| import serverAssets from "./entry-server?assets=ssr"; | ||
| export default { | ||
| async fetch(_req: Request) { | ||
| const assets = clientAssets.merge(serverAssets); | ||
| return new Response( | ||
| await renderToReadableStream( | ||
| <html lang="en"> | ||
| <head> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| {assets.css.map((attr: any) => ( | ||
| <link key={attr.href} rel="stylesheet" {...attr} /> | ||
| ))} | ||
| {assets.js.map((attr: any) => ( | ||
| <link key={attr.href} type="modulepreload" {...attr} /> | ||
| ))} | ||
| <script type="module" src={assets.entry} /> | ||
| </head> | ||
| <body id="app"> | ||
| <App /> | ||
| </body> | ||
| </html> | ||
| ), | ||
| { headers: { "Content-Type": "text/html;charset=utf-8" } } | ||
| ); | ||
| }, | ||
| }; | ||
| ``` | ||
| Import assets using the `?assets=client` and `?assets=ssr` query parameters. Nitro collects CSS and JS assets from each entry point, and `merge()` combines them into a single manifest. The `assets` object provides arrays of stylesheet and script attributes, plus the client entry URL. Use `renderToReadableStream` to stream HTML as React renders, improving time-to-first-byte. | ||
| ## 4. Create the Client Entry | ||
| The client entry hydrates the server-rendered HTML, attaching React's event handlers: | ||
| ```tsx [src/entry-client.tsx] | ||
| import "@vitejs/plugin-react/preamble"; | ||
| import { hydrateRoot } from "react-dom/client"; | ||
| import { App } from "./app.tsx"; | ||
| hydrateRoot(document.querySelector("#app")!, <App />); | ||
| ``` | ||
| The `@vitejs/plugin-react/preamble` import is required for React Fast Refresh during development. The `hydrateRoot` function attaches React to the existing server-rendered DOM without re-rendering it. | ||
| ## Learn More | ||
| - [Renderer](/docs/renderer) | ||
| - [Server Entry](/docs/server-entry) |
| # SSR with SolidJS | ||
| > Server-side rendering with SolidJS in Nitro using Vite. | ||
| <code-tree> | ||
| ```json [package.json] | ||
| { | ||
| "type": "module", | ||
| "scripts": { | ||
| "build": "vite build", | ||
| "dev": "vite dev" | ||
| }, | ||
| "devDependencies": { | ||
| "nitro": "latest", | ||
| "solid-js": "^1.9.11", | ||
| "vite": "beta", | ||
| "vite-plugin-solid": "^2.11.10" | ||
| } | ||
| } | ||
| ``` | ||
| ```json [tsconfig.json] | ||
| { | ||
| "extends": "nitro/tsconfig", | ||
| "compilerOptions": { | ||
| "jsx": "preserve", | ||
| "jsxImportSource": "solid-js" | ||
| } | ||
| } | ||
| ``` | ||
| ```js [vite.config.mjs] | ||
| import solid from "vite-plugin-solid"; | ||
| import { defineConfig } from "vite"; | ||
| import { nitro } from "nitro/vite"; | ||
| export default defineConfig({ | ||
| plugins: [solid({ ssr: true }), nitro()], | ||
| esbuild: { jsx: "preserve", jsxImportSource: "solid-js" }, | ||
| environments: { | ||
| ssr: { | ||
| build: { rollupOptions: { input: "./src/entry-server.tsx" } }, | ||
| }, | ||
| client: { | ||
| build: { rollupOptions: { input: "./src/entry-client.tsx" } }, | ||
| }, | ||
| }, | ||
| }); | ||
| ``` | ||
| ```tsx [src/app.tsx] | ||
| import { createSignal } from "solid-js"; | ||
| export function App() { | ||
| const [count, setCount] = createSignal(0); | ||
| return ( | ||
| <div> | ||
| <h1>Hello, Solid!</h1> | ||
| <button onClick={() => setCount((count) => count + 1)}>Count: {count()}</button> | ||
| </div> | ||
| ); | ||
| } | ||
| ``` | ||
| ```tsx [src/entry-client.tsx] | ||
| import { hydrate } from "solid-js/web"; | ||
| import "./styles.css"; | ||
| import { App } from "./app.jsx"; | ||
| hydrate(() => <App />, document.querySelector("#app")!); | ||
| ``` | ||
| ```tsx [src/entry-server.tsx] | ||
| import { renderToStringAsync, HydrationScript } from "solid-js/web"; | ||
| import { App } from "./app.jsx"; | ||
| import clientAssets from "./entry-client?assets=client"; | ||
| import serverAssets from "./entry-server?assets=ssr"; | ||
| export default { | ||
| async fetch(req: Request): Promise<Response> { | ||
| const appHTML = await renderToStringAsync(() => <App />); | ||
| const rootHTML = await renderToStringAsync(() => <Root appHTML={appHTML} />); | ||
| return new Response(rootHTML, { | ||
| headers: { "Content-Type": "text/html" }, | ||
| }); | ||
| }, | ||
| }; | ||
| function Root(props: { appHTML?: string }) { | ||
| const assets = clientAssets.merge(serverAssets); | ||
| return ( | ||
| <html lang="en"> | ||
| <head> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| {assets.css.map((attr: any) => ( | ||
| <link key={attr.href} rel="stylesheet" {...attr} /> | ||
| ))} | ||
| {assets.js.map((attr: any) => ( | ||
| <link key={attr.href} type="modulepreload" {...attr} /> | ||
| ))} | ||
| </head> | ||
| <body> | ||
| <div id="app" innerHTML={props.appHTML || ""} /> | ||
| <HydrationScript /> | ||
| <script type="module" src={assets.entry} /> | ||
| </body> | ||
| </html> | ||
| ); | ||
| } | ||
| ``` | ||
| ```css [src/styles.css] | ||
| div { | ||
| font-family: system-ui, Arial, sans-serif; | ||
| font-size: 20px; | ||
| margin-bottom: 10px; | ||
| } | ||
| button { | ||
| background-color: rgb(147 197 253); | ||
| color: rgb(15 23 42); | ||
| border: none; | ||
| padding: 10px 20px; | ||
| font-size: 16px; | ||
| cursor: pointer; | ||
| border-radius: 5px; | ||
| } | ||
| button:hover { | ||
| background-color: rgb(191 219 254); | ||
| } | ||
| ``` | ||
| </code-tree> | ||
| Set up server-side rendering (SSR) with SolidJS, Vite, and Nitro. This setup uses `renderToStringAsync` for HTML generation and supports client hydration. | ||
| ## Overview | ||
| 1. Add the Nitro Vite plugin to your Vite config | ||
| 2. Configure client and server entry points | ||
| 3. Create a server entry that renders your app to HTML | ||
| 4. Create a client entry that hydrates the server-rendered HTML | ||
| ## 1. Configure Vite | ||
| Add the Nitro and SolidJS plugins to your Vite config. SolidJS requires explicit JSX configuration and both `ssr` and `client` environments: | ||
| ```js [vite.config.mjs] | ||
| import solid from "vite-plugin-solid"; | ||
| import { defineConfig } from "vite"; | ||
| import { nitro } from "nitro/vite"; | ||
| export default defineConfig({ | ||
| plugins: [solid({ ssr: true }), nitro()], | ||
| esbuild: { jsx: "preserve", jsxImportSource: "solid-js" }, | ||
| environments: { | ||
| ssr: { | ||
| build: { rollupOptions: { input: "./src/entry-server.tsx" } }, | ||
| }, | ||
| client: { | ||
| build: { rollupOptions: { input: "./src/entry-client.tsx" } }, | ||
| }, | ||
| }, | ||
| }); | ||
| ``` | ||
| Enable SSR mode in the Solid plugin with `solid({ ssr: true })`. Configure esbuild to preserve JSX for Solid's compiler and use Solid's JSX runtime. SolidJS requires explicit `ssr` and `client` environment configuration in Vite. | ||
| ## 2. Create the App Component | ||
| Create a shared SolidJS component using reactive signals: | ||
| ```tsx [src/app.tsx] | ||
| import { createSignal } from "solid-js"; | ||
| export function App() { | ||
| const [count, setCount] = createSignal(0); | ||
| return ( | ||
| <div> | ||
| <h1>Hello, Solid!</h1> | ||
| <button onClick={() => setCount((count) => count + 1)}>Count: {count()}</button> | ||
| </div> | ||
| ); | ||
| } | ||
| ``` | ||
| SolidJS uses signals (`createSignal`) for state management. Unlike React's `useState`, signals are getter functions that you call to read the value. | ||
| ## 3. Create the Server Entry | ||
| The server entry renders your SolidJS app to HTML using `renderToStringAsync` and includes the `HydrationScript` for client-side hydration: | ||
| ```tsx [src/entry-server.tsx] | ||
| import { renderToStringAsync, HydrationScript } from "solid-js/web"; | ||
| import { App } from "./app.jsx"; | ||
| import clientAssets from "./entry-client?assets=client"; | ||
| import serverAssets from "./entry-server?assets=ssr"; | ||
| export default { | ||
| async fetch(req: Request): Promise<Response> { | ||
| const appHTML = await renderToStringAsync(() => <App />); | ||
| const rootHTML = await renderToStringAsync(() => <Root appHTML={appHTML} />); | ||
| return new Response(rootHTML, { | ||
| headers: { "Content-Type": "text/html" }, | ||
| }); | ||
| }, | ||
| }; | ||
| function Root(props: { appHTML?: string }) { | ||
| const assets = clientAssets.merge(serverAssets); | ||
| return ( | ||
| <html lang="en"> | ||
| <head> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| {assets.css.map((attr: any) => ( | ||
| <link key={attr.href} rel="stylesheet" {...attr} /> | ||
| ))} | ||
| {assets.js.map((attr: any) => ( | ||
| <link key={attr.href} type="modulepreload" {...attr} /> | ||
| ))} | ||
| </head> | ||
| <body> | ||
| <div id="app" innerHTML={props.appHTML || ""} /> | ||
| <HydrationScript /> | ||
| <script type="module" src={assets.entry} /> | ||
| </body> | ||
| </html> | ||
| ); | ||
| } | ||
| ``` | ||
| SolidJS requires rendering the app separately from the shell (two-phase rendering). The app HTML is injected via `innerHTML` to preserve hydration markers. Include the `HydrationScript` component to inject the script Solid needs to rehydrate on the client. Import assets using the `?assets=client` and `?assets=ssr` query parameters to collect CSS and JS from each entry point. | ||
| ## 4. Create the Client Entry | ||
| The client entry hydrates the server-rendered HTML, restoring Solid's reactivity: | ||
| ```tsx [src/entry-client.tsx] | ||
| import { hydrate } from "solid-js/web"; | ||
| import "./styles.css"; | ||
| import { App } from "./app.jsx"; | ||
| hydrate(() => <App />, document.querySelector("#app")!); | ||
| ``` | ||
| The `hydrate` function attaches Solid's reactive system to the existing server-rendered DOM inside `#app`. The component is wrapped in a function `() => <App />` as required by Solid's API. | ||
| ## Learn More | ||
| - [SolidJS Documentation](https://docs.solidjs.com/) | ||
| - [Renderer](/docs/renderer) | ||
| - [Server Entry](/docs/server-entry) |
| # SSR with TanStack Router | ||
| > Client-side routing with TanStack Router in Nitro using Vite. | ||
| <code-tree> | ||
| ```html [index.html] | ||
| <!doctype html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <title>Nitro + TanStack Router + React</title> | ||
| </head> | ||
| <body> | ||
| <div id="root"></div> | ||
| <script type="module" src="/src/main.tsx"></script> | ||
| </body> | ||
| </html> | ||
| ``` | ||
| ```json [package.json] | ||
| { | ||
| "type": "module", | ||
| "scripts": { | ||
| "build": "vite build", | ||
| "dev": "vite dev", | ||
| "preview": "vite preview" | ||
| }, | ||
| "devDependencies": { | ||
| "@tanstack/react-router": "^1.158.1", | ||
| "@tanstack/react-router-devtools": "^1.158.1", | ||
| "@tanstack/router-plugin": "^1.158.1", | ||
| "@types/react": "^19.2.13", | ||
| "@types/react-dom": "^19.2.3", | ||
| "@vitejs/plugin-react": "^5.1.3", | ||
| "nitro": "latest", | ||
| "react": "^19.2.4", | ||
| "react-dom": "^19.2.4", | ||
| "vite": "beta" | ||
| } | ||
| } | ||
| ``` | ||
| ```json [tsconfig.json] | ||
| { | ||
| "extends": "nitro/tsconfig", | ||
| "compilerOptions": { | ||
| "baseUrl": ".", | ||
| "jsx": "react-jsx", | ||
| "paths": { | ||
| "@/*": ["sec/*"] | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
| ```js [vite.config.mjs] | ||
| import { defineConfig } from "vite"; | ||
| import { nitro } from "nitro/vite"; | ||
| import react from "@vitejs/plugin-react"; | ||
| import { tanstackRouter } from "@tanstack/router-plugin/vite"; | ||
| export default defineConfig({ | ||
| plugins: [tanstackRouter({ target: "react", autoCodeSplitting: true }), react(), nitro()], | ||
| }); | ||
| ``` | ||
| ```tsx [src/main.tsx] | ||
| import { StrictMode } from "react"; | ||
| import ReactDOM from "react-dom/client"; | ||
| import { RouterProvider, createRouter } from "@tanstack/react-router"; | ||
| // Import the generated route tree | ||
| import { routeTree } from "./routeTree.gen.ts"; | ||
| // Create a new router instance | ||
| const router = createRouter({ routeTree }); | ||
| // Register the router instance for type safety | ||
| declare module "@tanstack/react-router" { | ||
| interface Register { | ||
| router: typeof router; | ||
| } | ||
| } | ||
| // Render the app | ||
| const rootElement = document.querySelector("#root")!; | ||
| if (!rootElement.innerHTML) { | ||
| const root = ReactDOM.createRoot(rootElement); | ||
| root.render( | ||
| <StrictMode> | ||
| <RouterProvider router={router} /> | ||
| </StrictMode> | ||
| ); | ||
| } | ||
| ``` | ||
| ```ts [src/routeTree.gen.ts] | ||
| /* eslint-disable */ | ||
| // @ts-nocheck | ||
| // noinspection JSUnusedGlobalSymbols | ||
| // This file was automatically generated by TanStack Router. | ||
| // You should NOT make any changes in this file as it will be overwritten. | ||
| // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. | ||
| import { Route as rootRouteImport } from './routes/__root' | ||
| import { Route as IndexRouteImport } from './routes/index' | ||
| const IndexRoute = IndexRouteImport.update({ | ||
| id: '/', | ||
| path: '/', | ||
| getParentRoute: () => rootRouteImport, | ||
| } as any) | ||
| export interface FileRoutesByFullPath { | ||
| '/': typeof IndexRoute | ||
| } | ||
| export interface FileRoutesByTo { | ||
| '/': typeof IndexRoute | ||
| } | ||
| export interface FileRoutesById { | ||
| __root__: typeof rootRouteImport | ||
| '/': typeof IndexRoute | ||
| } | ||
| export interface FileRouteTypes { | ||
| fileRoutesByFullPath: FileRoutesByFullPath | ||
| fullPaths: '/' | ||
| fileRoutesByTo: FileRoutesByTo | ||
| to: '/' | ||
| id: '__root__' | '/' | ||
| fileRoutesById: FileRoutesById | ||
| } | ||
| export interface RootRouteChildren { | ||
| IndexRoute: typeof IndexRoute | ||
| } | ||
| declare module '@tanstack/react-router' { | ||
| interface FileRoutesByPath { | ||
| '/': { | ||
| id: '/' | ||
| path: '/' | ||
| fullPath: '/' | ||
| preLoaderRoute: typeof IndexRouteImport | ||
| parentRoute: typeof rootRouteImport | ||
| } | ||
| } | ||
| } | ||
| const rootRouteChildren: RootRouteChildren = { | ||
| IndexRoute: IndexRoute, | ||
| } | ||
| export const routeTree = rootRouteImport | ||
| ._addFileChildren(rootRouteChildren) | ||
| ._addFileTypes<FileRouteTypes>() | ||
| ``` | ||
| ```css [src/assets/main.css] | ||
| :root { | ||
| font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; | ||
| line-height: 1.5; | ||
| font-weight: 400; | ||
| color-scheme: light dark; | ||
| color: rgba(255, 255, 255, 0.87); | ||
| background-color: #242424; | ||
| font-synthesis: none; | ||
| text-rendering: optimizeLegibility; | ||
| -webkit-font-smoothing: antialiased; | ||
| -moz-osx-font-smoothing: grayscale; | ||
| } | ||
| a { | ||
| font-weight: 500; | ||
| color: #ff2056; | ||
| text-decoration: inherit; | ||
| } | ||
| a:hover { | ||
| color: #ff637e; | ||
| } | ||
| body { | ||
| margin: 0; | ||
| display: flex; | ||
| flex-direction: column; | ||
| place-items: center; | ||
| justify-content: center; | ||
| min-width: 320px; | ||
| min-height: 100vh; | ||
| } | ||
| h1 { | ||
| font-size: 3.2em; | ||
| line-height: 1.1; | ||
| } | ||
| #app { | ||
| max-width: 1280px; | ||
| margin: 0 auto; | ||
| padding: 2rem; | ||
| text-align: center; | ||
| } | ||
| .logo { | ||
| height: 6em; | ||
| padding: 1.5em; | ||
| will-change: filter; | ||
| transition: filter 300ms; | ||
| transition: transform 300ms; | ||
| } | ||
| .logo:hover { | ||
| transform: scale(1.1); | ||
| } | ||
| .card { | ||
| padding: 2em; | ||
| } | ||
| .read-the-docs { | ||
| color: #888; | ||
| } | ||
| button { | ||
| border-radius: 8px; | ||
| border: 1px solid transparent; | ||
| padding: 0.6em 1.2em; | ||
| font-size: 1em; | ||
| font-weight: 500; | ||
| font-family: inherit; | ||
| background-color: #1a1a1a; | ||
| cursor: pointer; | ||
| transition: border-color 0.25s; | ||
| } | ||
| button:hover { | ||
| border-color: #646cff; | ||
| } | ||
| button:focus, | ||
| button:focus-visible { | ||
| outline: 4px auto -webkit-focus-ring-color; | ||
| } | ||
| @media (prefers-color-scheme: light) { | ||
| :root { | ||
| color: #213547; | ||
| background-color: #ffffff; | ||
| } | ||
| a:hover { | ||
| color: #747bff; | ||
| } | ||
| button { | ||
| background-color: #f9f9f9; | ||
| } | ||
| } | ||
| ``` | ||
| ```tsx [src/routes/__root.tsx] | ||
| import { createRootRoute, Link, Outlet } from "@tanstack/react-router"; | ||
| import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; | ||
| const RootLayout = () => ( | ||
| <> | ||
| <div className="p-2 flex gap-2"> | ||
| <Link to="/" className="[&.active]:font-bold"> | ||
| Home | ||
| </Link> | ||
| </div> | ||
| <hr /> | ||
| <Outlet /> | ||
| <TanStackRouterDevtools /> | ||
| </> | ||
| ); | ||
| export const Route = createRootRoute({ component: RootLayout }); | ||
| ``` | ||
| ```tsx [src/routes/index.tsx] | ||
| import { createFileRoute } from "@tanstack/react-router"; | ||
| export const Route = createFileRoute("/")({ | ||
| loader: async () => { | ||
| const r = await fetch("/api/hello"); | ||
| return r.json(); | ||
| }, | ||
| component: Index, | ||
| }); | ||
| function Index() { | ||
| const r = Route.useLoaderData(); | ||
| return ( | ||
| <div className="p-2"> | ||
| <h3>{JSON.stringify(r)}</h3> | ||
| </div> | ||
| ); | ||
| } | ||
| ``` | ||
| </code-tree> | ||
| Set up TanStack Router with React, Vite, and Nitro. This setup provides file-based routing with type-safe navigation and automatic code splitting. | ||
| ## Overview | ||
| 1. Add the Nitro Vite plugin to your Vite config | ||
| 2. Create an HTML template with your app entry | ||
| 3. Create a main entry that initializes the router | ||
| 4. Define routes using file-based routing | ||
| ## 1. Configure Vite | ||
| Add the Nitro, React, and TanStack Router plugins to your Vite config: | ||
| ```js [vite.config.mjs] | ||
| import { defineConfig } from "vite"; | ||
| import { nitro } from "nitro/vite"; | ||
| import react from "@vitejs/plugin-react"; | ||
| import { tanstackRouter } from "@tanstack/router-plugin/vite"; | ||
| export default defineConfig({ | ||
| plugins: [tanstackRouter({ target: "react", autoCodeSplitting: true }), react(), nitro()], | ||
| }); | ||
| ``` | ||
| The `tanstackRouter` plugin generates a route tree from your `routes/` directory structure. Enable `autoCodeSplitting` to automatically split routes into separate chunks. Place the TanStack Router plugin before the React plugin in the array. | ||
| ## 2. Create the HTML Template | ||
| Create an HTML file that serves as your app shell: | ||
| ```html [index.html] | ||
| <!doctype html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <title>Nitro + TanStack Router + React</title> | ||
| </head> | ||
| <body> | ||
| <div id="root"></div> | ||
| <script type="module" src="/src/main.tsx"></script> | ||
| </body> | ||
| </html> | ||
| ``` | ||
| ## 3. Create the App Entry | ||
| Create the main entry that initializes TanStack Router: | ||
| ```tsx [src/main.tsx] | ||
| import { StrictMode } from "react"; | ||
| import ReactDOM from "react-dom/client"; | ||
| import { RouterProvider, createRouter } from "@tanstack/react-router"; | ||
| // Import the generated route tree | ||
| import { routeTree } from "./routeTree.gen.ts"; | ||
| // Create a new router instance | ||
| const router = createRouter({ routeTree }); | ||
| // Register the router instance for type safety | ||
| declare module "@tanstack/react-router" { | ||
| interface Register { | ||
| router: typeof router; | ||
| } | ||
| } | ||
| // Render the app | ||
| const rootElement = document.querySelector("#root")!; | ||
| if (!rootElement.innerHTML) { | ||
| const root = ReactDOM.createRoot(rootElement); | ||
| root.render( | ||
| <StrictMode> | ||
| <RouterProvider router={router} /> | ||
| </StrictMode> | ||
| ); | ||
| } | ||
| ``` | ||
| The `routeTree.gen.ts` file is auto-generated from your `routes/` directory structure. The `Register` interface declaration provides full type inference for route paths and params. The `!rootElement.innerHTML` check prevents re-rendering during hot module replacement. | ||
| ## 4. Create the Root Route | ||
| The root route (`__root.tsx`) defines your app's layout: | ||
| ```tsx [src/routes/__root.tsx] | ||
| import { createRootRoute, Link, Outlet } from "@tanstack/react-router"; | ||
| import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; | ||
| const RootLayout = () => ( | ||
| <> | ||
| <div className="p-2 flex gap-2"> | ||
| <Link to="/" className="[&.active]:font-bold"> | ||
| Home | ||
| </Link> | ||
| </div> | ||
| <hr /> | ||
| <Outlet /> | ||
| <TanStackRouterDevtools /> | ||
| </> | ||
| ); | ||
| export const Route = createRootRoute({ component: RootLayout }); | ||
| ``` | ||
| Use `Link` for type-safe navigation with active state styling. The `Outlet` component renders child routes. Include `TanStackRouterDevtools` for development tools (automatically removed in production). | ||
| ## 5. Create Page Routes | ||
| Page routes use `createFileRoute` and can include loaders: | ||
| ```tsx [src/routes/index.tsx] | ||
| import { createFileRoute } from "@tanstack/react-router"; | ||
| export const Route = createFileRoute("/")({ | ||
| loader: async () => { | ||
| const r = await fetch("/api/hello"); | ||
| return r.json(); | ||
| }, | ||
| component: Index, | ||
| }); | ||
| function Index() { | ||
| const r = Route.useLoaderData(); | ||
| return ( | ||
| <div className="p-2"> | ||
| <h3>{JSON.stringify(r)}</h3> | ||
| </div> | ||
| ); | ||
| } | ||
| ``` | ||
| Fetch data before rendering with the `loader` function—data is available via `Route.useLoaderData()`. File paths determine URL paths: `routes/index.tsx` maps to `/`, `routes/about.tsx` to `/about`, and `routes/users/$id.tsx` to `/users/:id`. | ||
| ## Learn More | ||
| - [TanStack Router Documentation](https://tanstack.com/router) | ||
| - [Renderer](/docs/renderer) |
| # SSR with TanStack Start | ||
| > Full-stack React with TanStack Start in Nitro using Vite. | ||
| <code-tree> | ||
| ```json [package.json] | ||
| { | ||
| "type": "module", | ||
| "scripts": { | ||
| "build": "vite build", | ||
| "dev": "vite dev", | ||
| "start": "node .output/server/index.mjs" | ||
| }, | ||
| "dependencies": { | ||
| "@tanstack/react-router": "^1.158.1", | ||
| "@tanstack/react-router-devtools": "^1.158.1", | ||
| "@tanstack/react-start": "^1.158.3", | ||
| "nitro": "latest", | ||
| "react": "^19.2.4", | ||
| "react-dom": "^19.2.4", | ||
| "tailwind-merge": "^3.4.0", | ||
| "zod": "^4.3.6" | ||
| }, | ||
| "devDependencies": { | ||
| "@tailwindcss/vite": "^4.1.18", | ||
| "@types/node": "latest", | ||
| "@types/react": "^19.2.13", | ||
| "@types/react-dom": "^19.2.3", | ||
| "@vitejs/plugin-react": "^5.1.3", | ||
| "tailwindcss": "^4.1.18", | ||
| "typescript": "^5.9.3", | ||
| "vite": "beta", | ||
| "vite-tsconfig-paths": "^6.0.5" | ||
| } | ||
| } | ||
| ``` | ||
| ```ts [server.ts] | ||
| import handler, { createServerEntry } from "@tanstack/react-start/server-entry"; | ||
| export default createServerEntry({ | ||
| fetch(request) { | ||
| return handler.fetch(request); | ||
| }, | ||
| }); | ||
| ``` | ||
| ```json [tsconfig.json] | ||
| { | ||
| "extends": "nitro/tsconfig", | ||
| "compilerOptions": { | ||
| "baseUrl": ".", | ||
| "jsx": "react-jsx", | ||
| "paths": { | ||
| "~/*": ["./src/*"] | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
| ```js [vite.config.mjs] | ||
| import { defineConfig } from "vite"; | ||
| import { nitro } from "nitro/vite"; | ||
| import { tanstackStart } from "@tanstack/react-start/plugin/vite"; | ||
| import viteReact from "@vitejs/plugin-react"; | ||
| import viteTsConfigPaths from "vite-tsconfig-paths"; | ||
| import tailwindcss from "@tailwindcss/vite"; | ||
| export default defineConfig({ | ||
| plugins: [ | ||
| viteTsConfigPaths({ projects: ["./tsconfig.json"] }), | ||
| tanstackStart(), | ||
| viteReact(), | ||
| tailwindcss(), | ||
| nitro(), | ||
| ], | ||
| environments: { | ||
| ssr: { build: { rollupOptions: { input: "./server.ts" } } }, | ||
| }, | ||
| }); | ||
| ``` | ||
| ```tsx [src/router.tsx] | ||
| import { createRouter } from "@tanstack/react-router"; | ||
| import { routeTree } from "./routeTree.gen.ts"; | ||
| export function getRouter() { | ||
| const router = createRouter({ | ||
| routeTree, | ||
| defaultPreload: "intent", | ||
| defaultErrorComponent: () => <div>Internal Server Error</div>, | ||
| defaultNotFoundComponent: () => <div>Not Found</div>, | ||
| scrollRestoration: true, | ||
| }); | ||
| return router; | ||
| } | ||
| ``` | ||
| ```ts [src/routeTree.gen.ts] | ||
| /* eslint-disable */ | ||
| // @ts-nocheck | ||
| // noinspection JSUnusedGlobalSymbols | ||
| // This file was automatically generated by TanStack Router. | ||
| // You should NOT make any changes in this file as it will be overwritten. | ||
| // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. | ||
| import { Route as rootRouteImport } from './routes/__root' | ||
| import { Route as IndexRouteImport } from './routes/index' | ||
| import { Route as ApiTestRouteImport } from './routes/api/test' | ||
| const IndexRoute = IndexRouteImport.update({ | ||
| id: '/', | ||
| path: '/', | ||
| getParentRoute: () => rootRouteImport, | ||
| } as any) | ||
| const ApiTestRoute = ApiTestRouteImport.update({ | ||
| id: '/api/test', | ||
| path: '/api/test', | ||
| getParentRoute: () => rootRouteImport, | ||
| } as any) | ||
| export interface FileRoutesByFullPath { | ||
| '/': typeof IndexRoute | ||
| '/api/test': typeof ApiTestRoute | ||
| } | ||
| export interface FileRoutesByTo { | ||
| '/': typeof IndexRoute | ||
| '/api/test': typeof ApiTestRoute | ||
| } | ||
| export interface FileRoutesById { | ||
| __root__: typeof rootRouteImport | ||
| '/': typeof IndexRoute | ||
| '/api/test': typeof ApiTestRoute | ||
| } | ||
| export interface FileRouteTypes { | ||
| fileRoutesByFullPath: FileRoutesByFullPath | ||
| fullPaths: '/' | '/api/test' | ||
| fileRoutesByTo: FileRoutesByTo | ||
| to: '/' | '/api/test' | ||
| id: '__root__' | '/' | '/api/test' | ||
| fileRoutesById: FileRoutesById | ||
| } | ||
| export interface RootRouteChildren { | ||
| IndexRoute: typeof IndexRoute | ||
| ApiTestRoute: typeof ApiTestRoute | ||
| } | ||
| declare module '@tanstack/react-router' { | ||
| interface FileRoutesByPath { | ||
| '/': { | ||
| id: '/' | ||
| path: '/' | ||
| fullPath: '/' | ||
| preLoaderRoute: typeof IndexRouteImport | ||
| parentRoute: typeof rootRouteImport | ||
| } | ||
| '/api/test': { | ||
| id: '/api/test' | ||
| path: '/api/test' | ||
| fullPath: '/api/test' | ||
| preLoaderRoute: typeof ApiTestRouteImport | ||
| parentRoute: typeof rootRouteImport | ||
| } | ||
| } | ||
| } | ||
| const rootRouteChildren: RootRouteChildren = { | ||
| IndexRoute: IndexRoute, | ||
| ApiTestRoute: ApiTestRoute, | ||
| } | ||
| export const routeTree = rootRouteImport | ||
| ._addFileChildren(rootRouteChildren) | ||
| ._addFileTypes<FileRouteTypes>() | ||
| import type { getRouter } from './router.tsx' | ||
| import type { createStart } from '@tanstack/react-start' | ||
| declare module '@tanstack/react-start' { | ||
| interface Register { | ||
| ssr: true | ||
| router: Awaited<ReturnType<typeof getRouter>> | ||
| } | ||
| } | ||
| ``` | ||
| ```tsx [src/routes/__root.tsx] | ||
| /// <reference types="vite/client" /> | ||
| import { HeadContent, Link, Scripts, createRootRoute } from "@tanstack/react-router"; | ||
| import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; | ||
| import * as React from "react"; | ||
| import appCss from "~/styles/app.css?url"; | ||
| export const Route = createRootRoute({ | ||
| head: () => ({ | ||
| meta: [ | ||
| { charSet: "utf8" }, | ||
| { name: "viewport", content: "width=device-width, initial-scale=1" }, | ||
| ], | ||
| links: [{ rel: "stylesheet", href: appCss }], | ||
| scripts: [{ src: "/customScript.js", type: "text/javascript" }], | ||
| }), | ||
| errorComponent: () => <h1>500: Internal Server Error</h1>, | ||
| notFoundComponent: () => <h1>404: Page Not Found</h1>, | ||
| shellComponent: RootDocument, | ||
| }); | ||
| function RootDocument({ children }: { children: React.ReactNode }) { | ||
| return ( | ||
| <html> | ||
| <head> | ||
| <HeadContent /> | ||
| </head> | ||
| <body> | ||
| <div className="p-2 flex gap-2 text-lg"> | ||
| <Link to="/" activeProps={{ className: "font-bold" }} activeOptions={{ exact: true }}> | ||
| Home | ||
| </Link>{" "} | ||
| <Link | ||
| // @ts-ignore | ||
| to="/this-route-does-not-exist" | ||
| activeProps={{ className: "font-bold" }} | ||
| > | ||
| 404 | ||
| </Link> | ||
| </div> | ||
| <hr /> | ||
| {children} | ||
| <TanStackRouterDevtools position="bottom-right" /> | ||
| <Scripts /> | ||
| </body> | ||
| </html> | ||
| ); | ||
| } | ||
| ``` | ||
| ```tsx [src/routes/index.tsx] | ||
| import { createFileRoute } from "@tanstack/react-router"; | ||
| export const Route = createFileRoute("/")({ component: Home }); | ||
| function Home() { | ||
| return ( | ||
| <div className="p-2"> | ||
| <h3>Welcome Home!</h3> | ||
| <a href="/api/test">/api/test</a> | ||
| </div> | ||
| ); | ||
| } | ||
| ``` | ||
| ```css [src/styles/app.css] | ||
| @import "tailwindcss"; | ||
| @layer base { | ||
| *, | ||
| ::after, | ||
| ::before, | ||
| ::backdrop, | ||
| ::file-selector-button { | ||
| border-color: var(--color-gray-200, currentcolor); | ||
| } | ||
| } | ||
| @layer base { | ||
| html { | ||
| color-scheme: light dark; | ||
| } | ||
| * { | ||
| @apply border-gray-200 dark:border-gray-800; | ||
| } | ||
| html, | ||
| body { | ||
| @apply text-gray-900 bg-gray-50 dark:bg-gray-950 dark:text-gray-200; | ||
| } | ||
| .using-mouse * { | ||
| outline: none !important; | ||
| } | ||
| } | ||
| ``` | ||
| </code-tree> | ||
| Set up TanStack Start with Nitro for a full-stack React framework experience with server-side rendering, file-based routing, and integrated API routes. | ||
| ## Overview | ||
| 1. Add the Nitro Vite plugin to your Vite config | ||
| 2. Create a server entry using TanStack Start's server handler | ||
| 3. Configure the router with default components | ||
| 4. Define routes and API endpoints using file-based routing | ||
| ## 1. Configure Vite | ||
| Add the Nitro, React, TanStack Start, and Tailwind plugins to your Vite config: | ||
| ```js [vite.config.mjs] | ||
| import { defineConfig } from "vite"; | ||
| import { nitro } from "nitro/vite"; | ||
| import { tanstackStart } from "@tanstack/react-start/plugin/vite"; | ||
| import viteReact from "@vitejs/plugin-react"; | ||
| import viteTsConfigPaths from "vite-tsconfig-paths"; | ||
| import tailwindcss from "@tailwindcss/vite"; | ||
| export default defineConfig({ | ||
| plugins: [ | ||
| viteTsConfigPaths({ projects: ["./tsconfig.json"] }), | ||
| tanstackStart(), | ||
| viteReact(), | ||
| tailwindcss(), | ||
| nitro(), | ||
| ], | ||
| environments: { | ||
| ssr: { build: { rollupOptions: { input: "./server.ts" } } }, | ||
| }, | ||
| }); | ||
| ``` | ||
| The `tanstackStart()` plugin provides full SSR integration with automatic client entry handling. Use `viteTsConfigPaths()` to enable path aliases like `~/` from tsconfig. The `environments.ssr` option points to the server entry file. | ||
| ## 2. Create the Server Entry | ||
| Create a server entry that uses TanStack Start's handler: | ||
| ```ts [server.ts] | ||
| import handler, { createServerEntry } from "@tanstack/react-start/server-entry"; | ||
| export default createServerEntry({ | ||
| fetch(request) { | ||
| return handler.fetch(request); | ||
| }, | ||
| }); | ||
| ``` | ||
| TanStack Start handles SSR automatically. The `createServerEntry` wrapper integrates with Nitro's server entry format, and the `handler.fetch` processes all incoming requests. | ||
| ## 3. Configure the Router | ||
| Create a router factory function with default error and not-found components: | ||
| ```tsx [src/router.tsx] | ||
| import { createRouter } from "@tanstack/react-router"; | ||
| import { routeTree } from "./routeTree.gen.ts"; | ||
| export function getRouter() { | ||
| const router = createRouter({ | ||
| routeTree, | ||
| defaultPreload: "intent", | ||
| defaultErrorComponent: () => <div>Internal Server Error</div>, | ||
| defaultNotFoundComponent: () => <div>Not Found</div>, | ||
| scrollRestoration: true, | ||
| }); | ||
| return router; | ||
| } | ||
| ``` | ||
| The router factory configures preloading behavior, scroll restoration, and default error/not-found components. | ||
| ## 4. Create the Root Route | ||
| The root route defines your HTML shell with head management and scripts: | ||
| ```tsx [src/routes/__root.tsx] | ||
| /// <reference types="vite/client" /> | ||
| import { HeadContent, Link, Scripts, createRootRoute } from "@tanstack/react-router"; | ||
| import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; | ||
| import * as React from "react"; | ||
| import appCss from "~/styles/app.css?url"; | ||
| export const Route = createRootRoute({ | ||
| head: () => ({ | ||
| meta: [ | ||
| { charSet: "utf8" }, | ||
| { name: "viewport", content: "width=device-width, initial-scale=1" }, | ||
| ], | ||
| links: [{ rel: "stylesheet", href: appCss }], | ||
| scripts: [{ src: "/customScript.js", type: "text/javascript" }], | ||
| }), | ||
| errorComponent: () => <h1>500: Internal Server Error</h1>, | ||
| notFoundComponent: () => <h1>404: Page Not Found</h1>, | ||
| shellComponent: RootDocument, | ||
| }); | ||
| function RootDocument({ children }: { children: React.ReactNode }) { | ||
| return ( | ||
| <html> | ||
| <head> | ||
| <HeadContent /> | ||
| </head> | ||
| <body> | ||
| <div className="p-2 flex gap-2 text-lg"> | ||
| <Link to="/" activeProps={{ className: "font-bold" }} activeOptions={{ exact: true }}> | ||
| Home | ||
| </Link>{" "} | ||
| <Link | ||
| // @ts-ignore | ||
| to="/this-route-does-not-exist" | ||
| activeProps={{ className: "font-bold" }} | ||
| > | ||
| 404 | ||
| </Link> | ||
| </div> | ||
| <hr /> | ||
| {children} | ||
| <TanStackRouterDevtools position="bottom-right" /> | ||
| <Scripts /> | ||
| </body> | ||
| </html> | ||
| ); | ||
| } | ||
| ``` | ||
| Define meta tags, stylesheets, and scripts in the `head()` function. The `shellComponent` provides the HTML document shell that wraps all pages. Use `HeadContent` to render the head configuration and `Scripts` to inject the client-side JavaScript for hydration. | ||
| ## 5. Create Page Routes | ||
| Page routes define your application pages: | ||
| ```tsx [src/routes/index.tsx] | ||
| import { createFileRoute } from "@tanstack/react-router"; | ||
| export const Route = createFileRoute("/")({ component: Home }); | ||
| function Home() { | ||
| return ( | ||
| <div className="p-2"> | ||
| <h3>Welcome Home!</h3> | ||
| <a href="/api/test">/api/test</a> | ||
| </div> | ||
| ); | ||
| } | ||
| ``` | ||
| ## API Routes | ||
| TanStack Start supports API routes alongside page routes. Create files in `src/routes/api/` to define server endpoints that Nitro serves automatically. | ||
| ## Learn More | ||
| - [TanStack Start Documentation](https://tanstack.com/start) | ||
| - [Server Entry](/docs/server-entry) |
| # SSR with Vue Router | ||
| > Server-side rendering with Vue Router in Nitro using Vite. | ||
| <code-tree> | ||
| ```json [package.json] | ||
| { | ||
| "type": "module", | ||
| "scripts": { | ||
| "build": "vite build", | ||
| "dev": "vite dev", | ||
| "preview": "vite preview" | ||
| }, | ||
| "devDependencies": { | ||
| "@vitejs/plugin-vue": "^6.0.4", | ||
| "nitro": "latest", | ||
| "unhead": "^2.1.3", | ||
| "vite": "beta", | ||
| "vite-plugin-devtools-json": "^1.0.0", | ||
| "vue": "^3.5.27", | ||
| "vue-router": "^4.6.4" | ||
| } | ||
| } | ||
| ``` | ||
| ```json [tsconfig.json] | ||
| { | ||
| "extends": "nitro/tsconfig" | ||
| } | ||
| ``` | ||
| ```js [vite.config.mjs] | ||
| import vue from "@vitejs/plugin-vue"; | ||
| import { defineConfig } from "vite"; | ||
| import devtoolsJson from "vite-plugin-devtools-json"; | ||
| import { nitro } from "nitro/vite"; | ||
| export default defineConfig((_env) => ({ | ||
| plugins: [patchVueExclude(vue(), /\?assets/), devtoolsJson(), nitro()], | ||
| environments: { | ||
| client: { build: { rollupOptions: { input: "./app/entry-client.ts" } } }, | ||
| ssr: { build: { rollupOptions: { input: "./app/entry-server.ts" } } }, | ||
| }, | ||
| })); | ||
| // Workaround https://github.com/vitejs/vite-plugin-vue/issues/677 | ||
| function patchVueExclude(plugin, exclude) { | ||
| const original = plugin.transform.handler; | ||
| plugin.transform.handler = function (...args) { | ||
| if (exclude.test(args[1])) return; | ||
| return original.call(this, ...args); | ||
| }; | ||
| return plugin; | ||
| } | ||
| ``` | ||
| ```vue [app/app.vue] | ||
| <script setup lang="ts"> | ||
| import { RouterLink, RouterView } from "vue-router"; | ||
| import "./styles.css"; | ||
| </script> | ||
| <template> | ||
| <nav> | ||
| <ul> | ||
| <li> | ||
| <RouterLink to="/" exact-active-class="active">Home</RouterLink> | ||
| </li> | ||
| <li> | ||
| <RouterLink to="/about" active-class="active">About</RouterLink> | ||
| </li> | ||
| </ul> | ||
| </nav> | ||
| <RouterView /> | ||
| </template> | ||
| <style scoped> | ||
| nav { | ||
| background: white; | ||
| box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); | ||
| padding: 1rem; | ||
| } | ||
| nav ul { | ||
| list-style: none; | ||
| margin: 0; | ||
| padding: 0; | ||
| display: flex; | ||
| gap: 2rem; | ||
| max-width: 800px; | ||
| margin: 0 auto; | ||
| } | ||
| nav a { | ||
| color: #666; | ||
| text-decoration: none; | ||
| } | ||
| nav a:hover { | ||
| color: #333; | ||
| } | ||
| nav a.active { | ||
| color: #646cff; | ||
| } | ||
| </style> | ||
| ``` | ||
| ```ts [app/entry-client.ts] | ||
| import { createSSRApp } from "vue"; | ||
| import { RouterView, createRouter, createWebHistory } from "vue-router"; | ||
| import { routes } from "./routes.ts"; | ||
| async function main() { | ||
| const app = createSSRApp(RouterView); | ||
| const router = createRouter({ history: createWebHistory(), routes }); | ||
| app.use(router); | ||
| await router.isReady(); | ||
| app.mount("#root"); | ||
| } | ||
| // eslint-disable-next-line unicorn/prefer-top-level-await | ||
| main(); | ||
| ``` | ||
| ```ts [app/entry-server.ts] | ||
| import { createSSRApp } from "vue"; | ||
| import { renderToString } from "vue/server-renderer"; | ||
| import { RouterView, createMemoryHistory, createRouter } from "vue-router"; | ||
| import { createHead, transformHtmlTemplate } from "unhead/server"; | ||
| import { routes } from "./routes.ts"; | ||
| import clientAssets from "./entry-client.ts?assets=client"; | ||
| async function handler(request: Request): Promise<Response> { | ||
| const app = createSSRApp(RouterView); | ||
| const router = createRouter({ history: createMemoryHistory(), routes }); | ||
| app.use(router); | ||
| const url = new URL(request.url); | ||
| const href = url.href.slice(url.origin.length); | ||
| await router.push(href); | ||
| await router.isReady(); | ||
| const assets = clientAssets.merge( | ||
| ...(await Promise.all( | ||
| router.currentRoute.value.matched | ||
| .map((to) => to.meta.assets) | ||
| .filter(Boolean) | ||
| .map((fn) => (fn as any)().then((m: any) => m.default)) | ||
| )) | ||
| ); | ||
| const head = createHead(); | ||
| head.push({ | ||
| link: [ | ||
| ...assets.css.map((attrs: any) => ({ rel: "stylesheet", ...attrs })), | ||
| ...assets.js.map((attrs: any) => ({ rel: "modulepreload", ...attrs })), | ||
| ], | ||
| script: [{ type: "module", src: clientAssets.entry }], | ||
| }); | ||
| const renderedApp = await renderToString(app); | ||
| const html = await transformHtmlTemplate(head, htmlTemplate(renderedApp)); | ||
| return new Response(html, { | ||
| headers: { "Content-Type": "text/html;charset=utf-8" }, | ||
| }); | ||
| } | ||
| function htmlTemplate(body: string): string { | ||
| return /* html */ `<!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <title>Vue Router Custom Framework</title> | ||
| </head> | ||
| <body> | ||
| <div id="root">${body}</div> | ||
| </body> | ||
| </html>`; | ||
| } | ||
| export default { | ||
| fetch: handler, | ||
| }; | ||
| ``` | ||
| ```ts [app/routes.ts] | ||
| import type { RouteRecordRaw } from "vue-router"; | ||
| export const routes: RouteRecordRaw[] = [ | ||
| { | ||
| path: "/", | ||
| name: "app", | ||
| component: () => import("./app.vue"), | ||
| meta: { | ||
| assets: () => import("./app.vue?assets"), | ||
| }, | ||
| children: [ | ||
| { | ||
| path: "/", | ||
| name: "home", | ||
| component: () => import("./pages/index.vue"), | ||
| meta: { | ||
| assets: () => import("./pages/index.vue?assets"), | ||
| }, | ||
| }, | ||
| { | ||
| path: "/about", | ||
| name: "about", | ||
| component: () => import("./pages/about.vue"), | ||
| meta: { | ||
| assets: () => import("./pages/about.vue?assets"), | ||
| }, | ||
| }, | ||
| { | ||
| path: "/:catchAll(.*)", | ||
| name: "not-found", | ||
| component: () => import("./pages/not-found.vue"), | ||
| meta: { | ||
| assets: () => import("./pages/not-found.vue?assets"), | ||
| }, | ||
| }, | ||
| ], | ||
| }, | ||
| ]; | ||
| ``` | ||
| ```ts [app/shims.d.ts] | ||
| declare module "*.vue" { | ||
| import type { DefineComponent } from "vue"; | ||
| const component: DefineComponent<{}, {}, any>; | ||
| export default component; | ||
| } | ||
| ``` | ||
| ```css [app/styles.css] | ||
| * { | ||
| box-sizing: border-box; | ||
| } | ||
| body { | ||
| margin: 0; | ||
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; | ||
| background: #f5f5f5; | ||
| color: #333; | ||
| } | ||
| main { | ||
| max-width: 800px; | ||
| margin: 0 auto; | ||
| padding: 2rem; | ||
| } | ||
| h1 { | ||
| font-size: 2.5rem; | ||
| margin-bottom: 0.5rem; | ||
| } | ||
| .card { | ||
| background: white; | ||
| border-radius: 8px; | ||
| padding: 2rem; | ||
| box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); | ||
| margin: 2rem 0; | ||
| } | ||
| button { | ||
| background: rgb(83, 91, 242); | ||
| color: white; | ||
| border: none; | ||
| padding: 0.5rem 1rem; | ||
| border-radius: 4px; | ||
| font-size: 1rem; | ||
| cursor: pointer; | ||
| } | ||
| button:hover { | ||
| background: #535bf2; | ||
| } | ||
| .subtitle { | ||
| color: #666; | ||
| font-size: 1.1rem; | ||
| margin-bottom: 2rem; | ||
| } | ||
| ``` | ||
| ```vue [app/pages/about.vue] | ||
| <template> | ||
| <main> | ||
| <h1>About</h1> | ||
| <div class="card"> | ||
| <p>This is a simple Vue Router demo app built with Vite Plugin Fullstack.</p> | ||
| <p>It demonstrates basic routing and server-side rendering.</p> | ||
| </div> | ||
| </main> | ||
| </template> | ||
| ``` | ||
| ```vue [app/pages/index.vue] | ||
| <script setup lang="ts"> | ||
| import { ref } from "vue"; | ||
| const count = ref(0); | ||
| function increment() { | ||
| count.value++; | ||
| } | ||
| </script> | ||
| <template> | ||
| <main> | ||
| <div class="hero"> | ||
| <h1>Vue Router Custom Framework</h1> | ||
| <p class="subtitle">A simple demo app with Vite</p> | ||
| </div> | ||
| <div class="card counter-card"> | ||
| <p>Count: {{ count }}</p> | ||
| <button @click="increment">Increment</button> | ||
| </div> | ||
| </main> | ||
| </template> | ||
| <style scoped> | ||
| .hero { | ||
| text-align: center; | ||
| margin-bottom: 2rem; | ||
| } | ||
| .hero h1 { | ||
| color: rgb(100, 108, 255); | ||
| } | ||
| .counter-card { | ||
| text-align: center; | ||
| } | ||
| .counter-card h2 { | ||
| color: #646cff; | ||
| margin-bottom: 1rem; | ||
| } | ||
| .counter-card p { | ||
| font-size: 1.5rem; | ||
| font-weight: bold; | ||
| margin: 1rem 0; | ||
| } | ||
| </style> | ||
| ``` | ||
| ```vue [app/pages/not-found.vue] | ||
| <template> | ||
| <main> | ||
| <h1>Not Found 404</h1> | ||
| </main> | ||
| </template> | ||
| ``` | ||
| </code-tree> | ||
| Set up server-side rendering (SSR) with Vue, Vue Router, Vite, and Nitro. This setup enables per-route code splitting, head management with unhead, and client hydration. | ||
| ## Overview | ||
| 1. Add the Nitro Vite plugin to your Vite config | ||
| 2. Define routes with lazy-loaded components | ||
| 3. Create a server entry that renders your app with router support | ||
| 4. Create a client entry that hydrates and takes over routing | ||
| 5. Create page components | ||
| ## 1. Configure Vite | ||
| Add the Nitro and Vue plugins to your Vite config. Define both `client` and `ssr` environments: | ||
| ```js [vite.config.mjs] | ||
| import vue from "@vitejs/plugin-vue"; | ||
| import { defineConfig } from "vite"; | ||
| import devtoolsJson from "vite-plugin-devtools-json"; | ||
| import { nitro } from "nitro/vite"; | ||
| export default defineConfig((_env) => ({ | ||
| plugins: [patchVueExclude(vue(), /\?assets/), devtoolsJson(), nitro()], | ||
| environments: { | ||
| client: { build: { rollupOptions: { input: "./app/entry-client.ts" } } }, | ||
| ssr: { build: { rollupOptions: { input: "./app/entry-server.ts" } } }, | ||
| }, | ||
| })); | ||
| // Workaround https://github.com/vitejs/vite-plugin-vue/issues/677 | ||
| function patchVueExclude(plugin, exclude) { | ||
| const original = plugin.transform.handler; | ||
| plugin.transform.handler = function (...args) { | ||
| if (exclude.test(args[1])) return; | ||
| return original.call(this, ...args); | ||
| }; | ||
| return plugin; | ||
| } | ||
| ``` | ||
| The `patchVueExclude` helper prevents the Vue plugin from processing asset imports (files with `?assets` query parameter). | ||
| ## 2. Define Routes | ||
| Create route definitions with lazy-loaded components and asset metadata: | ||
| ```ts [app/routes.ts] | ||
| import type { RouteRecordRaw } from "vue-router"; | ||
| export const routes: RouteRecordRaw[] = [ | ||
| { | ||
| path: "/", | ||
| name: "app", | ||
| component: () => import("./app.vue"), | ||
| meta: { | ||
| assets: () => import("./app.vue?assets"), | ||
| }, | ||
| children: [ | ||
| { | ||
| path: "/", | ||
| name: "home", | ||
| component: () => import("./pages/index.vue"), | ||
| meta: { | ||
| assets: () => import("./pages/index.vue?assets"), | ||
| }, | ||
| }, | ||
| { | ||
| path: "/about", | ||
| name: "about", | ||
| component: () => import("./pages/about.vue"), | ||
| meta: { | ||
| assets: () => import("./pages/about.vue?assets"), | ||
| }, | ||
| }, | ||
| { | ||
| path: "/:catchAll(.*)", | ||
| name: "not-found", | ||
| component: () => import("./pages/not-found.vue"), | ||
| meta: { | ||
| assets: () => import("./pages/not-found.vue?assets"), | ||
| }, | ||
| }, | ||
| ], | ||
| }, | ||
| ]; | ||
| ``` | ||
| Use dynamic imports for lazy-loaded components to enable code splitting. The `meta.assets` function loads route-specific CSS and JS chunks. Define child routes under a root layout component for nested routing. | ||
| ## 3. Create the Server Entry | ||
| The server entry renders your Vue app with router support and head management: | ||
| ```ts [app/entry-server.ts] | ||
| import { createSSRApp } from "vue"; | ||
| import { renderToString } from "vue/server-renderer"; | ||
| import { RouterView, createMemoryHistory, createRouter } from "vue-router"; | ||
| import { createHead, transformHtmlTemplate } from "unhead/server"; | ||
| import { routes } from "./routes.ts"; | ||
| import clientAssets from "./entry-client.ts?assets=client"; | ||
| async function handler(request: Request): Promise<Response> { | ||
| const app = createSSRApp(RouterView); | ||
| const router = createRouter({ history: createMemoryHistory(), routes }); | ||
| app.use(router); | ||
| const url = new URL(request.url); | ||
| const href = url.href.slice(url.origin.length); | ||
| await router.push(href); | ||
| await router.isReady(); | ||
| const assets = clientAssets.merge( | ||
| ...(await Promise.all( | ||
| router.currentRoute.value.matched | ||
| .map((to) => to.meta.assets) | ||
| .filter(Boolean) | ||
| .map((fn) => (fn as any)().then((m: any) => m.default)) | ||
| )) | ||
| ); | ||
| const head = createHead(); | ||
| head.push({ | ||
| link: [ | ||
| ...assets.css.map((attrs: any) => ({ rel: "stylesheet", ...attrs })), | ||
| ...assets.js.map((attrs: any) => ({ rel: "modulepreload", ...attrs })), | ||
| ], | ||
| script: [{ type: "module", src: clientAssets.entry }], | ||
| }); | ||
| const renderedApp = await renderToString(app); | ||
| const html = await transformHtmlTemplate(head, htmlTemplate(renderedApp)); | ||
| return new Response(html, { | ||
| headers: { "Content-Type": "text/html;charset=utf-8" }, | ||
| }); | ||
| } | ||
| function htmlTemplate(body: string): string { | ||
| return /* html */ `<!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <title>Vue Router Custom Framework</title> | ||
| </head> | ||
| <body> | ||
| <div id="root">${body}</div> | ||
| </body> | ||
| </html>`; | ||
| } | ||
| export default { | ||
| fetch: handler, | ||
| }; | ||
| ``` | ||
| The server uses `createMemoryHistory()` since there's no browser URL bar—the router navigates to the requested URL before rendering. Assets are loaded dynamically based on matched routes, ensuring only the CSS and JS needed for the current page are included. The `unhead` library manages `<head>` elements, injecting stylesheets and scripts via `transformHtmlTemplate`. | ||
| ## 4. Create the Client Entry | ||
| The client entry hydrates the server-rendered HTML and takes over routing: | ||
| ```ts [app/entry-client.ts] | ||
| import { createSSRApp } from "vue"; | ||
| import { RouterView, createRouter, createWebHistory } from "vue-router"; | ||
| import { routes } from "./routes.ts"; | ||
| async function main() { | ||
| const app = createSSRApp(RouterView); | ||
| const router = createRouter({ history: createWebHistory(), routes }); | ||
| app.use(router); | ||
| await router.isReady(); | ||
| app.mount("#root"); | ||
| } | ||
| // eslint-disable-next-line unicorn/prefer-top-level-await | ||
| main(); | ||
| ``` | ||
| The client entry creates a Vue app with `createWebHistory()` for browser-based routing. After the router is ready, it mounts to the `#root` element and hydrates the server-rendered HTML. | ||
| ## 5. Create the Root Component | ||
| The root component provides navigation and renders child routes: | ||
| ```vue [app/app.vue] | ||
| <script setup lang="ts"> | ||
| import { RouterLink, RouterView } from "vue-router"; | ||
| import "./styles.css"; | ||
| </script> | ||
| <template> | ||
| <nav> | ||
| <ul> | ||
| <li> | ||
| <RouterLink to="/" exact-active-class="active">Home</RouterLink> | ||
| </li> | ||
| <li> | ||
| <RouterLink to="/about" active-class="active">About</RouterLink> | ||
| </li> | ||
| </ul> | ||
| </nav> | ||
| <RouterView /> | ||
| </template> | ||
| <style scoped> | ||
| nav { | ||
| background: white; | ||
| box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); | ||
| padding: 1rem; | ||
| } | ||
| nav ul { | ||
| list-style: none; | ||
| margin: 0; | ||
| padding: 0; | ||
| display: flex; | ||
| gap: 2rem; | ||
| max-width: 800px; | ||
| margin: 0 auto; | ||
| } | ||
| nav a { | ||
| color: #666; | ||
| text-decoration: none; | ||
| } | ||
| nav a:hover { | ||
| color: #333; | ||
| } | ||
| nav a.active { | ||
| color: #646cff; | ||
| } | ||
| </style> | ||
| ``` | ||
| ## Learn More | ||
| - [Vue Router Documentation](https://router.vuejs.org/) | ||
| - [Unhead Documentation](https://unhead.unjs.io/) | ||
| - [Renderer](/docs/renderer) | ||
| - [Server Entry](/docs/server-entry) |
| # Vite + tRPC | ||
| > End-to-end typesafe APIs with tRPC in Nitro using Vite. | ||
| <code-tree> | ||
| ```html [index.html] | ||
| <!doctype html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <title>tRPC Counter</title> | ||
| <style> | ||
| body { | ||
| font-family: system-ui, sans-serif; | ||
| background: #0f1115; | ||
| color: #e5e7eb; | ||
| display: grid; | ||
| place-items: center; | ||
| height: 100vh; | ||
| margin: 0; | ||
| } | ||
| .box { | ||
| background: #181b22; | ||
| padding: 24px 32px; | ||
| border-radius: 10px; | ||
| text-align: center; | ||
| min-width: 200px; | ||
| } | ||
| button { | ||
| background: #2563eb; | ||
| border: none; | ||
| color: white; | ||
| padding: 8px 14px; | ||
| border-radius: 6px; | ||
| cursor: pointer; | ||
| margin-top: 12px; | ||
| font-size: 14px; | ||
| } | ||
| button:hover { | ||
| background: #1d4ed8; | ||
| } | ||
| .value { | ||
| font-size: 36px; | ||
| margin: 12px 0; | ||
| } | ||
| </style> | ||
| </head> | ||
| <body> | ||
| <div class="box"> | ||
| <div>Counter</div> | ||
| <div class="value" id="value"> | ||
| <script server> | ||
| // Server-side Rendering | ||
| const { result } = await serverFetch("/trpc/get").then(r => r.json()) | ||
| echo(result?.data?.value) | ||
| </script> | ||
| </div> | ||
| <button id="inc">Increment</button> | ||
| </div> | ||
| <script setup> | ||
| const valueEl = document.getElementById("value"); | ||
| const incBtn = document.getElementById("inc"); | ||
| async function call(path, body) { | ||
| const res = await fetch(`/trpc/${path}`, { | ||
| method: body ? "POST" : "GET", | ||
| headers: { "content-type": "application/json" }, | ||
| body: body ? JSON.stringify(body) : undefined, | ||
| }); | ||
| const json = await res.json(); | ||
| return json.result.data; | ||
| } | ||
| async function refresh() { | ||
| const data = await call("get"); | ||
| valueEl.textContent = data.value; | ||
| } | ||
| incBtn.onclick = async () => { | ||
| const data = await call("inc", {}); | ||
| valueEl.textContent = data.value; | ||
| }; | ||
| refresh(); | ||
| </script> | ||
| </body> | ||
| </html> | ||
| ``` | ||
| ```json [package.json] | ||
| { | ||
| "type": "module", | ||
| "scripts": { | ||
| "dev": "vite", | ||
| "build": "vite build", | ||
| "preview": "vite preview" | ||
| }, | ||
| "devDependencies": { | ||
| "@trpc/client": "^11.9.0", | ||
| "@trpc/server": "^11.9.0", | ||
| "nitro": "latest", | ||
| "vite": "beta", | ||
| "zod": "^4.3.6" | ||
| } | ||
| } | ||
| ``` | ||
| ```json [tsconfig.json] | ||
| { | ||
| "extends": "nitro/tsconfig", | ||
| "compilerOptions": {} | ||
| } | ||
| ``` | ||
| ```ts [vite.config.ts] | ||
| import { defineConfig } from "vite"; | ||
| import { nitro } from "nitro/vite"; | ||
| export default defineConfig({ | ||
| plugins: [ | ||
| nitro({ | ||
| routes: { | ||
| "/trpc/**": "./server/trpc.ts", | ||
| }, | ||
| }), | ||
| ], | ||
| }); | ||
| ``` | ||
| ```ts [server/trpc.ts] | ||
| import { initTRPC } from "@trpc/server"; | ||
| import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; | ||
| let counter = 0; | ||
| const t = initTRPC.create(); | ||
| export const appRouter = t.router({ | ||
| get: t.procedure.query(() => { | ||
| return { value: counter }; | ||
| }), | ||
| inc: t.procedure.mutation(() => { | ||
| counter++; | ||
| return { value: counter }; | ||
| }), | ||
| }); | ||
| export type AppRouter = typeof appRouter; | ||
| export default { | ||
| async fetch(request: Request): Promise<Response> { | ||
| return fetchRequestHandler({ | ||
| endpoint: "/trpc", | ||
| req: request, | ||
| router: appRouter, | ||
| }); | ||
| }, | ||
| }; | ||
| ``` | ||
| </code-tree> | ||
| Set up tRPC with Vite and Nitro for end-to-end typesafe APIs without code generation. This example builds a counter with server-side rendering for the initial value and client-side updates. | ||
| ## Overview | ||
| 1. Configure Vite with the Nitro plugin and route tRPC requests | ||
| 2. Create a tRPC router with procedures | ||
| 3. Create an HTML page with server-side rendering and client interactivity | ||
| ## 1. Configure Vite | ||
| Add the Nitro plugin and configure the `/trpc/**` route to point to your tRPC handler: | ||
| ```ts [vite.config.ts] | ||
| import { defineConfig } from "vite"; | ||
| import { nitro } from "nitro/vite"; | ||
| export default defineConfig({ | ||
| plugins: [ | ||
| nitro({ | ||
| routes: { | ||
| "/trpc/**": "./server/trpc.ts", | ||
| }, | ||
| }), | ||
| ], | ||
| }); | ||
| ``` | ||
| The `routes` option maps URL patterns to handler files. All requests to `/trpc/*` are handled by the tRPC router. | ||
| ## 2. Create the tRPC Router | ||
| Define your tRPC router with procedures and export it as a fetch handler: | ||
| ```ts [server/trpc.ts] | ||
| import { initTRPC } from "@trpc/server"; | ||
| import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; | ||
| let counter = 0; | ||
| const t = initTRPC.create(); | ||
| export const appRouter = t.router({ | ||
| get: t.procedure.query(() => { | ||
| return { value: counter }; | ||
| }), | ||
| inc: t.procedure.mutation(() => { | ||
| counter++; | ||
| return { value: counter }; | ||
| }), | ||
| }); | ||
| export type AppRouter = typeof appRouter; | ||
| export default { | ||
| async fetch(request: Request): Promise<Response> { | ||
| return fetchRequestHandler({ | ||
| endpoint: "/trpc", | ||
| req: request, | ||
| router: appRouter, | ||
| }); | ||
| }, | ||
| }; | ||
| ``` | ||
| Define procedures using `t.procedure.query()` for read operations and `t.procedure.mutation()` for write operations. Export the `AppRouter` type so clients get full type inference. The default export uses tRPC's fetch adapter to handle incoming requests. | ||
| ## 3. Create the HTML Page | ||
| Create an HTML page with server-side rendering and client-side interactivity: | ||
| ```html [index.html] | ||
| <!doctype html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <title>tRPC Counter</title> | ||
| <style> | ||
| body { | ||
| font-family: system-ui, sans-serif; | ||
| background: #0f1115; | ||
| color: #e5e7eb; | ||
| display: grid; | ||
| place-items: center; | ||
| height: 100vh; | ||
| margin: 0; | ||
| } | ||
| .box { | ||
| background: #181b22; | ||
| padding: 24px 32px; | ||
| border-radius: 10px; | ||
| text-align: center; | ||
| min-width: 200px; | ||
| } | ||
| button { | ||
| background: #2563eb; | ||
| border: none; | ||
| color: white; | ||
| padding: 8px 14px; | ||
| border-radius: 6px; | ||
| cursor: pointer; | ||
| margin-top: 12px; | ||
| font-size: 14px; | ||
| } | ||
| button:hover { | ||
| background: #1d4ed8; | ||
| } | ||
| .value { | ||
| font-size: 36px; | ||
| margin: 12px 0; | ||
| } | ||
| </style> | ||
| </head> | ||
| <body> | ||
| <div class="box"> | ||
| <div>Counter</div> | ||
| <div class="value" id="value"> | ||
| <script server> | ||
| // Server-side Rendering | ||
| const { result } = await serverFetch("/trpc/get").then(r => r.json()) | ||
| echo(result?.data?.value) | ||
| </script> | ||
| </div> | ||
| <button id="inc">Increment</button> | ||
| </div> | ||
| <script setup> | ||
| const valueEl = document.getElementById("value"); | ||
| const incBtn = document.getElementById("inc"); | ||
| async function call(path, body) { | ||
| const res = await fetch(`/trpc/${path}`, { | ||
| method: body ? "POST" : "GET", | ||
| headers: { "content-type": "application/json" }, | ||
| body: body ? JSON.stringify(body) : undefined, | ||
| }); | ||
| const json = await res.json(); | ||
| return json.result.data; | ||
| } | ||
| async function refresh() { | ||
| const data = await call("get"); | ||
| valueEl.textContent = data.value; | ||
| } | ||
| incBtn.onclick = async () => { | ||
| const data = await call("inc", {}); | ||
| valueEl.textContent = data.value; | ||
| }; | ||
| refresh(); | ||
| </script> | ||
| </body> | ||
| </html> | ||
| ``` | ||
| The `<script server>` block runs on the server before sending the response, fetching the initial counter value via `serverFetch`. The `<script setup>` block runs in the browser and handles the increment button click. | ||
| ## Learn More | ||
| - [tRPC](https://trpc.io/) | ||
| - [Routing](/docs/routing) |
| # WebSocket | ||
| > Real-time bidirectional communication with WebSocket support. | ||
| <code-tree> | ||
| ```html [index.html] | ||
| <html lang="en" data-theme="dark"> | ||
| <head> | ||
| <title>CrossWS Test Page</title> | ||
| <script src="https://cdn.tailwindcss.com"></script> | ||
| <style> | ||
| body { | ||
| background-color: #1a1a1a; | ||
| } | ||
| </style> | ||
| <script type="module"> | ||
| import { createApp, reactive, nextTick } from "https://esm.sh/petite-vue@0.4.1"; | ||
| let ws; | ||
| const store = reactive({ | ||
| message: "", | ||
| messages: [], | ||
| }); | ||
| const scroll = () => { | ||
| nextTick(() => { | ||
| const el = document.querySelector("#messages"); | ||
| el.scrollTop = el.scrollHeight; | ||
| el.scrollTo({ | ||
| top: el.scrollHeight, | ||
| behavior: "smooth", | ||
| }); | ||
| }); | ||
| }; | ||
| const format = async () => { | ||
| for (const message of store.messages) { | ||
| if (!message._fmt && message.text.startsWith("{")) { | ||
| message._fmt = true; | ||
| const { codeToHtml } = await import("https://esm.sh/shiki@1.0.0"); | ||
| const str = JSON.stringify(JSON.parse(message.text), null, 2); | ||
| message.formattedText = await codeToHtml(str, { | ||
| lang: "json", | ||
| theme: "dark-plus", | ||
| }); | ||
| } | ||
| } | ||
| }; | ||
| const log = (user, ...args) => { | ||
| console.log("[ws]", user, ...args); | ||
| store.messages.push({ | ||
| text: args.join(" "), | ||
| formattedText: "", | ||
| user: user, | ||
| date: new Date().toLocaleString(), | ||
| }); | ||
| scroll(); | ||
| format(); | ||
| }; | ||
| const connect = async () => { | ||
| const isSecure = location.protocol === "https:"; | ||
| const url = (isSecure ? "wss://" : "ws://") + location.host + "/_ws"; | ||
| if (ws) { | ||
| log("ws", "Closing previous connection before reconnecting..."); | ||
| ws.close(); | ||
| clear(); | ||
| } | ||
| log("ws", "Connecting to", url, "..."); | ||
| ws = new WebSocket(url); | ||
| ws.addEventListener("message", async (event) => { | ||
| let data = typeof event.data === "string" ? event.data : await event.data.text(); | ||
| const { user = "system", message = "" } = data.startsWith("{") | ||
| ? JSON.parse(data) | ||
| : { message: data }; | ||
| log(user, typeof message === "string" ? message : JSON.stringify(message)); | ||
| }); | ||
| await new Promise((resolve) => ws.addEventListener("open", resolve)); | ||
| log("ws", "Connected!"); | ||
| }; | ||
| const clear = () => { | ||
| store.messages.splice(0, store.messages.length); | ||
| log("system", "previous messages cleared"); | ||
| }; | ||
| const send = () => { | ||
| console.log("sending message..."); | ||
| if (store.message) { | ||
| ws.send(store.message); | ||
| } | ||
| store.message = ""; | ||
| }; | ||
| const ping = () => { | ||
| log("ws", "Sending ping"); | ||
| ws.send("ping"); | ||
| }; | ||
| createApp({ | ||
| store, | ||
| send, | ||
| ping, | ||
| clear, | ||
| connect, | ||
| rand: Math.random(), | ||
| }).mount(); | ||
| await connect(); | ||
| </script> | ||
| </head> | ||
| <body class="h-screen flex flex-col justify-between"> | ||
| <main v-scope="{}"> | ||
| <!-- Messages --> | ||
| <div id="messages" class="flex-grow flex flex-col justify-end px-4 py-8"> | ||
| <div class="flex items-center mb-4" v-for="message in store.messages"> | ||
| <div class="flex flex-col"> | ||
| <p class="text-gray-500 mb-1 text-xs ml-10">{{ message.user }}</p> | ||
| <div class="flex items-center"> | ||
| <img | ||
| :src="'https://www.gravatar.com/avatar/' + encodeURIComponent(message.user + rand) + '?s=512&d=monsterid'" | ||
| alt="Avatar" | ||
| class="w-8 h-8 rounded-full" | ||
| /> | ||
| <div class="ml-2 bg-gray-800 rounded-lg p-2"> | ||
| <p | ||
| v-if="message.formattedText" | ||
| class="overflow-x-scroll" | ||
| v-html="message.formattedText" | ||
| ></p> | ||
| <p v-else class="text-white">{{ message.text }}</p> | ||
| </div> | ||
| </div> | ||
| <p class="text-gray-500 mt-1 text-xs ml-10">{{ message.date }}</p> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| <!-- Chatbox --> | ||
| <div class="bg-gray-800 px-4 py-2 flex items-center justify-between fixed bottom-0 w-full"> | ||
| <div class="w-full min-w-6"> | ||
| <input | ||
| type="text" | ||
| placeholder="Type your message..." | ||
| class="w-full rounded-l-lg px-4 py-2 bg-gray-700 text-white focus:outline-none focus:ring focus:border-blue-300" | ||
| @keydown.enter="send" | ||
| v-model="store.message" | ||
| /> | ||
| </div> | ||
| <div class="flex"> | ||
| <button class="bg-blue-500 hover:bg-blue-600 text-white py-2 px-4" @click="send"> | ||
| Send | ||
| </button> | ||
| <button class="bg-blue-500 hover:bg-blue-600 text-white py-2 px-4" @click="ping"> | ||
| Ping | ||
| </button> | ||
| <button class="bg-blue-500 hover:bg-blue-600 text-white py-2 px-4" @click="connect"> | ||
| Reconnect | ||
| </button> | ||
| <button | ||
| class="bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded-r-lg" | ||
| @click="clear" | ||
| > | ||
| Clear | ||
| </button> | ||
| </div> | ||
| </div> | ||
| </main> | ||
| </body> | ||
| </html> | ||
| ` | ||
| ``` | ||
| ```ts [nitro.config.ts] | ||
| import { defineConfig } from "nitro"; | ||
| export default defineConfig({ | ||
| serverDir: "./", | ||
| renderer: { static: true }, | ||
| features: { websocket: true }, | ||
| }); | ||
| ``` | ||
| ```json [package.json] | ||
| { | ||
| "type": "module", | ||
| "scripts": { | ||
| "dev": "nitro dev", | ||
| "build": "nitro build" | ||
| }, | ||
| "devDependencies": { | ||
| "nitro": "latest" | ||
| } | ||
| } | ||
| ``` | ||
| ```json [tsconfig.json] | ||
| { | ||
| "extends": "nitro/tsconfig" | ||
| } | ||
| ``` | ||
| ```ts [vite.config.ts] | ||
| import { defineConfig } from "vite"; | ||
| import { nitro } from "nitro/vite"; | ||
| export default defineConfig({ plugins: [nitro()] }); | ||
| ``` | ||
| ```ts [routes/_ws.ts] | ||
| import { defineWebSocketHandler } from "nitro"; | ||
| export default defineWebSocketHandler({ | ||
| open(peer) { | ||
| peer.send({ user: "server", message: `Welcome ${peer}!` }); | ||
| peer.publish("chat", { user: "server", message: `${peer} joined!` }); | ||
| peer.subscribe("chat"); | ||
| }, | ||
| message(peer, message) { | ||
| if (message.text().includes("ping")) { | ||
| peer.send({ user: "server", message: "pong" }); | ||
| } else { | ||
| const msg = { | ||
| user: peer.toString(), | ||
| message: message.toString(), | ||
| }; | ||
| peer.send(msg); // echo | ||
| peer.publish("chat", msg); | ||
| } | ||
| }, | ||
| close(peer) { | ||
| peer.publish("chat", { user: "server", message: `${peer} left!` }); | ||
| }, | ||
| }); | ||
| ``` | ||
| </code-tree> | ||
| This example implements a simple chat room using WebSockets. Clients connect, send messages, and receive messages from other users in real-time. The server broadcasts messages to all connected clients using pub/sub channels. | ||
| ## WebSocket Handler | ||
| Create a WebSocket route using `defineWebSocketHandler`. | ||
| ```ts [routes/_ws.ts] | ||
| import { defineWebSocketHandler } from "nitro"; | ||
| export default defineWebSocketHandler({ | ||
| open(peer) { | ||
| peer.send({ user: "server", message: `Welcome ${peer}!` }); | ||
| peer.publish("chat", { user: "server", message: `${peer} joined!` }); | ||
| peer.subscribe("chat"); | ||
| }, | ||
| message(peer, message) { | ||
| if (message.text().includes("ping")) { | ||
| peer.send({ user: "server", message: "pong" }); | ||
| } else { | ||
| const msg = { | ||
| user: peer.toString(), | ||
| message: message.toString(), | ||
| }; | ||
| peer.send(msg); // echo | ||
| peer.publish("chat", msg); | ||
| } | ||
| }, | ||
| close(peer) { | ||
| peer.publish("chat", { user: "server", message: `${peer} left!` }); | ||
| }, | ||
| }); | ||
| ``` | ||
| Different hooks are exposed by `defineWebSocketHandler()` to integrate with different parts of the websocket lifecycle. | ||
| ## Learn More | ||
| - [Routing](/docs/routing) | ||
| - [crossws Documentation](https://crossws.h3.dev/guide/hooks) |
| <u-page-hero> | ||
| <code-group> | ||
| <prose-pre> | ||
| ```ts | ||
| import { defineConfig } from 'vite' | ||
| import { nitro } from 'nitro/vite' | ||
| export default defineConfig({ | ||
| plugins: [nitro()], | ||
| nitro: { | ||
| serverDir: "./server" | ||
| } | ||
| }) | ||
| ``` | ||
| </prose-pre> | ||
| <prose-pre> | ||
| ```ts | ||
| import { defineConfig } from 'nitro' | ||
| export default defineConfig({ | ||
| preset: "node", | ||
| serverDir: "./server", | ||
| routeRules: { | ||
| "/api/**": { cache: true } | ||
| } | ||
| }) | ||
| ``` | ||
| </prose-pre> | ||
| </code-group> | ||
| <hero-background></hero-background> | ||
| Build /Servers | ||
| Nitro extends your Vite application with a production-ready server, compatible with any runtime. Add server routes to your application and deploy many hosting platform with a zero-config experience. | ||
| <app-hero-links></app-hero-links> | ||
| </u-page-hero> | ||
| <hero-features> | ||
| </hero-features> | ||
| <performance-showcase> | ||
| </performance-showcase> | ||
| <landing-features> | ||
| <feature-card> | ||
| File-system routing | ||
| Create server routes in the routes/ folder and they are automatically registered. Or bring your own framework — H3, Hono, Elysia, Express — via a server.ts entry. | ||
| </feature-card> | ||
| <feature-card> | ||
| Deploy everywhere | ||
| The same codebase deploys to Node.js, Cloudflare Workers, Deno, Bun, AWS Lambda, Vercel, Netlify, and more — zero config, no vendor lock-in. | ||
| </feature-card> | ||
| <feature-card> | ||
| Universal storage | ||
| Built-in key-value storage abstraction powered by unstorage. Works with filesystem, Redis, Cloudflare KV, and more — same API everywhere. | ||
| </feature-card> | ||
| <feature-card> | ||
| Built-in caching | ||
| Cache route handlers and arbitrary functions with a simple API. Supports multiple storage backends and stale-while-revalidate patterns. | ||
| </feature-card> | ||
| <feature-card> | ||
| Web standard server | ||
| Go full Web standard and pick the library of your choice. Use H3, Hono, Elysia, Express, or the raw fetch API — Nitro handles the rest. | ||
| </feature-card> | ||
| <feature-card> | ||
| Universal renderer | ||
| Use any frontend framework as your renderer. Nitro provides the server layer while your framework handles the UI. | ||
| </feature-card> | ||
| <feature-card> | ||
| Server plugins | ||
| Extend Nitro's runtime behavior with plugins. Hook into lifecycle events, register custom logic, and auto-load from the plugins/ directory. | ||
| </feature-card> | ||
| <feature-card> | ||
| Built-in database | ||
| Lightweight SQL database layer powered by db0. Pre-configured with SQLite out of the box, with support for PostgreSQL, MySQL, and Cloudflare D1. | ||
| </feature-card> | ||
| <feature-card> | ||
| Static & server assets | ||
| Serve public assets directly to clients or bundle server assets for programmatic access. Works seamlessly across all deployment targets. | ||
| </feature-card> | ||
| </landing-features> | ||
| <page-sponsors> | ||
| </page-sponsors> |
| # Nitro Documentation | ||
| - [Docs](./docs/index.md) | ||
| - [Introduction](./docs/index.md) | ||
| - [Quick Start](./docs/quick-start.md) | ||
| - [Renderer](./docs/renderer.md) | ||
| - [Routing](./docs/routing.md) | ||
| - [Server Entry](./docs/server-entry.md) | ||
| - [Cache](./docs/cache.md) | ||
| - [KV Storage](./docs/storage.md) | ||
| - [Assets](./docs/assets.md) | ||
| - [Configuration](./docs/configuration.md) | ||
| - [Database](./docs/database.md) | ||
| - [Lifecycle](./docs/lifecycle.md) | ||
| - [Plugins](./docs/plugins.md) | ||
| - [Tasks](./docs/tasks.md) | ||
| - [Migration Guide](./docs/migration.md) | ||
| - [Nightly Channel](./docs/nightly.md) | ||
| - [Deploy](./deploy/index.md) | ||
| - [Deploy](./deploy/index.md) | ||
| - [Node.js](./deploy/runtimes/node.md) | ||
| - [Bun](./deploy/runtimes/bun.md) | ||
| - [Deno](./deploy/runtimes/deno.md) | ||
| - [Alwaysdata](./deploy/providers/alwaysdata.md) | ||
| - [AWS Lambda](./deploy/providers/aws.md) | ||
| - [AWS Amplify](./deploy/providers/aws-amplify.md) | ||
| - [Azure](./deploy/providers/azure.md) | ||
| - [Cleavr](./deploy/providers/cleavr.md) | ||
| - [Cloudflare](./deploy/providers/cloudflare.md) | ||
| - [Deno Deploy](./deploy/providers/deno-deploy.md) | ||
| - [DigitalOcean](./deploy/providers/digitalocean.md) | ||
| - [Firebase](./deploy/providers/firebase.md) | ||
| - [Flightcontrol](./deploy/providers/flightcontrol.md) | ||
| - [Genezio](./deploy/providers/genezio.md) | ||
| - [GitHub Pages](./deploy/providers/github-pages.md) | ||
| - [GitLab Pages](./deploy/providers/gitlab-pages.md) | ||
| - [Heroku](./deploy/providers/heroku.md) | ||
| - [IIS](./deploy/providers/iis.md) | ||
| - [Koyeb](./deploy/providers/koyeb.md) | ||
| - [Netlify](./deploy/providers/netlify.md) | ||
| - [Platform.sh](./deploy/providers/platform-sh.md) | ||
| - [Render.com](./deploy/providers/render.md) | ||
| - [StormKit](./deploy/providers/stormkit.md) | ||
| - [Vercel](./deploy/providers/vercel.md) | ||
| - [Zeabur](./deploy/providers/zeabur.md) | ||
| - [Zephyr Cloud](./deploy/providers/zephyr.md) | ||
| - [Zerops](./deploy/providers/zerops.md) | ||
| - [Config](./config/index.md) | ||
| - [Config](./config/index.md) | ||
| - [Examples](./examples/index.md) | ||
| - [Examples](./examples/index.md) | ||
| - [API Routes](./examples/api-routes.md) | ||
| - [Auto Imports](./examples/auto-imports.md) | ||
| - [Cached Handler](./examples/cached-handler.md) | ||
| - [Custom Error Handler](./examples/custom-error-handler.md) | ||
| - [Database](./examples/database.md) | ||
| - [Elysia](./examples/elysia.md) | ||
| - [Express](./examples/express.md) | ||
| - [Fastify](./examples/fastify.md) | ||
| - [Hello World](./examples/hello-world.md) | ||
| - [Hono](./examples/hono.md) | ||
| - [Import Alias](./examples/import-alias.md) | ||
| - [Middleware](./examples/middleware.md) | ||
| - [Mono JSX](./examples/mono-jsx.md) | ||
| - [Nano JSX](./examples/nano-jsx.md) | ||
| - [Plugins](./examples/plugins.md) | ||
| - [Custom Renderer](./examples/renderer.md) | ||
| - [Runtime Config](./examples/runtime-config.md) | ||
| - [Server Fetch](./examples/server-fetch.md) | ||
| - [Shiki](./examples/shiki.md) | ||
| - [Virtual Routes](./examples/virtual-routes.md) | ||
| - [Vite Nitro Plugin](./examples/vite-nitro-plugin.md) | ||
| - [Vite RSC](./examples/vite-rsc.md) | ||
| - [Vite SSR HTML](./examples/vite-ssr-html.md) | ||
| - [SSR with Preact](./examples/vite-ssr-preact.md) | ||
| - [SSR with React](./examples/vite-ssr-react.md) | ||
| - [SSR with SolidJS](./examples/vite-ssr-solid.md) | ||
| - [SSR with TanStack Router](./examples/vite-ssr-tsr-react.md) | ||
| - [SSR with TanStack Start](./examples/vite-ssr-tss-react.md) | ||
| - [SSR with Vue Router](./examples/vite-ssr-vue-router.md) | ||
| - [Vite + tRPC](./examples/vite-trpc.md) | ||
| - [WebSocket](./examples/websocket.md) | ||
| - [Index](./index.md) |
| --- | ||
| name: nitro | ||
| description: Build and deploy universal JavaScript servers with Nitro | ||
| --- | ||
| @docs/TOC.md | ||
| You can use `npx nitro docs [--page <path>] [...args]` to explore the documentation locally. | ||
| For example, `npx nitro docs --page /docs/routing` will open the routing page of the guide section. | ||
| If not available, fallback to https://nitro.build/llms.txt |
+32
-31
@@ -1,12 +0,9 @@ | ||
| import "../_common.mjs"; | ||
| import { _ as writeTypes, at as join, d as libChunkName, f as baseBuildConfig, h as writeBuildInfo, l as NODE_MODULES_RE, n as baseBuildPlugins, st as relative, u as getChunkName } from "./common.mjs"; | ||
| import { i as debounce } from "../_libs/rc9+c12+dotenv.mjs"; | ||
| import { _ as writeTypes, ct as join, d as libChunkName, f as baseBuildConfig, h as writeBuildInfo, it as basename, l as NODE_MODULES_RE, n as baseBuildPlugins, u as getChunkName, ut as relative } from "./common.mjs"; | ||
| import { t as formatCompatibilityDate } from "../_libs/compatx.mjs"; | ||
| import { n as scanHandlers } from "../_chunks/nitro2.mjs"; | ||
| import { n as watch$1 } from "../_libs/readdirp+chokidar.mjs"; | ||
| import { t as generateFSTree } from "../_chunks/utils.mjs"; | ||
| import { n as watch } from "../_libs/readdirp+chokidar.mjs"; | ||
| import { t as debounce } from "../_libs/perfect-debounce.mjs"; | ||
| import { n as generateFSTree } from "../_chunks/utils.mjs"; | ||
| import { builtinModules } from "node:module"; | ||
| import { watch } from "node:fs"; | ||
| import { defu } from "defu"; | ||
| //#region src/build/rolldown/config.ts | ||
@@ -70,6 +67,8 @@ const getRolldownConfig = async (nitro) => { | ||
| const outputConfig = config.output; | ||
| if (outputConfig.inlineDynamicImports || outputConfig.format === "iife") delete outputConfig.codeSplitting; | ||
| if (outputConfig.inlineDynamicImports || outputConfig.format === "iife") { | ||
| delete outputConfig.inlineDynamicImports; | ||
| outputConfig.codeSplitting = false; | ||
| } | ||
| return config; | ||
| }; | ||
| //#endregion | ||
@@ -101,7 +100,11 @@ //#region src/build/rolldown/dev.ts | ||
| ]); | ||
| const scanDirsWatcher = watch$1(scanDirs, { ignoreInitial: true }).on("all", (event) => { | ||
| const scanDirsWatcher = watch(scanDirs, { ignoreInitial: true }).on("all", (event) => { | ||
| if (watchReloadEvents.has(event)) reload(); | ||
| }); | ||
| const rootDirWatcher = watch(nitro.options.rootDir, { persistent: false }, (_event, filename) => { | ||
| if (filename && /^server\.[mc]?[jt]sx?$/.test(filename)) reload(); | ||
| const serverEntryRe = /^server\.[mc]?[jt]sx?$/; | ||
| const rootDirWatcher = watch(nitro.options.rootDir, { | ||
| ignoreInitial: true, | ||
| depth: 0 | ||
| }).on("all", (event, path) => { | ||
| if (watchReloadEvents.has(event) && serverEntryRe.test(basename(path))) reload(); | ||
| }); | ||
@@ -114,27 +117,26 @@ nitro.hooks.hook("close", () => { | ||
| nitro.hooks.hook("rollup:reload", () => reload()); | ||
| nitro.logger.info(`Starting dev watcher (builder: \`rolldown\`, preset: \`${nitro.options.preset}\`, compatibility date: \`${formatCompatibilityDate(nitro.options.compatibilityDate)}\`)`); | ||
| await load(); | ||
| function startWatcher(nitro$1, config$1) { | ||
| const watcher$1 = rolldown.watch(config$1); | ||
| function startWatcher(nitro, config) { | ||
| const watcher = rolldown.watch(config); | ||
| let start; | ||
| watcher$1.on("event", (event) => { | ||
| watcher.on("event", (event) => { | ||
| switch (event.code) { | ||
| case "START": | ||
| start = Date.now(); | ||
| nitro$1.logger.info(`Starting dev watcher (builder: \`rolldown\`, preset: \`${nitro$1.options.preset}\`, compatibility date: \`${formatCompatibilityDate(nitro$1.options.compatibilityDate)}\`)`); | ||
| nitro$1.hooks.callHook("dev:start"); | ||
| nitro.hooks.callHook("dev:start"); | ||
| break; | ||
| case "BUNDLE_END": | ||
| nitro$1.hooks.callHook("compiled", nitro$1); | ||
| if (nitro$1.options.logging.buildSuccess) nitro$1.logger.success(`Server built`, start ? `in ${Date.now() - start}ms` : ""); | ||
| nitro$1.hooks.callHook("dev:reload"); | ||
| nitro.hooks.callHook("compiled", nitro); | ||
| if (nitro.options.logging.buildSuccess) nitro.logger.success(`Server built`, start ? `in ${Date.now() - start}ms` : ""); | ||
| nitro.hooks.callHook("dev:reload"); | ||
| break; | ||
| case "ERROR": | ||
| nitro$1.logger.error(event.error); | ||
| nitro$1.hooks.callHook("dev:error", event.error); | ||
| nitro.logger.error(event.error); | ||
| nitro.hooks.callHook("dev:error", event.error); | ||
| } | ||
| }); | ||
| return watcher$1; | ||
| return watcher; | ||
| } | ||
| } | ||
| //#endregion | ||
@@ -147,7 +149,8 @@ //#region src/build/rolldown/prod.ts | ||
| await writeTypes(nitro); | ||
| let output; | ||
| if (!nitro.options.static) { | ||
| nitro.logger.info(`Building server (builder: \`rolldown\`, preset: \`${nitro.options.preset}\`, compatibility date: \`${formatCompatibilityDate(nitro.options.compatibilityDate)}\`)`); | ||
| await (await rolldown.rolldown(config)).write(config.output); | ||
| output = await (await rolldown.rolldown(config)).write(config.output); | ||
| } | ||
| const buildInfo = await writeBuildInfo(nitro); | ||
| const buildInfo = await writeBuildInfo(nitro, output); | ||
| if (!nitro.options.static) { | ||
@@ -162,6 +165,5 @@ if (nitro.options.logging.buildSuccess) nitro.logger.success(`Server built in ${Date.now() - buildStartTime}ms`); | ||
| }; | ||
| if (buildInfo.commands.preview) nitro.logger.success(`You can preview this build using \`${rewriteRelativePaths(buildInfo.commands.preview)}\``); | ||
| if (buildInfo.commands.deploy) nitro.logger.success(`You can deploy this build using \`${rewriteRelativePaths(buildInfo.commands.deploy)}\``); | ||
| nitro.logger.success("You can preview this build using `npx nitro preview`"); | ||
| if (buildInfo.commands.deploy) nitro.logger.success(rewriteRelativePaths("You can deploy this build using `npx nitro deploy --prebuilt`")); | ||
| } | ||
| //#endregion | ||
@@ -175,4 +177,3 @@ //#region src/build/rolldown/build.ts | ||
| } | ||
| //#endregion | ||
| export { rolldownBuild }; | ||
| export { rolldownBuild }; |
+28
-35
@@ -1,16 +0,13 @@ | ||
| import "../_common.mjs"; | ||
| import { _ as writeTypes, at as join, d as libChunkName, f as baseBuildConfig, h as writeBuildInfo, it as isAbsolute, l as NODE_MODULES_RE, n as baseBuildPlugins, st as relative, t as oxc, u as getChunkName } from "./common.mjs"; | ||
| import { i as debounce } from "../_libs/rc9+c12+dotenv.mjs"; | ||
| import { _ as writeTypes, ct as join, d as libChunkName, f as baseBuildConfig, h as writeBuildInfo, it as basename, l as NODE_MODULES_RE, n as baseBuildPlugins, st as isAbsolute, t as oxc, u as getChunkName, ut as relative } from "./common.mjs"; | ||
| import { t as formatCompatibilityDate } from "../_libs/compatx.mjs"; | ||
| import { n as scanHandlers } from "../_chunks/nitro2.mjs"; | ||
| import { n as watch$1 } from "../_libs/readdirp+chokidar.mjs"; | ||
| import { n as watch } from "../_libs/readdirp+chokidar.mjs"; | ||
| import { t as debounce } from "../_libs/perfect-debounce.mjs"; | ||
| import { t as alias } from "../_libs/plugin-alias.mjs"; | ||
| import { n as inject } from "../_libs/plugin-inject.mjs"; | ||
| import { t as generateFSTree } from "../_chunks/utils.mjs"; | ||
| import { t as inject } from "../_libs/plugin-inject.mjs"; | ||
| import { n as generateFSTree } from "../_chunks/utils.mjs"; | ||
| import { t as commonjs } from "../_libs/commondir+is-reference.mjs"; | ||
| import { t as json } from "../_libs/plugin-json.mjs"; | ||
| import { t as nodeResolve } from "../_libs/hasown+resolve+deepmerge.mjs"; | ||
| import { watch } from "node:fs"; | ||
| import { defu } from "defu"; | ||
| //#region src/build/rollup/config.ts | ||
@@ -25,3 +22,3 @@ const getRollupConfig = async (nitro) => { | ||
| ...await baseBuildPlugins(nitro, base), | ||
| oxc({ | ||
| await oxc({ | ||
| sourcemap: !!nitro.options.sourcemap, | ||
@@ -80,3 +77,2 @@ minify: nitro.options.minify ? { ...nitro.options.oxc?.minify } : false, | ||
| }; | ||
| //#endregion | ||
@@ -101,3 +97,2 @@ //#region src/build/rollup/error.ts | ||
| } | ||
| //#endregion | ||
@@ -129,7 +124,11 @@ //#region src/build/rollup/dev.ts | ||
| ]); | ||
| const scanDirsWatcher = watch$1(scanDirs, { ignoreInitial: true }).on("all", (event, path, stat$1) => { | ||
| const scanDirsWatcher = watch(scanDirs, { ignoreInitial: true }).on("all", (event, path, stat) => { | ||
| if (watchReloadEvents.has(event)) reload(); | ||
| }); | ||
| const rootDirWatcher = watch(nitro.options.rootDir, { persistent: false }, (_event, filename) => { | ||
| if (filename && /^server\.[mc]?[jt]sx?$/.test(filename)) reload(); | ||
| const serverEntryRe = /^server\.[mc]?[jt]sx?$/; | ||
| const rootDirWatcher = watch(nitro.options.rootDir, { | ||
| ignoreInitial: true, | ||
| depth: 0 | ||
| }).on("all", (event, path) => { | ||
| if (watchReloadEvents.has(event) && serverEntryRe.test(basename(path))) reload(); | ||
| }); | ||
@@ -142,5 +141,6 @@ nitro.hooks.hook("close", () => { | ||
| nitro.hooks.hook("rollup:reload", () => reload()); | ||
| nitro.logger.info(`Starting dev watcher (builder: \`rollup\`, preset: \`${nitro.options.preset}\`, compatibility date: \`${formatCompatibilityDate(nitro.options.compatibilityDate)}\`)`); | ||
| await load(); | ||
| function startRollupWatcher(nitro$1, rollupConfig$1) { | ||
| const watcher = rollup.watch(defu(rollupConfig$1, { watch: { chokidar: nitro$1.options.watchOptions } })); | ||
| function startRollupWatcher(nitro, rollupConfig) { | ||
| const watcher = rollup.watch(defu(rollupConfig, { watch: { chokidar: nitro.options.watchOptions } })); | ||
| let start; | ||
@@ -151,13 +151,12 @@ watcher.on("event", (event) => { | ||
| start = Date.now(); | ||
| nitro$1.logger.info(`Starting dev watcher (builder: \`rollup\`, preset: \`${nitro$1.options.preset}\`, compatibility date: \`${formatCompatibilityDate(nitro$1.options.compatibilityDate)}\`)`); | ||
| nitro$1.hooks.callHook("dev:start"); | ||
| nitro.hooks.callHook("dev:start"); | ||
| break; | ||
| case "BUNDLE_END": | ||
| nitro$1.hooks.callHook("compiled", nitro$1); | ||
| if (nitro$1.options.logging.buildSuccess) nitro$1.logger.success(`Server built`, start ? `in ${Date.now() - start}ms` : ""); | ||
| nitro$1.hooks.callHook("dev:reload"); | ||
| nitro.hooks.callHook("compiled", nitro); | ||
| if (nitro.options.logging.buildSuccess) nitro.logger.success(`Server built`, start ? `in ${Date.now() - start}ms` : ""); | ||
| nitro.hooks.callHook("dev:reload"); | ||
| break; | ||
| case "ERROR": | ||
| nitro$1.logger.error(formatRollupError(event.error)); | ||
| nitro$1.hooks.callHook("dev:error", event.error); | ||
| nitro.logger.error(formatRollupError(event.error)); | ||
| nitro.hooks.callHook("dev:error", event.error); | ||
| } | ||
@@ -168,3 +167,2 @@ }); | ||
| } | ||
| //#endregion | ||
@@ -177,5 +175,6 @@ //#region src/build/rollup/prod.ts | ||
| await writeTypes(nitro); | ||
| let output; | ||
| if (!nitro.options.static) { | ||
| nitro.logger.info(`Building server (builder: \`rollup\`, preset: \`${nitro.options.preset}\`, compatibility date: \`${formatCompatibilityDate(nitro.options.compatibilityDate)}\`)`); | ||
| await (await rollup.rollup(rollupConfig).catch((error) => { | ||
| output = await (await rollup.rollup(rollupConfig).catch((error) => { | ||
| nitro.logger.error(formatRollupError(error)); | ||
@@ -185,3 +184,3 @@ throw error; | ||
| } | ||
| const buildInfo = await writeBuildInfo(nitro); | ||
| const buildInfo = await writeBuildInfo(nitro, output); | ||
| if (!nitro.options.static) { | ||
@@ -192,10 +191,5 @@ if (nitro.options.logging.buildSuccess) nitro.logger.success(`Server built in ${Date.now() - buildStartTime}ms`); | ||
| await nitro.hooks.callHook("compiled", nitro); | ||
| const rOutput = relative(process.cwd(), nitro.options.output.dir); | ||
| const rewriteRelativePaths = (input) => { | ||
| return input.replace(/([\s:])\.\/(\S*)/g, `$1${rOutput}/$2`); | ||
| }; | ||
| if (buildInfo.commands.preview) nitro.logger.success(`You can preview this build using \`${rewriteRelativePaths(buildInfo.commands.preview)}\``); | ||
| if (buildInfo.commands.deploy) nitro.logger.success(`You can deploy this build using \`${rewriteRelativePaths(buildInfo.commands.deploy)}\``); | ||
| nitro.logger.success("You can preview this build using `npx nitro preview`"); | ||
| if (buildInfo.commands.deploy) nitro.logger.success("You can deploy this build using `npx nitro deploy --prebuilt`"); | ||
| } | ||
| //#endregion | ||
@@ -209,4 +203,3 @@ //#region src/build/rollup/build.ts | ||
| } | ||
| //#endregion | ||
| export { rollupBuild }; | ||
| export { rollupBuild }; |
@@ -1,4 +0,3 @@ | ||
| import { V as a } from "./common.mjs"; | ||
| import { H as v } from "./common.mjs"; | ||
| import { nitro } from "nitro/vite"; | ||
| //#region src/build/vite/build.ts | ||
@@ -8,10 +7,13 @@ async function viteBuild(nitro$1) { | ||
| const { createBuilder } = await import(nitro$1.options.__vitePkg__ || "vite"); | ||
| await (await createBuilder({ | ||
| const pluginInstance = nitro({ _nitro: nitro$1 }); | ||
| globalThis.__nitro_build__ = true; | ||
| const builder = await createBuilder({ | ||
| base: nitro$1.options.rootDir, | ||
| plugins: [await nitro({ _nitro: nitro$1 })], | ||
| logLevel: a ? "warn" : void 0 | ||
| })).buildApp(); | ||
| plugins: [pluginInstance], | ||
| logLevel: v ? "warn" : void 0 | ||
| }); | ||
| delete globalThis.__nitro_build__; | ||
| await builder.buildApp(); | ||
| } | ||
| //#endregion | ||
| export { viteBuild }; | ||
| export { viteBuild }; |
+80
-112
@@ -1,3 +0,3 @@ | ||
| import { $ as resolveModulePath, A as compressPublicAssets, H as d, I as prettyPath, L as resolveNitroPath, M as build, R as writeFile$1, V as a, at as join, ct as resolve, et as resolveModuleURL, j as src_default, k as scanUnprefixedPublicAssets, p as runParallel, q as findWorkspaceDir, st as relative } from "../_build/common.mjs"; | ||
| import { n as loadConfig, r as watchConfig } from "../_libs/rc9+c12+dotenv.mjs"; | ||
| import { i as loadConfig, s as watchConfig } from "../_libs/c12+rc9.mjs"; | ||
| import { A as src_default, F as prettyPath, H as v, I as resolveNitroPath, K as findWorkspaceDir, L as writeFile$1, O as scanUnprefixedPublicAssets, ct as join, dt as resolve, j as build, k as compressPublicAssets, nt as resolveModulePath, p as runParallel, rt as resolveModuleURL, ut as relative, z as _ } from "../_build/common.mjs"; | ||
| import { n as resolveCompatibilityDates, r as resolveCompatibilityDatesFromEnv } from "../_libs/compatx.mjs"; | ||
@@ -11,20 +11,19 @@ import { t as klona } from "../_libs/klona.mjs"; | ||
| import { createRequire } from "node:module"; | ||
| import consola$1, { consola } from "consola"; | ||
| import consola, { consola as consola$1 } from "consola"; | ||
| import { Hookable, createDebugger } from "hookable"; | ||
| import { existsSync } from "node:fs"; | ||
| import { existsSync, readFileSync } from "node:fs"; | ||
| import { readFile } from "node:fs/promises"; | ||
| import { pathToFileURL } from "node:url"; | ||
| import { defu } from "defu"; | ||
| import { runtimeDir, version } from "nitro/meta"; | ||
| import { joinURL, parseURL, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutTrailingSlash } from "ufo"; | ||
| import { runtimeDir } from "nitro/meta"; | ||
| import { colors } from "consola/utils"; | ||
| import { hash } from "ohash"; | ||
| import { toRequest } from "h3"; | ||
| import http from "node:http"; | ||
| import { toRequest } from "h3"; | ||
| //#region src/config/defaults.ts | ||
| const NitroDefaults = { | ||
| compatibilityDate: "latest", | ||
| debug: d, | ||
| logLevel: a ? 1 : 3, | ||
| debug: _, | ||
| logLevel: v ? 1 : 3, | ||
| runtimeConfig: { | ||
@@ -99,6 +98,5 @@ app: {}, | ||
| name: "nitro", | ||
| version: "" | ||
| version | ||
| } | ||
| }; | ||
| //#endregion | ||
@@ -131,3 +129,2 @@ //#region src/config/resolvers/assets.ts | ||
| } | ||
| //#endregion | ||
@@ -138,3 +135,2 @@ //#region src/config/resolvers/compatibility.ts | ||
| } | ||
| //#endregion | ||
@@ -159,3 +155,2 @@ //#region src/config/resolvers/database.ts | ||
| } | ||
| //#endregion | ||
@@ -170,15 +165,10 @@ //#region src/config/resolvers/export-conditions.ts | ||
| } | ||
| function _resolveExportConditions(conditions, opts) { | ||
| const resolvedConditions = []; | ||
| resolvedConditions.push(opts.dev ? "development" : "production"); | ||
| resolvedConditions.push(...conditions); | ||
| if (opts.node) resolvedConditions.push("node"); | ||
| else resolvedConditions.push("wintercg", "worker", "web", "browser", "workerd", "edge-light", "netlify", "edge-routine", "deno"); | ||
| if (opts.wasm) resolvedConditions.push("wasm", "unwasm"); | ||
| resolvedConditions.push("import", "default", "module"); | ||
| if ("Bun" in globalThis) resolvedConditions.push("bun"); | ||
| else if ("Deno" in globalThis) resolvedConditions.push("deno"); | ||
| return resolvedConditions.filter((c, i) => resolvedConditions.indexOf(c) === i); | ||
| function _resolveExportConditions(userConditions, opts) { | ||
| const conditions = [...userConditions.filter((c) => !c.startsWith("!"))]; | ||
| conditions.push(opts.dev ? "development" : "production"); | ||
| if (opts.wasm) conditions.push("wasm", "unwasm"); | ||
| if (opts.node) conditions.push("node"); | ||
| const negated = new Set(userConditions.filter((c) => c.startsWith("!")).map((c) => c.slice(1))); | ||
| return [...new Set(conditions)].filter((c) => !negated.has(c)); | ||
| } | ||
| //#endregion | ||
@@ -195,6 +185,5 @@ //#region src/config/resolvers/imports.ts | ||
| const scanDirsInNodeModules = options.scanDirs.map((dir) => dir.match(/(?<=\/)node_modules\/(.+)$/)?.[1]).filter(Boolean); | ||
| options.imports.exclude.push(scanDirsInNodeModules.length > 0 ? /* @__PURE__ */ new RegExp(`node_modules\\/(?!${scanDirsInNodeModules.map((dir) => escapeStringRegexp(dir)).join("|")})`) : /[/\\]node_modules[/\\]/); | ||
| options.imports.exclude.push(scanDirsInNodeModules.length > 0 ? new RegExp(`node_modules\\/(?!${scanDirsInNodeModules.map((dir) => escapeStringRegexp(dir)).join("|")})`) : /[/\\]node_modules[/\\]/); | ||
| } | ||
| } | ||
| //#endregion | ||
@@ -239,3 +228,2 @@ //#region src/config/resolvers/open-api.ts | ||
| } | ||
| //#endregion | ||
@@ -257,4 +245,3 @@ //#region src/config/resolvers/tsconfig.ts | ||
| }; | ||
| const tsConfigPath = join(root, "tsconfig.json"); | ||
| const parsed = await parse(tsConfigPath, opts).catch(() => void 0); | ||
| const parsed = await parse(join(root, "tsconfig.json"), opts).catch(() => void 0); | ||
| if (!parsed) return {}; | ||
@@ -288,3 +275,2 @@ const { tsconfig, tsconfigFile } = parsed; | ||
| } | ||
| //#endregion | ||
@@ -306,3 +292,3 @@ //#region src/config/resolvers/paths.ts | ||
| if (options.serverDir === void 0) options.serverDir = options.srcDir; | ||
| consola$1.warn(`"srcDir" option is deprecated. Please use "serverDir" instead.`); | ||
| consola.warn(`"srcDir" option is deprecated. Please use "serverDir" instead.`); | ||
| } | ||
@@ -346,3 +332,3 @@ if (options.serverDir !== false) { | ||
| options.serverEntry.handler = detected; | ||
| consola$1.info(`Detected \`${prettyPath(detected)}\` as server entry.`); | ||
| consola.info(`Detected \`${prettyPath(detected)}\` as server entry.`); | ||
| } | ||
@@ -374,3 +360,3 @@ } | ||
| options.renderer.template = defaultIndex; | ||
| consola$1.info(`Using \`${prettyPath(defaultIndex)}\` as renderer template.`); | ||
| consola.info(`Using \`${prettyPath(defaultIndex)}\` as renderer template.`); | ||
| } | ||
@@ -384,3 +370,2 @@ } | ||
| } | ||
| //#endregion | ||
@@ -430,3 +415,2 @@ //#region src/config/resolvers/route-rules.ts | ||
| } | ||
| //#endregion | ||
@@ -468,7 +452,5 @@ //#region src/config/resolvers/runtime-config.ts | ||
| } | ||
| //#endregion | ||
| //#region src/config/resolvers/storage.ts | ||
| async function resolveStorageOptions(options) {} | ||
| //#endregion | ||
@@ -479,3 +461,2 @@ //#region src/config/resolvers/url.ts | ||
| } | ||
| //#endregion | ||
@@ -489,3 +470,2 @@ //#region src/config/resolvers/error.ts | ||
| } | ||
| //#endregion | ||
@@ -535,3 +515,2 @@ //#region src/config/resolvers/unenv.ts | ||
| } | ||
| //#endregion | ||
@@ -549,4 +528,4 @@ //#region src/config/resolvers/builder.ts | ||
| const pkg = options.builder; | ||
| if (!isPkgInstalled(pkg, options.rootDir)) { | ||
| if (!await consola$1.prompt(`Nitro builder package \`${pkg}\` is not installed. Would you like to install it?`, { | ||
| if (pkg !== "rolldown" && !isPkgInstalled(pkg, options.rootDir)) { | ||
| if (!await consola.prompt(`Nitro builder package \`${pkg}\` is not installed. Would you like to install it?`, { | ||
| type: "confirm", | ||
@@ -560,26 +539,12 @@ default: true, | ||
| } | ||
| for (const pkg of [ | ||
| "rolldown", | ||
| "rollup", | ||
| "vite" | ||
| ]) if (isPkgInstalled(pkg, options.rootDir)) { | ||
| options.builder = pkg; | ||
| if (isPkgInstalled("vite", options.rootDir) && hasNitroViteConfig(options)) { | ||
| options.builder = "vite"; | ||
| return; | ||
| } | ||
| const pkgToInstall = await consola$1.prompt(`No nitro builder specified. Which builder would you like to install?`, { | ||
| type: "select", | ||
| cancel: "null", | ||
| options: VALID_BUILDERS.map((b) => ({ | ||
| label: b, | ||
| value: b | ||
| })) | ||
| }); | ||
| if (!pkgToInstall) throw new Error(`No nitro builder specified. Please install one of the following packages: ${VALID_BUILDERS.join(", ")} and set it as the builder in your nitro config or via the NITRO_BUILDER environment variable.`); | ||
| await installPkg(pkgToInstall, options.rootDir); | ||
| options.builder = pkgToInstall; | ||
| options.builder = "rolldown"; | ||
| } | ||
| const require = createRequire(process.cwd() + "/_index.js"); | ||
| const _require = createRequire(import.meta.url); | ||
| function isPkgInstalled(pkg, root) { | ||
| try { | ||
| require.resolve(pkg, { paths: [root] }); | ||
| _require.resolve(pkg, { paths: [root] }); | ||
| return true; | ||
@@ -591,6 +556,19 @@ } catch { | ||
| async function installPkg(pkg, root) { | ||
| const { addDevDependency } = await import("../_libs/nypm+giget+tinyexec.mjs").then((n) => n.n); | ||
| const { addDevDependency } = await import("../_libs/_5.mjs"); | ||
| return addDevDependency(pkg, { cwd: root }); | ||
| } | ||
| function hasNitroViteConfig(options) { | ||
| for (const ext of [ | ||
| ".ts", | ||
| ".mts", | ||
| ".js", | ||
| ".mjs" | ||
| ]) { | ||
| const configPath = resolve(options.rootDir, `vite.config${ext}`); | ||
| if (existsSync(configPath)) try { | ||
| if (readFileSync(configPath, "utf8").includes("nitro(")) return true; | ||
| } catch {} | ||
| } | ||
| return false; | ||
| } | ||
| //#endregion | ||
@@ -627,2 +605,3 @@ //#region src/config/loader.ts | ||
| const _dotenv = opts.dotenv ?? (configOverrides.dev && { fileName: [".env", ".env.local"] }); | ||
| const envName = opts.c12?.envName ?? (configOverrides.dev ? "development" : "production"); | ||
| const loadedConfig = await (opts.watch ? watchConfig : loadConfig)({ | ||
@@ -632,8 +611,5 @@ name: "nitro", | ||
| dotenv: _dotenv, | ||
| envName, | ||
| extend: { extendKey: ["extends", "preset"] }, | ||
| defaults: NitroDefaults, | ||
| jitiOptions: { alias: { | ||
| nitropack: "nitro/config", | ||
| "nitro/config": "nitro/config" | ||
| } }, | ||
| async overrides({ rawConfigs }) { | ||
@@ -666,3 +642,3 @@ const getConf = (key) => configOverrides[key] ?? rawConfigs.main?.[key] ?? rawConfigs.rc?.[key] ?? rawConfigs.packageJson?.[key]; | ||
| async resolve(id) { | ||
| const preset$1 = await resolvePreset(id, { | ||
| const preset = await resolvePreset(id, { | ||
| static: configOverrides.static, | ||
@@ -672,3 +648,3 @@ compatibilityDate: compatibilityDate || "latest", | ||
| }); | ||
| if (preset$1) return { config: klona(preset$1) }; | ||
| if (preset) return { config: klona(preset) }; | ||
| }, | ||
@@ -682,6 +658,5 @@ ...opts.c12 | ||
| options.compatibilityDate = resolveCompatibilityDates(compatibilityDate, options.compatibilityDate); | ||
| if (options.dev && options.preset !== "nitro-dev") consola$1.info(`Using \`${options.preset}\` emulation in development mode.`); | ||
| if (options.dev && options.preset !== "nitro-dev") consola.info(`Using \`${options.preset}\` emulation in development mode.`); | ||
| return options; | ||
| } | ||
| //#endregion | ||
@@ -693,5 +668,4 @@ //#region src/config/update.ts | ||
| await nitro.hooks.callHook("rollup:reload"); | ||
| consola$1.success("Nitro config hot reloaded!"); | ||
| consola.success("Nitro config hot reloaded!"); | ||
| } | ||
| //#endregion | ||
@@ -713,13 +687,16 @@ //#region src/module.ts | ||
| let _url; | ||
| if (typeof mod === "string") mod = await import(resolveModuleURL(mod, { | ||
| from: [nitroOptions.rootDir], | ||
| extensions: [ | ||
| ".mjs", | ||
| ".cjs", | ||
| ".js", | ||
| ".mts", | ||
| ".cts", | ||
| ".ts" | ||
| ] | ||
| })).then((m) => m.default || m); | ||
| if (typeof mod === "string") { | ||
| _url = resolveModuleURL(mod, { | ||
| from: [nitroOptions.rootDir], | ||
| extensions: [ | ||
| ".mjs", | ||
| ".cjs", | ||
| ".js", | ||
| ".mts", | ||
| ".cts", | ||
| ".ts" | ||
| ] | ||
| }); | ||
| mod = await import(_url).then((m) => m.default || m); | ||
| } | ||
| if (typeof mod === "function") mod = { setup: mod }; | ||
@@ -733,3 +710,2 @@ if ("nitro" in mod) mod = mod.nitro; | ||
| } | ||
| //#endregion | ||
@@ -868,3 +844,2 @@ //#region src/routing.ts | ||
| } | ||
| //#endregion | ||
@@ -886,3 +861,3 @@ //#region src/global.ts | ||
| globalThis[globalKey] = { async fetch(req) { | ||
| for (let r = 0; r < 10 && nitroInstances.length === 0; r++) await new Promise((resolve$1) => setTimeout(resolve$1, 300)); | ||
| for (let r = 0; r < 10 && nitroInstances.length === 0; r++) await new Promise((resolve) => setTimeout(resolve, 300)); | ||
| const nitro = nitroInstances[0]; | ||
@@ -893,3 +868,2 @@ if (!nitro) throw new Error("No Nitro instance is running."); | ||
| } | ||
| //#endregion | ||
@@ -903,3 +877,3 @@ //#region src/nitro.ts | ||
| routing: {}, | ||
| logger: consola.withTag("nitro"), | ||
| logger: consola$1.withTag("nitro"), | ||
| scannedHandlers: [], | ||
@@ -910,4 +884,4 @@ fetch: () => { | ||
| close: () => Promise.resolve(nitro.hooks.callHook("close")), | ||
| async updateConfig(config$1) { | ||
| updateNitroConfig(nitro, config$1); | ||
| async updateConfig(config) { | ||
| updateNitroConfig(nitro, config); | ||
| } | ||
@@ -933,3 +907,2 @@ }; | ||
| } | ||
| //#endregion | ||
@@ -985,3 +958,3 @@ //#region src/prerender/utils.ts | ||
| const errorLead = parents?.size ? "├──" : "└──"; | ||
| str += `\n │ ${errorLead} ${errorColor(route.error.message)}`; | ||
| str += `\n │ ${errorLead} ${errorColor(route.error.message || "unknown error")}`; | ||
| if (parents?.size) str += `\n${[...parents.values()].map((link) => ` │ └── Linked from ${link}`).join("\n")}`; | ||
@@ -998,3 +971,2 @@ } | ||
| } | ||
| //#endregion | ||
@@ -1008,8 +980,4 @@ //#region src/prerender/prerender.ts | ||
| } | ||
| if (nitro.options.builder === "vite") { | ||
| nitro.logger.warn("Skipping prerender since not supported with vite builder yet..."); | ||
| return; | ||
| } | ||
| const routes = new Set(nitro.options.prerender.routes); | ||
| const prerenderRulePaths = Object.entries(nitro.options.routeRules).filter(([path$1, options]) => options.prerender && !path$1.includes("*")).map((e) => e[0]); | ||
| const prerenderRulePaths = Object.entries(nitro.options.routeRules).filter(([path, options]) => options.prerender && !path.includes("*")).map((e) => e[0]); | ||
| for (const route of prerenderRulePaths) routes.add(route); | ||
@@ -1027,3 +995,4 @@ await nitro.hooks.callHook("prerender:routes", routes); | ||
| logLevel: 0, | ||
| preset: "nitro-prerender" | ||
| preset: "nitro-prerender", | ||
| builder: nitro.options.builder === "vite" ? "rolldown" : nitro.options.builder | ||
| }; | ||
@@ -1043,3 +1012,3 @@ await nitro.hooks.callHook("prerender:config", prerendererConfig); | ||
| for (const [route, rules] of Object.entries(nitro.options.routeRules)) addRoute(routeRules, void 0, route, rules); | ||
| const _getRouteRules = (path$1) => defu({}, ...findAllRoutes(routeRules, void 0, path$1).map((r) => r.data).reverse()); | ||
| const _getRouteRules = (path) => defu({}, ...findAllRoutes(routeRules, void 0, path).map((r) => r.data).reverse()); | ||
| const generatedRoutes = /* @__PURE__ */ new Set(); | ||
@@ -1049,3 +1018,3 @@ const failedRoutes = /* @__PURE__ */ new Set(); | ||
| const displayedLengthWarns = /* @__PURE__ */ new Set(); | ||
| const publicAssetBases = nitro.options.publicAssets.filter((a$1) => !!a$1.baseURL && a$1.baseURL !== "/" && !a$1.fallthrough).map((a$1) => withTrailingSlash(a$1.baseURL)); | ||
| const publicAssetBases = nitro.options.publicAssets.filter((a) => !!a.baseURL && a.baseURL !== "/" && !a.fallthrough).map((a) => withTrailingSlash(a.baseURL)); | ||
| const scannedPublicAssets = nitro.options.prerender.ignoreUnprefixedPublicAssets ? new Set(await scanUnprefixedPublicAssets(nitro)) : /* @__PURE__ */ new Set(); | ||
@@ -1063,3 +1032,3 @@ const canPrerender = (route = "/") => { | ||
| const canWriteToDisk = (route) => { | ||
| if (route.route.includes("?")) return false; | ||
| if (route.route.includes("?") || route.route.includes("..")) return false; | ||
| const FS_MAX_SEGMENT = 255; | ||
@@ -1139,4 +1108,5 @@ const FS_MAX_PATH_PUBLIC_HTML = 1024 - (nitro.options.output.publicDir.length + 10); | ||
| } | ||
| if (canWriteToDisk(_route)) { | ||
| await writeFile$1(join(nitro.options.output.publicDir, _route.fileName), dataBuff); | ||
| const filePath = join(nitro.options.output.publicDir, _route.fileName); | ||
| if (canWriteToDisk(_route) && filePath.startsWith(nitro.options.output.publicDir)) { | ||
| await writeFile$1(filePath, dataBuff); | ||
| nitro._prerenderedRoutes.push(_route); | ||
@@ -1173,3 +1143,2 @@ } else _route.skip = true; | ||
| } | ||
| //#endregion | ||
@@ -1198,3 +1167,3 @@ //#region src/task.ts | ||
| const devFetch = (path, options) => { | ||
| return new Promise((resolve$1, reject) => { | ||
| return new Promise((resolve, reject) => { | ||
| let url = withBase(path, baseURL); | ||
@@ -1215,3 +1184,3 @@ if (options?.query) url = withQuery(url, options.query); | ||
| let data = ""; | ||
| response.on("data", (chunk) => data += chunk).on("end", () => resolve$1(JSON.parse(data))).on("error", (e) => reject(e)); | ||
| response.on("data", (chunk) => data += chunk).on("end", () => resolve(JSON.parse(data))).on("error", (e) => reject(e)); | ||
| }); | ||
@@ -1236,4 +1205,3 @@ request.on("error", (e) => reject(e)); | ||
| } | ||
| //#endregion | ||
| export { loadOptions as a, createNitro as i, runTask as n, prerender as r, listTasks as t }; | ||
| export { loadOptions as a, createNitro as i, runTask as n, prerender as r, listTasks as t }; |
@@ -1,4 +0,3 @@ | ||
| import { N as glob, at as join, st as relative } from "../_build/common.mjs"; | ||
| import { M as glob, ct as join, ut as relative } from "../_build/common.mjs"; | ||
| import { withBase, withLeadingSlash, withoutTrailingSlash } from "ufo"; | ||
| //#region src/scan.ts | ||
@@ -26,5 +25,8 @@ const GLOB_SCAN_PATTERN = "**/*.{js,mjs,cjs,ts,mts,cts,tsx,jsx}"; | ||
| const handlers = await Promise.all([scanServerRoutes(nitro, nitro.options.apiDir || "api", nitro.options.apiBaseURL || "/api"), scanServerRoutes(nitro, nitro.options.routesDir || "routes")]).then((r) => r.flat()); | ||
| nitro.scannedHandlers = [...middleware, ...handlers.filter((h, index, array) => { | ||
| return array.findIndex((h2) => h.route === h2.route && h.method === h2.method && h.env === h2.env) === index; | ||
| const seenHandlers = /* @__PURE__ */ new Set(); | ||
| nitro.scannedHandlers = [...middleware, ...handlers.filter((h) => { | ||
| const key = `${h.route}\0${h.method}\0${h.env}`; | ||
| return seenHandlers.has(key) ? false : (seenHandlers.add(key), true); | ||
| })]; | ||
| nitro.routing.sync(); | ||
| return handlers; | ||
@@ -43,3 +45,3 @@ } | ||
| return (await scanFiles(nitro, dir)).map((file) => { | ||
| let route = file.path.replace(/\.[A-Za-z]+$/, "").replace(/\(([^(/\\]+)\)[/\\]/g, "").replace(/\[\.{3}]/g, "**").replace(/\[\.{3}(\w+)]/g, "**:$1").replace(/\[([^/\]]+)]/g, ":$1"); | ||
| let route = file.path.replace(/\.[A-Za-z]+$/, "").replace(/\(([^(/\\]+)\)[/\\]/g, "").replace(/\[\.{3}]/g, "**").replace(/\[\.{3}([^\]]+)]/g, (_, p) => "**:" + p.replace(/[^\w-]/g, "_")).replace(/\[([^/\]]+)]/g, (_, p) => ":" + p.replace(/[^\w-]/g, "_")); | ||
| route = withLeadingSlash(withoutTrailingSlash(withBase(route, prefix))); | ||
@@ -101,4 +103,3 @@ const suffixMatch = route.match(suffixRegex); | ||
| } | ||
| //#endregion | ||
| export { scanHandlers as n, scanAndSyncOptions as t }; | ||
| export { scanHandlers as n, scanAndSyncOptions as t }; |
+42
-24
@@ -1,11 +0,9 @@ | ||
| import { i as __toESM, r as __require, t as __commonJSMin } from "../_common.mjs"; | ||
| import { N as glob, V as a, ct as resolve, nt as dirname, p as runParallel, st as relative } from "../_build/common.mjs"; | ||
| import fs, { promises } from "node:fs"; | ||
| import { r as __require, t as __commonJSMin } from "../_common.mjs"; | ||
| import { H as v, M as glob, V as m, at as dirname, dt as resolve, nt as resolveModulePath, p as runParallel, ut as relative } from "../_build/common.mjs"; | ||
| import { consola as consola$1 } from "consola"; | ||
| import { promises } from "node:fs"; | ||
| import { promisify } from "node:util"; | ||
| import { colors } from "consola/utils"; | ||
| import zlib from "node:zlib"; | ||
| import "node:stream"; | ||
| //#region node_modules/.pnpm/duplexer@0.1.2/node_modules/duplexer/index.js | ||
| var require_duplexer = /* @__PURE__ */ __commonJSMin(((exports, module) => { | ||
| (/* @__PURE__ */ __commonJSMin(((exports, module) => { | ||
| var Stream = __require("stream"); | ||
@@ -56,4 +54,4 @@ var writeMethods = [ | ||
| function proxyStream(methodName) { | ||
| reader.on(methodName, reemit$1); | ||
| function reemit$1() { | ||
| reader.on(methodName, reemit); | ||
| function reemit() { | ||
| var args = slice.call(arguments); | ||
@@ -75,7 +73,3 @@ args.unshift(methodName); | ||
| } | ||
| })); | ||
| //#endregion | ||
| //#region node_modules/.pnpm/gzip-size@7.0.0/node_modules/gzip-size/index.js | ||
| var import_duplexer = /* @__PURE__ */ __toESM(require_duplexer(), 1); | ||
| })))(); | ||
| const getOptions = (options) => ({ | ||
@@ -90,3 +84,2 @@ level: 9, | ||
| } | ||
| //#endregion | ||
@@ -206,7 +199,6 @@ //#region node_modules/.pnpm/pretty-bytes@7.1.0/node_modules/pretty-bytes/index.js | ||
| } | ||
| //#endregion | ||
| //#region src/utils/fs-tree.ts | ||
| async function generateFSTree(dir, options = {}) { | ||
| if (a) return; | ||
| if (v) return; | ||
| const files = await glob("**/*.*", { | ||
@@ -221,3 +213,3 @@ cwd: dir, | ||
| const size = src.byteLength; | ||
| const gzip$1 = options.compressedSizes ? await gzipSize(src) : 0; | ||
| const gzip = options.compressedSizes ? await gzipSize(src) : 0; | ||
| items.push({ | ||
@@ -227,6 +219,6 @@ file, | ||
| size, | ||
| gzip: gzip$1 | ||
| gzip | ||
| }); | ||
| }, { concurrency: 10 }); | ||
| items.sort((a$1, b) => a$1.path.localeCompare(b.path)); | ||
| items.sort((a, b) => a.path.localeCompare(b.path)); | ||
| let totalSize = 0; | ||
@@ -238,4 +230,4 @@ let totalGzip = 0; | ||
| for (const [index, item] of items.entries()) { | ||
| let dir$1 = dirname(item.file); | ||
| if (dir$1 === ".") dir$1 = ""; | ||
| let dir = dirname(item.file); | ||
| if (dir === ".") dir = ""; | ||
| const rpath = relative(process.cwd(), item.path); | ||
@@ -259,4 +251,30 @@ const treeChar = index === items.length - 1 ? "└─" : "├─"; | ||
| } | ||
| //#endregion | ||
| export { generateFSTree as t }; | ||
| //#region src/utils/dep.ts | ||
| async function importDep(opts, _retry) { | ||
| const resolved = resolveModulePath(opts.id, { | ||
| from: [opts.dir, import.meta.url], | ||
| cache: _retry ? false : true, | ||
| try: true | ||
| }); | ||
| if (resolved) return await import(resolved); | ||
| let shouldInstall; | ||
| if (_retry || v) shouldInstall = false; | ||
| else if (m) { | ||
| consola$1.info(`\`${opts.id}\` is required for ${opts.reason}. Installing automatically in CI environment...`); | ||
| shouldInstall = true; | ||
| } else shouldInstall = await consola$1.prompt(`\`${opts.id}\` is required for ${opts.reason}, but it is not installed. Would you like to install it?`, { | ||
| type: "confirm", | ||
| default: true, | ||
| cancel: "undefined" | ||
| }); | ||
| if (!shouldInstall) throw new Error(`\`${opts.id}\` is not installed. Please add it to your dependencies for ${opts.reason}.`); | ||
| const start = Date.now(); | ||
| consola$1.start(`Installing \`${opts.id}\` in \`${opts.dir}\`...`); | ||
| const { addDevDependency } = await import("../_libs/_5.mjs"); | ||
| await addDevDependency(opts.id, { cwd: opts.dir }); | ||
| consola$1.success(`Installed \`${opts.id}\` in ${opts.dir} (${Date.now() - start}ms).`); | ||
| return importDep(opts, true); | ||
| } | ||
| //#endregion | ||
| export { generateFSTree as n, importDep as t }; |
+14
-24
| import { createRequire } from "node:module"; | ||
| //#region rolldown:runtime | ||
| //#region \0rolldown/runtime.js | ||
| var __create = Object.create; | ||
@@ -11,26 +10,18 @@ var __defProp = Object.defineProperty; | ||
| var __commonJSMin = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports); | ||
| var __exportAll = (all, symbols) => { | ||
| var __exportAll = (all, no_symbols) => { | ||
| let target = {}; | ||
| for (var name in all) { | ||
| __defProp(target, name, { | ||
| get: all[name], | ||
| enumerable: true | ||
| }); | ||
| } | ||
| if (symbols) { | ||
| __defProp(target, Symbol.toStringTag, { value: "Module" }); | ||
| } | ||
| for (var name in all) __defProp(target, name, { | ||
| get: all[name], | ||
| enumerable: true | ||
| }); | ||
| if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" }); | ||
| return target; | ||
| }; | ||
| var __copyProps = (to, from, except, desc) => { | ||
| if (from && typeof from === "object" || typeof from === "function") { | ||
| for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) { | ||
| key = keys[i]; | ||
| if (!__hasOwnProp.call(to, key) && key !== except) { | ||
| __defProp(to, key, { | ||
| get: ((k) => from[k]).bind(null, key), | ||
| enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable | ||
| }); | ||
| } | ||
| } | ||
| if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) { | ||
| key = keys[i]; | ||
| if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { | ||
| get: ((k) => from[k]).bind(null, key), | ||
| enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable | ||
| }); | ||
| } | ||
@@ -44,4 +35,3 @@ return to; | ||
| var __require = /* @__PURE__ */ createRequire(import.meta.url); | ||
| //#endregion | ||
| export { __toESM as i, __exportAll as n, __require as r, __commonJSMin as t }; | ||
| export { __toESM as i, __exportAll as n, __require as r, __commonJSMin as t }; |
+10
-12
| import { parseArgs } from "node:util"; | ||
| //#region node_modules/.pnpm/citty@0.2.0/node_modules/citty/dist/_chunks/libs/scule.mjs | ||
| //#region node_modules/.pnpm/citty@0.2.1/node_modules/citty/dist/_chunks/libs/scule.mjs | ||
| const NUMBER_CHAR_RE = /\d/; | ||
@@ -68,5 +67,4 @@ const STR_SPLITTERS = [ | ||
| } | ||
| //#endregion | ||
| //#region node_modules/.pnpm/citty@0.2.0/node_modules/citty/dist/index.mjs | ||
| //#region node_modules/.pnpm/citty@0.2.1/node_modules/citty/dist/index.mjs | ||
| function toArray(val) { | ||
@@ -161,3 +159,9 @@ if (Array.isArray(val)) return val; | ||
| for (const [key, value] of Object.entries(parsed.values)) out[key] = value; | ||
| for (const [name] of Object.entries(negatedFlags)) out[name] = false; | ||
| for (const [name] of Object.entries(negatedFlags)) { | ||
| out[name] = false; | ||
| const mainName = aliasToMain.get(name); | ||
| if (mainName) out[mainName] = false; | ||
| const aliases = mainToAliases.get(name); | ||
| if (aliases) for (const alias of aliases) out[alias] = false; | ||
| } | ||
| for (const [alias, main] of aliasToMain.entries()) { | ||
@@ -299,7 +303,2 @@ if (out[alias] !== void 0 && out[main] === void 0) out[main] = out[alias]; | ||
| argLines.push([cyan(argStr + (isRequired ? " (required)" : "")), arg.description || ""]); | ||
| /** | ||
| * print negative boolean arg variant usage when | ||
| * - enabled by default or has `negativeDescription` | ||
| * - not prefixed with `no-` or `no[A-Z]` | ||
| */ | ||
| if (arg.type === "boolean" && (arg.default === true || arg.negativeDescription) && !negativePrefixRe.test(arg.name)) { | ||
@@ -364,4 +363,3 @@ const negativeArgStr = [...(arg.alias || []).map((a) => `--no-${a}`), `--no-${arg.name}`].join(", "); | ||
| } | ||
| //#endregion | ||
| export { runMain as n, defineCommand as t }; | ||
| export { runMain as n, defineCommand as t }; |
@@ -44,4 +44,3 @@ //#region node_modules/.pnpm/compatx@0.2.0/node_modules/compatx/dist/index.mjs | ||
| } | ||
| //#endregion | ||
| export { resolveCompatibilityDates as n, resolveCompatibilityDatesFromEnv as r, formatCompatibilityDate as t }; | ||
| export { resolveCompatibilityDates as n, resolveCompatibilityDatesFromEnv as r, formatCompatibilityDate as t }; |
@@ -1,2 +0,2 @@ | ||
| //#region node_modules/.pnpm/esbuild@0.27.2/node_modules/esbuild/lib/main.d.ts | ||
| //#region node_modules/.pnpm/esbuild@0.27.3/node_modules/esbuild/lib/main.d.ts | ||
| // Note: These declarations exist to avoid type errors when you omit "dom" from | ||
@@ -3,0 +3,0 @@ // "lib" in your "tsconfig.json" file. TypeScript confusingly declares the |
@@ -6,4 +6,3 @@ //#region node_modules/.pnpm/escape-string-regexp@5.0.0/node_modules/escape-string-regexp/index.js | ||
| } | ||
| //#endregion | ||
| export { escapeStringRegexp as t }; | ||
| export { escapeStringRegexp as t }; |
@@ -50,3 +50,2 @@ //#region node_modules/.pnpm/estree-walker@3.0.3/node_modules/estree-walker/src/walker.js | ||
| }; | ||
| //#endregion | ||
@@ -167,3 +166,2 @@ //#region node_modules/.pnpm/estree-walker@3.0.3/node_modules/estree-walker/src/sync.js | ||
| } | ||
| //#endregion | ||
@@ -187,4 +185,3 @@ //#region node_modules/.pnpm/estree-walker@3.0.3/node_modules/estree-walker/src/index.js | ||
| } | ||
| //#endregion | ||
| export { walk as t }; | ||
| export { walk as t }; |
@@ -1,9 +0,10 @@ | ||
| import http, { IncomingMessage, OutgoingMessage } from "node:http"; | ||
| import { EventEmitter } from "node:events"; | ||
| import * as stream from "node:stream"; | ||
| import http, { IncomingMessage } from "node:http"; | ||
| import { Socket } from "node:net"; | ||
| //#region node_modules/.pnpm/httpxy@0.1.7/node_modules/httpxy/dist/index.d.ts | ||
| //#region node_modules/.pnpm/httpxy@0.3.1/node_modules/httpxy/dist/index.d.ts | ||
| interface ProxyTargetDetailed { | ||
| host: string; | ||
| port: number; | ||
| host?: string; | ||
| port?: number | string; | ||
| protocol?: string; | ||
@@ -20,9 +21,9 @@ hostname?: string; | ||
| } | ||
| type ProxyTarget = ProxyTargetUrl | ProxyTargetDetailed; | ||
| type ProxyTargetUrl = string | Partial<URL>; | ||
| type ProxyTarget = string | URL | ProxyTargetDetailed; | ||
| /** Resolved proxy address — either TCP (host + port) or Unix socket. */ | ||
| interface ProxyServerOptions { | ||
| /** URL string to be parsed with the url module. */ | ||
| /** URL string to be parsed. */ | ||
| target?: ProxyTarget; | ||
| /** URL string to be parsed with the url module. */ | ||
| forward?: ProxyTargetUrl; | ||
| /** URL string to be parsed. */ | ||
| forward?: ProxyTarget; | ||
| /** Object to be passed to http(s).request. */ | ||
@@ -76,2 +77,4 @@ agent?: any; | ||
| selfHandleResponse?: boolean; | ||
| /** Follow HTTP redirects from target. `true` = max 5 hops; number = custom max. */ | ||
| followRedirects?: boolean | number; | ||
| /** Buffer */ | ||
@@ -78,0 +81,0 @@ buffer?: stream.Stream; |
+299
-43
@@ -1,15 +0,10 @@ | ||
| import { n as __exportAll } from "../_common.mjs"; | ||
| import http from "node:http"; | ||
| import https from "node:https"; | ||
| import { EventEmitter } from "node:events"; | ||
| //#region node_modules/.pnpm/httpxy@0.1.7/node_modules/httpxy/dist/index.mjs | ||
| var dist_exports = /* @__PURE__ */ __exportAll({ | ||
| ProxyServer: () => ProxyServer, | ||
| createProxyServer: () => createProxyServer | ||
| }); | ||
| import { Readable } from "node:stream"; | ||
| import http, { request } from "node:http"; | ||
| import nodeHTTPS, { request as request$1 } from "node:https"; | ||
| //#region node_modules/.pnpm/httpxy@0.3.1/node_modules/httpxy/dist/index.mjs | ||
| const upgradeHeader = /(^|,)\s*upgrade\s*($|,)/i; | ||
| const isSSL = /^https|wss/; | ||
| function setupOutgoing(outgoing, options, req, forward) { | ||
| outgoing.port = options[forward || "target"].port || (isSSL.test(options[forward || "target"].protocol) ? 443 : 80); | ||
| outgoing.port = options[forward || "target"].port || (isSSL.test(options[forward || "target"].protocol ?? "http") ? 443 : 80); | ||
| for (const e of [ | ||
@@ -35,3 +30,3 @@ "host", | ||
| if (options.ca) outgoing.ca = options.ca; | ||
| if (isSSL.test(options[forward || "target"].protocol)) outgoing.rejectUnauthorized = options.secure === void 0 ? true : options.secure; | ||
| if (isSSL.test(options[forward || "target"].protocol ?? "http")) outgoing.rejectUnauthorized = options.secure === void 0 ? true : options.secure; | ||
| outgoing.agent = options.agent || false; | ||
@@ -44,8 +39,12 @@ outgoing.localAddress = options.localAddress; | ||
| const target = options[forward || "target"]; | ||
| const targetPath = target && options.prependPath !== false ? target.pathname || target.path || "" : ""; | ||
| const parsed = new URL(req.url, "http://localhost"); | ||
| let outgoingPath = options.toProxy ? req.url : parsed.pathname + parsed.search || ""; | ||
| const targetPath = target && options.prependPath !== false ? target.pathname || "" : ""; | ||
| const reqUrl = req.url || ""; | ||
| const qIdx = reqUrl.indexOf("?"); | ||
| const reqPath = qIdx === -1 ? reqUrl : reqUrl.slice(0, qIdx); | ||
| const reqSearch = qIdx === -1 ? "" : reqUrl.slice(qIdx); | ||
| const normalizedPath = reqPath ? reqPath[0] === "/" ? reqPath : "/" + reqPath : "/"; | ||
| let outgoingPath = options.toProxy ? "/" + reqUrl : normalizedPath + reqSearch; | ||
| outgoingPath = options.ignorePath ? "" : outgoingPath; | ||
| outgoing.path = joinURL(targetPath, outgoingPath); | ||
| if (options.changeOrigin) outgoing.headers.host = requiresPort(outgoing.port, options[forward || "target"].protocol) && !hasPort(outgoing.host) ? outgoing.host + ":" + outgoing.port : outgoing.host; | ||
| if (options.changeOrigin) outgoing.headers.host = requiresPort(outgoing.port, options[forward || "target"].protocol) && !hasPort(outgoing.host) ? outgoing.host + ":" + outgoing.port : outgoing.host ?? void 0; | ||
| return outgoing; | ||
@@ -88,7 +87,19 @@ } | ||
| } | ||
| function parseAddr(addr) { | ||
| if (typeof addr === "string") { | ||
| if (addr.startsWith("unix:")) return { socketPath: addr.slice(5) }; | ||
| const url = new URL(addr); | ||
| return { | ||
| host: url.hostname, | ||
| port: Number(url.port) || (isSSL.test(url.protocol) ? 443 : 80) | ||
| }; | ||
| } | ||
| if (!addr.socketPath && !addr.port) throw new Error("ProxyAddr must have either `port` or `socketPath`"); | ||
| return addr; | ||
| } | ||
| function hasPort(host) { | ||
| return !!~host.indexOf(":"); | ||
| return host ? !!~host.indexOf(":") : false; | ||
| } | ||
| function requiresPort(_port, _protocol) { | ||
| const protocol = _protocol.split(":")[0]; | ||
| const protocol = _protocol?.split(":")[0]; | ||
| const port = +_port; | ||
@@ -124,7 +135,7 @@ if (!port) return false; | ||
| if ((options.hostRewrite || options.autoRewrite || options.protocolRewrite) && proxyRes.headers.location && redirectRegex.test(String(proxyRes.statusCode))) { | ||
| const target = new URL(options.target); | ||
| const target = options.target instanceof URL ? options.target : new URL(options.target); | ||
| const u = new URL(proxyRes.headers.location); | ||
| if (target.host !== u.host) return; | ||
| if (options.hostRewrite) u.host = options.hostRewrite; | ||
| else if (options.autoRewrite) u.host = req.headers.host; | ||
| else if (options.autoRewrite && req.headers.host) u.host = req.headers.host; | ||
| if (options.protocolRewrite) u.protocol = options.protocolRewrite; | ||
@@ -135,4 +146,4 @@ proxyRes.headers.location = u.toString(); | ||
| defineProxyOutgoingMiddleware((req, res, proxyRes, options) => { | ||
| let rewriteCookieDomainConfig = options.cookieDomainRewrite; | ||
| let rewriteCookiePathConfig = options.cookiePathRewrite; | ||
| const rewriteCookieDomainConfig = typeof options.cookieDomainRewrite === "string" ? { "*": options.cookieDomainRewrite } : options.cookieDomainRewrite; | ||
| const rewriteCookiePathConfig = typeof options.cookiePathRewrite === "string" ? { "*": options.cookiePathRewrite } : options.cookiePathRewrite; | ||
| const preserveHeaderKeyCase = options.preserveHeaderKeyCase; | ||
@@ -146,4 +157,2 @@ let rawHeaderKeyMap; | ||
| }; | ||
| if (typeof rewriteCookieDomainConfig === "string") rewriteCookieDomainConfig = { "*": rewriteCookieDomainConfig }; | ||
| if (typeof rewriteCookiePathConfig === "string") rewriteCookiePathConfig = { "*": rewriteCookiePathConfig }; | ||
| if (preserveHeaderKeyCase && proxyRes.rawHeaders !== void 0) { | ||
@@ -171,4 +180,11 @@ rawHeaderKeyMap = {}; | ||
| http, | ||
| https | ||
| https: nodeHTTPS | ||
| }; | ||
| const redirectStatuses = /* @__PURE__ */ new Set([ | ||
| 301, | ||
| 302, | ||
| 303, | ||
| 307, | ||
| 308 | ||
| ]); | ||
| const webIncomingMiddleware = [ | ||
@@ -182,3 +198,5 @@ defineProxyMiddleware((req) => { | ||
| defineProxyMiddleware((req, res, options) => { | ||
| if (options.timeout) req.socket.setTimeout(options.timeout); | ||
| if (options.timeout) req.socket.setTimeout(options.timeout, () => { | ||
| req.socket.destroy(); | ||
| }); | ||
| }), | ||
@@ -202,7 +220,7 @@ defineProxyMiddleware((req, res, options) => { | ||
| server.emit("start", req, res, options.target || options.forward); | ||
| const agents = nativeAgents; | ||
| const http$1 = agents.http; | ||
| const https$1 = agents.https; | ||
| const http = nativeAgents.http; | ||
| const https = nativeAgents.https; | ||
| const maxRedirects = typeof options.followRedirects === "number" ? options.followRedirects : options.followRedirects ? 5 : 0; | ||
| if (options.forward) { | ||
| const forwardReq = (options.forward.protocol === "https:" ? https$1 : http$1).request(setupOutgoing(options.ssl || {}, options, req, "forward")); | ||
| const forwardReq = (options.forward.protocol === "https:" ? https : http).request(setupOutgoing(options.ssl || {}, options, req, "forward")); | ||
| const forwardError = createErrorHandler(forwardReq, options.forward); | ||
@@ -217,4 +235,4 @@ req.on("error", forwardError); | ||
| } | ||
| const proxyReq = (options.target.protocol === "https:" ? https$1 : http$1).request(setupOutgoing(options.ssl || {}, options, req)); | ||
| proxyReq.on("socket", (socket) => { | ||
| const proxyReq = (options.target.protocol === "https:" ? https : http).request(setupOutgoing(options.ssl || {}, options, req)); | ||
| proxyReq.on("socket", (_socket) => { | ||
| if (server && !proxyReq.getHeader("expect")) server.emit("proxyReq", proxyReq, req, res, options); | ||
@@ -228,2 +246,5 @@ }); | ||
| }); | ||
| res.on("close", function() { | ||
| if (!res.writableFinished) proxyReq.destroy(); | ||
| }); | ||
| const proxyError = createErrorHandler(proxyReq, options.target); | ||
@@ -242,4 +263,62 @@ req.on("error", proxyError); | ||
| } | ||
| (options.buffer || req).pipe(proxyReq); | ||
| proxyReq.on("response", function(proxyRes) { | ||
| let bodyBuffer; | ||
| if (maxRedirects > 0) { | ||
| const chunks = []; | ||
| const source = options.buffer || req; | ||
| source.on("data", (chunk) => { | ||
| chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk); | ||
| proxyReq.write(chunk); | ||
| }); | ||
| source.on("end", () => { | ||
| bodyBuffer = Buffer.concat(chunks); | ||
| proxyReq.end(); | ||
| }); | ||
| source.on("error", (err) => { | ||
| proxyReq.destroy(err); | ||
| }); | ||
| } else (options.buffer || req).pipe(proxyReq); | ||
| function handleResponse(proxyRes, redirectCount, currentUrl) { | ||
| const statusCode = proxyRes.statusCode; | ||
| if (maxRedirects > 0 && redirectStatuses.has(statusCode) && redirectCount < maxRedirects && proxyRes.headers.location) { | ||
| proxyRes.resume(); | ||
| const location = new URL(proxyRes.headers.location, currentUrl); | ||
| const preserveMethod = statusCode === 307 || statusCode === 308; | ||
| const redirectMethod = preserveMethod ? req.method || "GET" : "GET"; | ||
| const isHTTPS = location.protocol === "https:"; | ||
| const agent = isHTTPS ? https : http; | ||
| const redirectHeaders = { ...req.headers }; | ||
| if (options.headers) Object.assign(redirectHeaders, options.headers); | ||
| redirectHeaders.host = location.host; | ||
| if (location.host !== currentUrl.host) { | ||
| delete redirectHeaders.authorization; | ||
| delete redirectHeaders.cookie; | ||
| } | ||
| if (!preserveMethod) { | ||
| delete redirectHeaders["content-length"]; | ||
| delete redirectHeaders["content-type"]; | ||
| delete redirectHeaders["transfer-encoding"]; | ||
| } | ||
| const redirectOpts = { | ||
| hostname: location.hostname, | ||
| port: location.port || (isHTTPS ? 443 : 80), | ||
| path: location.pathname + location.search, | ||
| method: redirectMethod, | ||
| headers: redirectHeaders, | ||
| agent: options.agent || false | ||
| }; | ||
| if (isHTTPS) redirectOpts.rejectUnauthorized = options.secure === void 0 ? true : options.secure; | ||
| const redirectReq = agent.request(redirectOpts); | ||
| if (server && !redirectReq.getHeader("expect")) server.emit("proxyReq", redirectReq, req, res, options); | ||
| if (options.proxyTimeout) redirectReq.setTimeout(options.proxyTimeout, () => { | ||
| redirectReq.abort(); | ||
| }); | ||
| const redirectError = createErrorHandler(redirectReq, location); | ||
| redirectReq.on("error", redirectError); | ||
| redirectReq.on("response", (nextRes) => { | ||
| handleResponse(nextRes, redirectCount + 1, location); | ||
| }); | ||
| if (preserveMethod && bodyBuffer && bodyBuffer.length > 0) redirectReq.end(bodyBuffer); | ||
| else redirectReq.end(); | ||
| return; | ||
| } | ||
| if (server) server.emit("proxyRes", proxyRes, req, res); | ||
@@ -260,2 +339,5 @@ if (!res.headersSent && !options.selfHandleResponse) { | ||
| } | ||
| } | ||
| proxyReq.on("response", function(proxyRes) { | ||
| handleResponse(proxyRes, 0, options.target); | ||
| }); | ||
@@ -302,3 +384,4 @@ }) | ||
| if (head && head.length > 0) socket.unshift(head); | ||
| const proxyReq = (isSSL.test(options.target.protocol) ? https : http).request(setupOutgoing(options.ssl || {}, options, req)); | ||
| socket.on("error", onSocketError); | ||
| const proxyReq = (isSSL.test(options.target.protocol || "http") ? nodeHTTPS : http).request(setupOutgoing(options.ssl || {}, options, req)); | ||
| if (server) server.emit("proxyReqWs", proxyReq, req, socket, options, head); | ||
@@ -328,2 +411,7 @@ proxyReq.on("error", onOutgoingError); | ||
| proxyReq.end(); | ||
| function onSocketError(err) { | ||
| if (callback) callback(err, req, socket); | ||
| else server.emit("error", err, req, socket); | ||
| proxyReq.destroy(); | ||
| } | ||
| function onOutgoingError(err) { | ||
@@ -363,5 +451,5 @@ if (callback) callback(err, req, socket); | ||
| }; | ||
| this._server = this.options.ssl ? https.createServer(this.options.ssl, closure) : http.createServer(closure); | ||
| this._server = this.options.ssl ? nodeHTTPS.createServer(this.options.ssl, closure) : http.createServer(closure); | ||
| if (this.options.ws) this._server.on("upgrade", (req, socket, head) => { | ||
| this._ws(req, socket, head); | ||
| this.ws(req, socket, head).catch(() => {}); | ||
| }); | ||
@@ -382,3 +470,3 @@ this._server.listen(port, hostname); | ||
| if (type !== "ws" && type !== "web") throw new Error("type must be `web` or `ws`"); | ||
| const passes = type === "ws" ? this._wsPasses : this._webPasses; | ||
| const passes = this._getPasses(type); | ||
| let i = false; | ||
@@ -391,3 +479,3 @@ for (const [idx, v] of passes.entries()) if (v.name === passName) i = idx; | ||
| if (type !== "ws" && type !== "web") throw new Error("type must be `web` or `ws`"); | ||
| const passes = type === "ws" ? this._wsPasses : this._webPasses; | ||
| const passes = this._getPasses(type); | ||
| let i = false; | ||
@@ -398,2 +486,6 @@ for (const [idx, v] of passes.entries()) if (v.name === passName) i = idx; | ||
| } | ||
| /** @internal */ | ||
| _getPasses(type) { | ||
| return type === "ws" ? this._wsPasses : this._webPasses; | ||
| } | ||
| }; | ||
@@ -410,3 +502,6 @@ function createProxyServer(options = {}) { | ||
| for (const key of ["target", "forward"]) if (typeof requestOptions[key] === "string") requestOptions[key] = new URL(requestOptions[key]); | ||
| if (!requestOptions.target && !requestOptions.forward) return this.emit("error", /* @__PURE__ */ new Error("Must provide a proper URL as target")); | ||
| if (!requestOptions.target && !requestOptions.forward) { | ||
| this.emit("error", /* @__PURE__ */ new Error("Must provide a proper URL as target")); | ||
| return Promise.resolve(); | ||
| } | ||
| let _resolve; | ||
@@ -424,4 +519,7 @@ let _reject; | ||
| }); | ||
| for (const pass of type === "ws" ? server._wsPasses : server._webPasses) if (pass(req, res, requestOptions, server, head, (error) => { | ||
| _reject(error); | ||
| for (const pass of server._getPasses(type)) if (pass(req, res, requestOptions, server, head, (error) => { | ||
| if (server.listenerCount("error") > 0) { | ||
| server.emit("error", error, req, res); | ||
| _resolve(); | ||
| } else _reject(error); | ||
| })) { | ||
@@ -434,4 +532,162 @@ _resolve(); | ||
| } | ||
| async function proxyFetch(addr, input, inputInit) { | ||
| const resolvedAddr = parseAddr(addr); | ||
| let url; | ||
| let init; | ||
| if (input instanceof Request) { | ||
| url = new URL(input.url); | ||
| init = { | ||
| ...toInit(input), | ||
| ...toInit(inputInit) | ||
| }; | ||
| } else { | ||
| url = new URL(input); | ||
| init = toInit(inputInit); | ||
| } | ||
| init = { | ||
| redirect: "manual", | ||
| ...init | ||
| }; | ||
| if (init.body) init.duplex = "half"; | ||
| const path = url.pathname + url.search; | ||
| const reqHeaders = {}; | ||
| if (init.headers) { | ||
| const h = init.headers instanceof Headers ? init.headers : new Headers(init.headers); | ||
| for (const [key, value] of h) reqHeaders[key] = value; | ||
| } | ||
| const res = await new Promise((resolve, reject) => { | ||
| const reqOpts = { | ||
| method: init.method || "GET", | ||
| path, | ||
| headers: reqHeaders | ||
| }; | ||
| if (resolvedAddr.socketPath) reqOpts.socketPath = resolvedAddr.socketPath; | ||
| else { | ||
| reqOpts.hostname = resolvedAddr.host || "localhost"; | ||
| reqOpts.port = resolvedAddr.port; | ||
| } | ||
| const req = request(reqOpts, resolve); | ||
| req.on("error", reject); | ||
| if (init.body instanceof ReadableStream) { | ||
| const readable = Readable.fromWeb(init.body); | ||
| readable.on("error", reject); | ||
| readable.pipe(req); | ||
| } else if (init.body) req.end(init.body); | ||
| else req.end(); | ||
| }); | ||
| const headers = new Headers(); | ||
| for (const [key, value] of Object.entries(res.headers)) { | ||
| if (key === "transfer-encoding" || key === "keep-alive" || key === "connection") continue; | ||
| if (Array.isArray(value)) for (const v of value) headers.append(key, v); | ||
| else if (value) headers.set(key, value); | ||
| } | ||
| const hasBody = res.statusCode !== 204 && res.statusCode !== 304; | ||
| return new Response(hasBody ? Readable.toWeb(res) : null, { | ||
| status: res.statusCode, | ||
| statusText: res.statusMessage, | ||
| headers | ||
| }); | ||
| } | ||
| function toInit(init) { | ||
| if (!init) return; | ||
| if (init instanceof Request) return { | ||
| method: init.method, | ||
| headers: init.headers, | ||
| body: init.body, | ||
| duplex: init.body ? "half" : void 0 | ||
| }; | ||
| return init; | ||
| } | ||
| function proxyUpgrade(addr, req, socket, head, opts) { | ||
| const resolvedAddr = parseAddr(addr); | ||
| if (req.method !== "GET" || req.headers.upgrade?.toLowerCase() !== "websocket") { | ||
| socket.destroy(); | ||
| return Promise.reject(/* @__PURE__ */ new Error("Not a valid WebSocket upgrade request")); | ||
| } | ||
| if (opts?.xfwd !== false) { | ||
| const xfFor = req.headers["x-forwarded-for"]; | ||
| const xfPort = req.headers["x-forwarded-port"]; | ||
| const xfProto = req.headers["x-forwarded-proto"]; | ||
| req.headers["x-forwarded-for"] = `${xfFor ? `${xfFor},` : ""}${req.socket?.remoteAddress}`; | ||
| req.headers["x-forwarded-port"] = `${xfPort ? `${xfPort},` : ""}${getPort(req)}`; | ||
| req.headers["x-forwarded-proto"] = `${xfProto ? `${xfProto},` : ""}${hasEncryptedConnection(req) ? "wss" : "ws"}`; | ||
| } | ||
| const target = _buildTargetURL(resolvedAddr); | ||
| const requestOptions = { | ||
| ...opts, | ||
| target, | ||
| prependPath: opts?.prependPath !== false | ||
| }; | ||
| const outgoing = setupOutgoing(requestOptions.ssl || {}, requestOptions, req); | ||
| const sock = socket; | ||
| return new Promise((resolve, reject) => { | ||
| let settled = false; | ||
| setupSocket(sock); | ||
| if (head && head.length > 0) sock.unshift(head); | ||
| sock.once("error", onSocketError); | ||
| const proxyReq = (isSSL.test(target.protocol) ? request$1 : request)(outgoing); | ||
| proxyReq.once("error", onOutgoingError); | ||
| proxyReq.once("response", (res) => { | ||
| if (!res.upgrade) { | ||
| sock.write(_createHttpHeader(`HTTP/${res.httpVersion} ${res.statusCode} ${res.statusMessage}`, res.headers)); | ||
| res.pipe(sock); | ||
| if (!settled) { | ||
| settled = true; | ||
| reject(/* @__PURE__ */ new Error("Upstream server did not upgrade the connection")); | ||
| } | ||
| } | ||
| }); | ||
| proxyReq.once("upgrade", (proxyRes, proxySocket, proxyHead) => { | ||
| proxySocket.once("error", onOutgoingError); | ||
| sock.removeListener("error", onSocketError); | ||
| sock.once("error", () => { | ||
| proxySocket.end(); | ||
| }); | ||
| setupSocket(proxySocket); | ||
| if (proxyHead && proxyHead.length > 0) proxySocket.unshift(proxyHead); | ||
| sock.write(_createHttpHeader("HTTP/1.1 101 Switching Protocols", proxyRes.headers)); | ||
| proxySocket.pipe(sock).pipe(proxySocket); | ||
| settled = true; | ||
| resolve(proxySocket); | ||
| }); | ||
| proxyReq.end(); | ||
| function onSocketError(err) { | ||
| proxyReq.destroy(); | ||
| if (!settled) { | ||
| settled = true; | ||
| reject(err); | ||
| } | ||
| } | ||
| function onOutgoingError(err) { | ||
| sock.end(); | ||
| if (!settled) { | ||
| settled = true; | ||
| reject(err); | ||
| } | ||
| } | ||
| }); | ||
| } | ||
| function _buildTargetURL(addr) { | ||
| if (addr.socketPath) { | ||
| const url = new URL("http://unix"); | ||
| url.socketPath = addr.socketPath; | ||
| return url; | ||
| } | ||
| return new URL(`http://${addr.host || "localhost"}${addr.port ? `:${addr.port}` : ""}`); | ||
| } | ||
| function _createHttpHeader(line, headers) { | ||
| let result = line; | ||
| for (const key of Object.keys(headers)) { | ||
| const value = headers[key]; | ||
| if (value === void 0) continue; | ||
| if (Array.isArray(value)) for (const element of value) result += `\r | ||
| ${key}: ${element}`; | ||
| else result += `\r | ||
| ${key}: ${value}`; | ||
| } | ||
| return `${result}\r | ||
| \r | ||
| `; | ||
| } | ||
| //#endregion | ||
| export { dist_exports as n, createProxyServer as t }; | ||
| export { proxyFetch as n, proxyUpgrade as r, createProxyServer as t }; |
@@ -36,4 +36,3 @@ //#region node_modules/.pnpm/klona@2.0.6/node_modules/klona/full/index.mjs | ||
| } | ||
| //#endregion | ||
| export { klona as t }; | ||
| export { klona as t }; |
@@ -1,2 +0,2 @@ | ||
| //#region node_modules/.pnpm/mlly@1.8.0/node_modules/mlly/dist/index.d.ts | ||
| //#region node_modules/.pnpm/mlly@1.8.1/node_modules/mlly/dist/index.d.ts | ||
| /** | ||
@@ -3,0 +3,0 @@ * Represents a general structure for ECMAScript module exports. |
| import { n as __exportAll } from "../_common.mjs"; | ||
| import path from "node:path"; | ||
| //#region node_modules/.pnpm/@rollup+plugin-alias@6.0.0_rollup@4.55.3/node_modules/@rollup/plugin-alias/dist/index.js | ||
| //#region node_modules/.pnpm/@rollup+plugin-alias@6.0.0_rollup@4.59.0/node_modules/@rollup/plugin-alias/dist/index.js | ||
| var dist_exports = /* @__PURE__ */ __exportAll({ default: () => alias }); | ||
@@ -64,4 +63,3 @@ function matches(pattern, importee) { | ||
| } | ||
| //#endregion | ||
| export { dist_exports as n, alias as t }; | ||
| export { dist_exports as n, alias as t }; |
@@ -1,7 +0,4 @@ | ||
| import { n as __exportAll } from "../_common.mjs"; | ||
| import { S as MagicString, c as walk, i as createFilter, r as attachScopes, s as makeLegalIdentifier } from "../_build/common.mjs"; | ||
| import { sep } from "path"; | ||
| //#region node_modules/.pnpm/@rollup+plugin-inject@5.0.5_rollup@4.55.3/node_modules/@rollup/plugin-inject/dist/es/index.js | ||
| var es_exports = /* @__PURE__ */ __exportAll({ default: () => inject }); | ||
| //#region node_modules/.pnpm/@rollup+plugin-inject@5.0.5_rollup@4.59.0/node_modules/@rollup/plugin-inject/dist/es/index.js | ||
| var escape = function(str) { | ||
@@ -133,4 +130,3 @@ return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&"); | ||
| } | ||
| //#endregion | ||
| export { inject as n, es_exports as t }; | ||
| export { inject as t }; |
| import { a as dataToEsm, i as createFilter } from "../_build/common.mjs"; | ||
| //#region node_modules/.pnpm/@rollup+plugin-json@6.1.0_rollup@4.55.3/node_modules/@rollup/plugin-json/dist/es/index.js | ||
| //#region node_modules/.pnpm/@rollup+plugin-json@6.1.0_rollup@4.59.0/node_modules/@rollup/plugin-json/dist/es/index.js | ||
| function json(options) { | ||
@@ -34,4 +33,3 @@ if (options === void 0) options = {}; | ||
| } | ||
| //#endregion | ||
| export { json as t }; | ||
| export { json as t }; |
@@ -1,2 +0,1 @@ | ||
| import { n as __exportAll } from "../_common.mjs"; | ||
| import { S as MagicString, x as stripLiteral } from "../_build/common.mjs"; | ||
@@ -6,7 +5,6 @@ import fs from "node:fs"; | ||
| import path from "node:path"; | ||
| import assert from "node:assert"; | ||
| import "node:assert"; | ||
| import { createHash } from "node:crypto"; | ||
| import { isCSSRequest, normalizePath } from "vite"; | ||
| import assert$1 from "node:assert/strict"; | ||
| //#region node_modules/.pnpm/@rolldown+pluginutils@1.0.0-beta.55/node_modules/@rolldown/pluginutils/dist/simple-filters.js | ||
@@ -40,5 +38,4 @@ /** | ||
| } | ||
| //#endregion | ||
| //#region node_modules/.pnpm/@hiogawa+vite-plugin-fullstack@0.0.11_vite@8.0.0-beta.8_@types+node@25.0.9_esbuild@0.27_577572ae8fc6296e77b8a4205393d7c0/node_modules/@hiogawa/vite-plugin-fullstack/dist/plugin-B4MlD0Bd.js | ||
| //#region node_modules/.pnpm/@hiogawa+vite-plugin-fullstack@0.0.11_vite@8.0.0-beta.18_@types+node@25.4.0_esbuild@0.2_62dda1eccee284f7388c2af4cc76ce61/node_modules/@hiogawa/vite-plugin-fullstack/dist/plugin-B4MlD0Bd.js | ||
| function parseIdQuery(id) { | ||
@@ -303,3 +300,3 @@ if (!id.includes("?")) return { | ||
| async handler(id) { | ||
| if (id === "\0virtual:fullstack/runtime") return `export const mergeAssets = ${(await Promise.resolve().then(() => runtime_exports)).mergeAssets.toString()};`; | ||
| if (id === "\0virtual:fullstack/runtime") return `export const mergeAssets = ${(await import("./_2.mjs")).mergeAssets.toString()};`; | ||
| const parsed = parseAssetsVirtual(id); | ||
@@ -627,6 +624,4 @@ if (!parsed) return; | ||
| } | ||
| //#endregion | ||
| //#region node_modules/.pnpm/@hiogawa+vite-plugin-fullstack@0.0.11_vite@8.0.0-beta.8_@types+node@25.0.9_esbuild@0.27_577572ae8fc6296e77b8a4205393d7c0/node_modules/@hiogawa/vite-plugin-fullstack/dist/runtime.js | ||
| var runtime_exports = /* @__PURE__ */ __exportAll({ mergeAssets: () => mergeAssets }); | ||
| //#region node_modules/.pnpm/@hiogawa+vite-plugin-fullstack@0.0.11_vite@8.0.0-beta.18_@types+node@25.4.0_esbuild@0.2_62dda1eccee284f7388c2af4cc76ce61/node_modules/@hiogawa/vite-plugin-fullstack/dist/runtime.js | ||
| function mergeAssets(...args) { | ||
@@ -654,4 +649,3 @@ const js = uniqBy(args.flatMap((h) => h.js), (a) => a.href); | ||
| } | ||
| //#endregion | ||
| export { assetsPlugin as n, runtime_exports as t }; | ||
| export { assetsPlugin as n, mergeAssets as t }; |
+187
-192
| import { n as __exportAll } from "../_common.mjs"; | ||
| import { Stats, stat, unwatchFile, watch, watchFile } from "node:fs"; | ||
| import { stat, unwatchFile, watch, watchFile } from "node:fs"; | ||
| import { lstat, open, readdir, realpath, stat as stat$1 } from "node:fs/promises"; | ||
@@ -9,3 +9,2 @@ import { type } from "node:os"; | ||
| import { Readable } from "node:stream"; | ||
| //#region node_modules/.pnpm/readdirp@5.0.0/node_modules/readdirp/index.js | ||
@@ -95,12 +94,12 @@ const EntryTypes = { | ||
| }; | ||
| const { root, type: type$1 } = opts; | ||
| const { root, type } = opts; | ||
| this._fileFilter = normalizeFilter(opts.fileFilter); | ||
| this._directoryFilter = normalizeFilter(opts.directoryFilter); | ||
| const statMethod = opts.lstat ? lstat : stat$1; | ||
| if (wantBigintFsStats) this._stat = (path$1) => statMethod(path$1, { bigint: true }); | ||
| if (wantBigintFsStats) this._stat = (path) => statMethod(path, { bigint: true }); | ||
| else this._stat = statMethod; | ||
| this._maxDepth = opts.depth != null && Number.isSafeInteger(opts.depth) ? opts.depth : defaultOptions.depth; | ||
| this._wantsDir = type$1 ? DIR_TYPES.has(type$1) : false; | ||
| this._wantsFile = type$1 ? FILE_TYPES.has(type$1) : false; | ||
| this._wantsEverything = type$1 === EntryTypes.EVERYTHING_TYPE; | ||
| this._wantsDir = type ? DIR_TYPES.has(type) : false; | ||
| this._wantsFile = type ? FILE_TYPES.has(type) : false; | ||
| this._wantsEverything = type === EntryTypes.EVERYTHING_TYPE; | ||
| this._root = resolve(root); | ||
@@ -125,4 +124,4 @@ this._isDirent = !opts.alwaysStat; | ||
| if (fil && fil.length > 0) { | ||
| const { path: path$1, depth } = par; | ||
| const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path$1)); | ||
| const { path, depth } = par; | ||
| const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path)); | ||
| const awaited = await Promise.all(slice); | ||
@@ -162,6 +161,6 @@ for (const entry of awaited) { | ||
| } | ||
| async _exploreDir(path$1, depth) { | ||
| async _exploreDir(path, depth) { | ||
| let files; | ||
| try { | ||
| files = await readdir(path$1, this._rdOptions); | ||
| files = await readdir(path, this._rdOptions); | ||
| } catch (error) { | ||
@@ -173,14 +172,14 @@ this._onError(error); | ||
| depth, | ||
| path: path$1 | ||
| path | ||
| }; | ||
| } | ||
| async _formatEntry(dirent, path$1) { | ||
| async _formatEntry(dirent, path) { | ||
| let entry; | ||
| const basename$1 = this._isDirent ? dirent.name : dirent; | ||
| const basename = this._isDirent ? dirent.name : dirent; | ||
| try { | ||
| const fullPath = resolve(join(path$1, basename$1)); | ||
| const fullPath = resolve(join(path, basename)); | ||
| entry = { | ||
| path: relative(this._root, fullPath), | ||
| fullPath, | ||
| basename: basename$1 | ||
| basename | ||
| }; | ||
@@ -236,16 +235,14 @@ entry[this._statsProp] = this._isDirent ? dirent : await this._stat(fullPath); | ||
| function readdirp(root, options = {}) { | ||
| let type$1 = options.entryType || options.type; | ||
| if (type$1 === "both") type$1 = EntryTypes.FILE_DIR_TYPE; | ||
| if (type$1) options.type = type$1; | ||
| let type = options.entryType || options.type; | ||
| if (type === "both") type = EntryTypes.FILE_DIR_TYPE; | ||
| if (type) options.type = type; | ||
| if (!root) throw new Error("readdirp: root argument is required. Usage: readdirp(root, options)"); | ||
| else if (typeof root !== "string") throw new TypeError("readdirp: root argument must be a string. Usage: readdirp(root, options)"); | ||
| else if (type$1 && !ALL_TYPES.includes(type$1)) throw new Error(`readdirp: Invalid type passed. Use one of ${ALL_TYPES.join(", ")}`); | ||
| else if (type && !ALL_TYPES.includes(type)) throw new Error(`readdirp: Invalid type passed. Use one of ${ALL_TYPES.join(", ")}`); | ||
| options.root = root; | ||
| return new ReaddirpStream(options); | ||
| } | ||
| //#endregion | ||
| //#region node_modules/.pnpm/chokidar@5.0.0/node_modules/chokidar/handler.js | ||
| const STR_DATA = "data"; | ||
| const STR_END = "end"; | ||
| const STR_CLOSE = "close"; | ||
@@ -578,10 +575,10 @@ const EMPTY_FN = () => {}; | ||
| */ | ||
| function createFsWatchInstance(path$1, options, listener, errHandler, emitRaw) { | ||
| function createFsWatchInstance(path, options, listener, errHandler, emitRaw) { | ||
| const handleEvent = (rawEvent, evPath) => { | ||
| listener(path$1); | ||
| emitRaw(rawEvent, evPath, { watchedPath: path$1 }); | ||
| if (evPath && path$1 !== evPath) fsWatchBroadcast(sp.resolve(path$1, evPath), KEY_LISTENERS, sp.join(path$1, evPath)); | ||
| listener(path); | ||
| emitRaw(rawEvent, evPath, { watchedPath: path }); | ||
| if (evPath && path !== evPath) fsWatchBroadcast(sp.resolve(path, evPath), KEY_LISTENERS, sp.join(path, evPath)); | ||
| }; | ||
| try { | ||
| return watch(path$1, { persistent: options.persistent }, handleEvent); | ||
| return watch(path, { persistent: options.persistent }, handleEvent); | ||
| } catch (error) { | ||
@@ -611,3 +608,3 @@ errHandler(error); | ||
| */ | ||
| const setFsWatchListener = (path$1, fullPath, options, handlers) => { | ||
| const setFsWatchListener = (path, fullPath, options, handlers) => { | ||
| const { listener, errHandler, rawEmitter } = handlers; | ||
@@ -617,3 +614,3 @@ let cont = FsWatchInstances.get(fullPath); | ||
| if (!options.persistent) { | ||
| watcher = createFsWatchInstance(path$1, options, listener, errHandler, rawEmitter); | ||
| watcher = createFsWatchInstance(path, options, listener, errHandler, rawEmitter); | ||
| if (!watcher) return; | ||
@@ -627,3 +624,3 @@ return watcher.close.bind(watcher); | ||
| } else { | ||
| watcher = createFsWatchInstance(path$1, options, fsWatchBroadcast.bind(null, fullPath, KEY_LISTENERS), errHandler, fsWatchBroadcast.bind(null, fullPath, KEY_RAW)); | ||
| watcher = createFsWatchInstance(path, options, fsWatchBroadcast.bind(null, fullPath, KEY_LISTENERS), errHandler, fsWatchBroadcast.bind(null, fullPath, KEY_RAW)); | ||
| if (!watcher) return; | ||
@@ -634,3 +631,3 @@ watcher.on(EV.ERROR, async (error) => { | ||
| if (isWindows && error.code === "EPERM") try { | ||
| await (await open(path$1, "r")).close(); | ||
| await (await open(path, "r")).close(); | ||
| broadcastErr(error); | ||
@@ -671,3 +668,3 @@ } catch (err) {} | ||
| */ | ||
| const setFsWatchFileListener = (path$1, fullPath, options, handlers) => { | ||
| const setFsWatchFileListener = (path, fullPath, options, handlers) => { | ||
| const { listener, rawEmitter } = handlers; | ||
@@ -689,4 +686,4 @@ let cont = FsWatchFileInstances.get(fullPath); | ||
| watcher: watchFile(fullPath, options, (curr, prev) => { | ||
| foreach(cont.rawEmitters, (rawEmitter$1) => { | ||
| rawEmitter$1(EV.CHANGE, fullPath, { | ||
| foreach(cont.rawEmitters, (rawEmitter) => { | ||
| rawEmitter(EV.CHANGE, fullPath, { | ||
| curr, | ||
@@ -697,3 +694,3 @@ prev | ||
| const currmtime = curr.mtimeMs; | ||
| if (curr.size !== prev.size || currmtime > prev.mtimeMs || currmtime === 0) foreach(cont.listeners, (listener$1) => listener$1(path$1, curr)); | ||
| if (curr.size !== prev.size || currmtime > prev.mtimeMs || currmtime === 0) foreach(cont.listeners, (listener) => listener(path, curr)); | ||
| }) | ||
@@ -730,8 +727,8 @@ }; | ||
| */ | ||
| _watchWithNodeFs(path$1, listener) { | ||
| _watchWithNodeFs(path, listener) { | ||
| const opts = this.fsw.options; | ||
| const directory = sp.dirname(path$1); | ||
| const basename$1 = sp.basename(path$1); | ||
| this.fsw._getWatchedDir(directory).add(basename$1); | ||
| const absolutePath = sp.resolve(path$1); | ||
| const directory = sp.dirname(path); | ||
| const basename = sp.basename(path); | ||
| this.fsw._getWatchedDir(directory).add(basename); | ||
| const absolutePath = sp.resolve(path); | ||
| const options = { persistent: opts.persistent }; | ||
@@ -741,8 +738,8 @@ if (!listener) listener = EMPTY_FN; | ||
| if (opts.usePolling) { | ||
| options.interval = opts.interval !== opts.binaryInterval && isBinaryPath(basename$1) ? opts.binaryInterval : opts.interval; | ||
| closer = setFsWatchFileListener(path$1, absolutePath, options, { | ||
| options.interval = opts.interval !== opts.binaryInterval && isBinaryPath(basename) ? opts.binaryInterval : opts.interval; | ||
| closer = setFsWatchFileListener(path, absolutePath, options, { | ||
| listener, | ||
| rawEmitter: this.fsw._emitRaw | ||
| }); | ||
| } else closer = setFsWatchListener(path$1, absolutePath, options, { | ||
| } else closer = setFsWatchListener(path, absolutePath, options, { | ||
| listener, | ||
@@ -760,25 +757,25 @@ errHandler: this._boundHandleError, | ||
| if (this.fsw.closed) return; | ||
| const dirname$1 = sp.dirname(file); | ||
| const basename$1 = sp.basename(file); | ||
| const parent = this.fsw._getWatchedDir(dirname$1); | ||
| const dirname = sp.dirname(file); | ||
| const basename = sp.basename(file); | ||
| const parent = this.fsw._getWatchedDir(dirname); | ||
| let prevStats = stats; | ||
| if (parent.has(basename$1)) return; | ||
| const listener = async (path$1, newStats) => { | ||
| if (parent.has(basename)) return; | ||
| const listener = async (path, newStats) => { | ||
| if (!this.fsw._throttle(THROTTLE_MODE_WATCH, file, 5)) return; | ||
| if (!newStats || newStats.mtimeMs === 0) try { | ||
| const newStats$1 = await stat$1(file); | ||
| const newStats = await stat$1(file); | ||
| if (this.fsw.closed) return; | ||
| const at = newStats$1.atimeMs; | ||
| const mt = newStats$1.mtimeMs; | ||
| if (!at || at <= mt || mt !== prevStats.mtimeMs) this.fsw._emit(EV.CHANGE, file, newStats$1); | ||
| if ((isMacos || isLinux || isFreeBSD) && prevStats.ino !== newStats$1.ino) { | ||
| this.fsw._closeFile(path$1); | ||
| prevStats = newStats$1; | ||
| const closer$1 = this._watchWithNodeFs(file, listener); | ||
| if (closer$1) this.fsw._addPathCloser(path$1, closer$1); | ||
| } else prevStats = newStats$1; | ||
| const at = newStats.atimeMs; | ||
| const mt = newStats.mtimeMs; | ||
| if (!at || at <= mt || mt !== prevStats.mtimeMs) this.fsw._emit(EV.CHANGE, file, newStats); | ||
| if ((isMacos || isLinux || isFreeBSD) && prevStats.ino !== newStats.ino) { | ||
| this.fsw._closeFile(path); | ||
| prevStats = newStats; | ||
| const closer = this._watchWithNodeFs(file, listener); | ||
| if (closer) this.fsw._addPathCloser(path, closer); | ||
| } else prevStats = newStats; | ||
| } catch (error) { | ||
| this.fsw._remove(dirname$1, basename$1); | ||
| this.fsw._remove(dirname, basename); | ||
| } | ||
| else if (parent.has(basename$1)) { | ||
| else if (parent.has(basename)) { | ||
| const at = newStats.atimeMs; | ||
@@ -805,3 +802,3 @@ const mt = newStats.mtimeMs; | ||
| */ | ||
| async _handleSymlink(entry, directory, path$1, item) { | ||
| async _handleSymlink(entry, directory, path, item) { | ||
| if (this.fsw.closed) return; | ||
@@ -814,3 +811,3 @@ const full = entry.fullPath; | ||
| try { | ||
| linkPath = await realpath(path$1); | ||
| linkPath = await realpath(path); | ||
| } catch (e) { | ||
@@ -824,3 +821,3 @@ this.fsw._emitReady(); | ||
| this.fsw._symlinkPaths.set(full, linkPath); | ||
| this.fsw._emit(EV.CHANGE, path$1, entry.stats); | ||
| this.fsw._emit(EV.CHANGE, path, entry.stats); | ||
| } | ||
@@ -830,3 +827,3 @@ } else { | ||
| this.fsw._symlinkPaths.set(full, linkPath); | ||
| this.fsw._emit(EV.ADD, path$1, entry.stats); | ||
| this.fsw._emit(EV.ADD, path, entry.stats); | ||
| } | ||
@@ -857,5 +854,5 @@ this.fsw._emitReady(); | ||
| const item = entry.path; | ||
| let path$1 = sp.join(directory, item); | ||
| let path = sp.join(directory, item); | ||
| current.add(item); | ||
| if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path$1, item)) return; | ||
| if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path, item)) return; | ||
| if (this.fsw.closed) { | ||
@@ -867,9 +864,9 @@ stream = void 0; | ||
| this.fsw._incrReadyCount(); | ||
| path$1 = sp.join(dir, sp.relative(dir, path$1)); | ||
| this._addToNodeFs(path$1, initialAdd, wh, depth + 1); | ||
| path = sp.join(dir, sp.relative(dir, path)); | ||
| this._addToNodeFs(path, initialAdd, wh, depth + 1); | ||
| } | ||
| }).on(EV.ERROR, this._boundHandleError); | ||
| return new Promise((resolve$1, reject) => { | ||
| return new Promise((resolve, reject) => { | ||
| if (!stream) return reject(); | ||
| stream.once(STR_END, () => { | ||
| stream.once("end", () => { | ||
| if (this.fsw.closed) { | ||
@@ -880,3 +877,3 @@ stream = void 0; | ||
| const wasThrottled = throttler ? throttler.clear() : false; | ||
| resolve$1(void 0); | ||
| resolve(void 0); | ||
| previous.getChildren().filter((item) => { | ||
@@ -903,3 +900,3 @@ return item !== directory && !current.has(item); | ||
| */ | ||
| async _handleDir(dir, stats, initialAdd, depth, target, wh, realpath$1) { | ||
| async _handleDir(dir, stats, initialAdd, depth, target, wh, realpath) { | ||
| const parentDir = this.fsw._getWatchedDir(sp.dirname(dir)); | ||
@@ -913,3 +910,3 @@ const tracked = parentDir.has(sp.basename(dir)); | ||
| const oDepth = this.fsw.options.depth; | ||
| if ((oDepth == null || depth <= oDepth) && !this.fsw._symlinkPaths.has(realpath$1)) { | ||
| if ((oDepth == null || depth <= oDepth) && !this.fsw._symlinkPaths.has(realpath)) { | ||
| if (!target) { | ||
@@ -919,4 +916,4 @@ await this._handleRead(dir, initialAdd, wh, target, dir, depth, throttler); | ||
| } | ||
| closer = this._watchWithNodeFs(dir, (dirPath, stats$1) => { | ||
| if (stats$1 && stats$1.mtimeMs === 0) return; | ||
| closer = this._watchWithNodeFs(dir, (dirPath, stats) => { | ||
| if (stats && stats.mtimeMs === 0) return; | ||
| this._handleRead(dirPath, false, wh, target, dir, depth, throttler); | ||
@@ -936,9 +933,9 @@ }); | ||
| */ | ||
| async _addToNodeFs(path$1, initialAdd, priorWh, depth, target) { | ||
| async _addToNodeFs(path, initialAdd, priorWh, depth, target) { | ||
| const ready = this.fsw._emitReady; | ||
| if (this.fsw._isIgnored(path$1) || this.fsw.closed) { | ||
| if (this.fsw._isIgnored(path) || this.fsw.closed) { | ||
| ready(); | ||
| return false; | ||
| } | ||
| const wh = this.fsw._getWatchHelpers(path$1); | ||
| const wh = this.fsw._getWatchHelpers(path); | ||
| if (priorWh) { | ||
@@ -958,4 +955,4 @@ wh.filterPath = (entry) => priorWh.filterPath(entry); | ||
| if (stats.isDirectory()) { | ||
| const absPath = sp.resolve(path$1); | ||
| const targetPath = follow ? await realpath(path$1) : path$1; | ||
| const absPath = sp.resolve(path); | ||
| const targetPath = follow ? await realpath(path) : path; | ||
| if (this.fsw.closed) return; | ||
@@ -966,3 +963,3 @@ closer = await this._handleDir(wh.watchPath, stats, initialAdd, depth, target, wh, targetPath); | ||
| } else if (stats.isSymbolicLink()) { | ||
| const targetPath = follow ? await realpath(path$1) : path$1; | ||
| const targetPath = follow ? await realpath(path) : path; | ||
| if (this.fsw.closed) return; | ||
@@ -972,8 +969,8 @@ const parent = sp.dirname(wh.watchPath); | ||
| this.fsw._emit(EV.ADD, wh.watchPath, stats); | ||
| closer = await this._handleDir(parent, stats, initialAdd, depth, path$1, wh, targetPath); | ||
| closer = await this._handleDir(parent, stats, initialAdd, depth, path, wh, targetPath); | ||
| if (this.fsw.closed) return; | ||
| if (targetPath !== void 0) this.fsw._symlinkPaths.set(sp.resolve(path$1), targetPath); | ||
| if (targetPath !== void 0) this.fsw._symlinkPaths.set(sp.resolve(path), targetPath); | ||
| } else closer = this._handleFile(wh.watchPath, stats, initialAdd); | ||
| ready(); | ||
| if (closer) this.fsw._addPathCloser(path$1, closer); | ||
| if (closer) this.fsw._addPathCloser(path, closer); | ||
| return false; | ||
@@ -983,3 +980,3 @@ } catch (error) { | ||
| ready(); | ||
| return path$1; | ||
| return path; | ||
| } | ||
@@ -989,3 +986,2 @@ } | ||
| }; | ||
| //#endregion | ||
@@ -1020,5 +1016,5 @@ //#region node_modules/.pnpm/chokidar@5.0.0/node_modules/chokidar/index.js | ||
| if (matcher.recursive) { | ||
| const relative$1 = sp.relative(matcher.path, string); | ||
| if (!relative$1) return false; | ||
| return !relative$1.startsWith("..") && !sp.isAbsolute(relative$1); | ||
| const relative = sp.relative(matcher.path, string); | ||
| if (!relative) return false; | ||
| return !relative.startsWith("..") && !sp.isAbsolute(relative); | ||
| } | ||
@@ -1029,17 +1025,17 @@ return false; | ||
| } | ||
| function normalizePath(path$1) { | ||
| if (typeof path$1 !== "string") throw new Error("string expected"); | ||
| path$1 = sp.normalize(path$1); | ||
| path$1 = path$1.replace(/\\/g, "/"); | ||
| function normalizePath(path) { | ||
| if (typeof path !== "string") throw new Error("string expected"); | ||
| path = sp.normalize(path); | ||
| path = path.replace(/\\/g, "/"); | ||
| let prepend = false; | ||
| if (path$1.startsWith("//")) prepend = true; | ||
| path$1 = path$1.replace(DOUBLE_SLASH_RE, "/"); | ||
| if (prepend) path$1 = "/" + path$1; | ||
| return path$1; | ||
| if (path.startsWith("//")) prepend = true; | ||
| path = path.replace(DOUBLE_SLASH_RE, "/"); | ||
| if (prepend) path = "/" + path; | ||
| return path; | ||
| } | ||
| function matchPatterns(patterns, testString, stats) { | ||
| const path$1 = normalizePath(testString); | ||
| const path = normalizePath(testString); | ||
| for (let index = 0; index < patterns.length; index++) { | ||
| const pattern = patterns[index]; | ||
| if (pattern(path$1, stats)) return true; | ||
| if (pattern(path, stats)) return true; | ||
| } | ||
@@ -1051,4 +1047,4 @@ return false; | ||
| const patterns = arrify(matchers).map((matcher) => createPattern(matcher)); | ||
| if (testString == null) return (testString$1, stats) => { | ||
| return matchPatterns(patterns, testString$1, stats); | ||
| if (testString == null) return (testString, stats) => { | ||
| return matchPatterns(patterns, testString, stats); | ||
| }; | ||
@@ -1070,10 +1066,10 @@ return matchPatterns(patterns, testString); | ||
| }; | ||
| const normalizePathToUnix = (path$1) => toUnix(sp.normalize(toUnix(path$1))); | ||
| const normalizeIgnored = (cwd = "") => (path$1) => { | ||
| if (typeof path$1 === "string") return normalizePathToUnix(sp.isAbsolute(path$1) ? path$1 : sp.join(cwd, path$1)); | ||
| else return path$1; | ||
| const normalizePathToUnix = (path) => toUnix(sp.normalize(toUnix(path))); | ||
| const normalizeIgnored = (cwd = "") => (path) => { | ||
| if (typeof path === "string") return normalizePathToUnix(sp.isAbsolute(path) ? path : sp.join(cwd, path)); | ||
| else return path; | ||
| }; | ||
| const getAbsolutePath = (path$1, cwd) => { | ||
| if (sp.isAbsolute(path$1)) return path$1; | ||
| return sp.join(cwd, path$1); | ||
| const getAbsolutePath = (path, cwd) => { | ||
| if (sp.isAbsolute(path)) return path; | ||
| return sp.join(cwd, path); | ||
| }; | ||
@@ -1138,6 +1134,6 @@ const EMPTY_SET = Object.freeze(/* @__PURE__ */ new Set()); | ||
| statMethod; | ||
| constructor(path$1, follow, fsw) { | ||
| constructor(path, follow, fsw) { | ||
| this.fsw = fsw; | ||
| const watchPath = path$1; | ||
| this.path = path$1 = path$1.replace(REPLACER_RE, ""); | ||
| const watchPath = path; | ||
| this.path = path = path.replace(REPLACER_RE, ""); | ||
| this.watchPath = watchPath; | ||
@@ -1273,7 +1269,7 @@ this.fullWatchPath = sp.resolve(watchPath); | ||
| let paths = unifyPaths(paths_); | ||
| if (cwd) paths = paths.map((path$1) => { | ||
| return getAbsolutePath(path$1, cwd); | ||
| if (cwd) paths = paths.map((path) => { | ||
| return getAbsolutePath(path, cwd); | ||
| }); | ||
| paths.forEach((path$1) => { | ||
| this._removeIgnoredPath(path$1); | ||
| paths.forEach((path) => { | ||
| this._removeIgnoredPath(path); | ||
| }); | ||
@@ -1283,4 +1279,4 @@ this._userIgnored = void 0; | ||
| this._readyCount += paths.length; | ||
| Promise.all(paths.map(async (path$1) => { | ||
| const res = await this._nodeFsHandler._addToNodeFs(path$1, !_internal, void 0, 0, _origAdd); | ||
| Promise.all(paths.map(async (path) => { | ||
| const res = await this._nodeFsHandler._addToNodeFs(path, !_internal, void 0, 0, _origAdd); | ||
| if (res) this._emitReady(); | ||
@@ -1303,11 +1299,11 @@ return res; | ||
| const { cwd } = this.options; | ||
| paths.forEach((path$1) => { | ||
| if (!sp.isAbsolute(path$1) && !this._closers.has(path$1)) { | ||
| if (cwd) path$1 = sp.join(cwd, path$1); | ||
| path$1 = sp.resolve(path$1); | ||
| paths.forEach((path) => { | ||
| if (!sp.isAbsolute(path) && !this._closers.has(path)) { | ||
| if (cwd) path = sp.join(cwd, path); | ||
| path = sp.resolve(path); | ||
| } | ||
| this._closePath(path$1); | ||
| this._addIgnoredPath(path$1); | ||
| if (this._watched.has(path$1)) this._addIgnoredPath({ | ||
| path: path$1, | ||
| this._closePath(path); | ||
| this._addIgnoredPath(path); | ||
| if (this._watched.has(path)) this._addIgnoredPath({ | ||
| path, | ||
| recursive: true | ||
@@ -1368,12 +1364,12 @@ }); | ||
| */ | ||
| async _emit(event, path$1, stats) { | ||
| async _emit(event, path, stats) { | ||
| if (this.closed) return; | ||
| const opts = this.options; | ||
| if (isWindows) path$1 = sp.normalize(path$1); | ||
| if (opts.cwd) path$1 = sp.relative(opts.cwd, path$1); | ||
| const args = [path$1]; | ||
| if (isWindows) path = sp.normalize(path); | ||
| if (opts.cwd) path = sp.relative(opts.cwd, path); | ||
| const args = [path]; | ||
| if (stats != null) args.push(stats); | ||
| const awf = opts.awaitWriteFinish; | ||
| let pw; | ||
| if (awf && (pw = this._pendingWrites.get(path$1))) { | ||
| if (awf && (pw = this._pendingWrites.get(path))) { | ||
| pw.lastChange = /* @__PURE__ */ new Date(); | ||
@@ -1384,8 +1380,8 @@ return this; | ||
| if (event === EVENTS.UNLINK) { | ||
| this._pendingUnlinks.set(path$1, [event, ...args]); | ||
| this._pendingUnlinks.set(path, [event, ...args]); | ||
| setTimeout(() => { | ||
| this._pendingUnlinks.forEach((entry, path$2) => { | ||
| this._pendingUnlinks.forEach((entry, path) => { | ||
| this.emit(...entry); | ||
| this.emit(EVENTS.ALL, ...entry); | ||
| this._pendingUnlinks.delete(path$2); | ||
| this._pendingUnlinks.delete(path); | ||
| }); | ||
@@ -1395,9 +1391,9 @@ }, typeof opts.atomic === "number" ? opts.atomic : 100); | ||
| } | ||
| if (event === EVENTS.ADD && this._pendingUnlinks.has(path$1)) { | ||
| if (event === EVENTS.ADD && this._pendingUnlinks.has(path)) { | ||
| event = EVENTS.CHANGE; | ||
| this._pendingUnlinks.delete(path$1); | ||
| this._pendingUnlinks.delete(path); | ||
| } | ||
| } | ||
| if (awf && (event === EVENTS.ADD || event === EVENTS.CHANGE) && this._readyEmitted) { | ||
| const awfEmit = (err, stats$1) => { | ||
| const awfEmit = (err, stats) => { | ||
| if (err) { | ||
@@ -1407,22 +1403,22 @@ event = EVENTS.ERROR; | ||
| this.emitWithAll(event, args); | ||
| } else if (stats$1) { | ||
| if (args.length > 1) args[1] = stats$1; | ||
| else args.push(stats$1); | ||
| } else if (stats) { | ||
| if (args.length > 1) args[1] = stats; | ||
| else args.push(stats); | ||
| this.emitWithAll(event, args); | ||
| } | ||
| }; | ||
| this._awaitWriteFinish(path$1, awf.stabilityThreshold, event, awfEmit); | ||
| this._awaitWriteFinish(path, awf.stabilityThreshold, event, awfEmit); | ||
| return this; | ||
| } | ||
| if (event === EVENTS.CHANGE) { | ||
| if (!this._throttle(EVENTS.CHANGE, path$1, 50)) return this; | ||
| if (!this._throttle(EVENTS.CHANGE, path, 50)) return this; | ||
| } | ||
| if (opts.alwaysStat && stats === void 0 && (event === EVENTS.ADD || event === EVENTS.ADD_DIR || event === EVENTS.CHANGE)) { | ||
| const fullPath = opts.cwd ? sp.join(opts.cwd, path$1) : path$1; | ||
| let stats$1; | ||
| const fullPath = opts.cwd ? sp.join(opts.cwd, path) : path; | ||
| let stats; | ||
| try { | ||
| stats$1 = await stat$1(fullPath); | ||
| stats = await stat$1(fullPath); | ||
| } catch (err) {} | ||
| if (!stats$1 || this.closed) return; | ||
| args.push(stats$1); | ||
| if (!stats || this.closed) return; | ||
| args.push(stats); | ||
| } | ||
@@ -1448,7 +1444,7 @@ this.emitWithAll(event, args); | ||
| */ | ||
| _throttle(actionType, path$1, timeout) { | ||
| _throttle(actionType, path, timeout) { | ||
| if (!this._throttled.has(actionType)) this._throttled.set(actionType, /* @__PURE__ */ new Map()); | ||
| const action = this._throttled.get(actionType); | ||
| if (!action) throw new Error("invalid throttle"); | ||
| const actionPath = action.get(path$1); | ||
| const actionPath = action.get(path); | ||
| if (actionPath) { | ||
@@ -1460,5 +1456,5 @@ actionPath.count++; | ||
| const clear = () => { | ||
| const item = action.get(path$1); | ||
| const item = action.get(path); | ||
| const count = item ? item.count : 0; | ||
| action.delete(path$1); | ||
| action.delete(path); | ||
| clearTimeout(timeoutObject); | ||
@@ -1474,3 +1470,3 @@ if (item) clearTimeout(item.timeoutObject); | ||
| }; | ||
| action.set(path$1, thr); | ||
| action.set(path, thr); | ||
| return thr; | ||
@@ -1489,3 +1485,3 @@ } | ||
| */ | ||
| _awaitWriteFinish(path$1, threshold, event, awfEmit) { | ||
| _awaitWriteFinish(path, threshold, event, awfEmit) { | ||
| const awf = this.options.awaitWriteFinish; | ||
@@ -1495,4 +1491,4 @@ if (typeof awf !== "object") return; | ||
| let timeoutHandler; | ||
| let fullPath = path$1; | ||
| if (this.options.cwd && !sp.isAbsolute(path$1)) fullPath = sp.join(this.options.cwd, path$1); | ||
| let fullPath = path; | ||
| if (this.options.cwd && !sp.isAbsolute(path)) fullPath = sp.join(this.options.cwd, path); | ||
| const now = /* @__PURE__ */ new Date(); | ||
@@ -1502,10 +1498,10 @@ const writes = this._pendingWrites; | ||
| stat(fullPath, (err, curStat) => { | ||
| if (err || !writes.has(path$1)) { | ||
| if (err || !writes.has(path)) { | ||
| if (err && err.code !== "ENOENT") awfEmit(err); | ||
| return; | ||
| } | ||
| const now$1 = Number(/* @__PURE__ */ new Date()); | ||
| if (prevStat && curStat.size !== prevStat.size) writes.get(path$1).lastChange = now$1; | ||
| if (now$1 - writes.get(path$1).lastChange >= threshold) { | ||
| writes.delete(path$1); | ||
| const now = Number(/* @__PURE__ */ new Date()); | ||
| if (prevStat && curStat.size !== prevStat.size) writes.get(path).lastChange = now; | ||
| if (now - writes.get(path).lastChange >= threshold) { | ||
| writes.delete(path); | ||
| awfEmit(void 0, curStat); | ||
@@ -1515,7 +1511,7 @@ } else timeoutHandler = setTimeout(awaitWriteFinishFn, pollInterval, curStat); | ||
| } | ||
| if (!writes.has(path$1)) { | ||
| writes.set(path$1, { | ||
| if (!writes.has(path)) { | ||
| writes.set(path, { | ||
| lastChange: now, | ||
| cancelWait: () => { | ||
| writes.delete(path$1); | ||
| writes.delete(path); | ||
| clearTimeout(timeoutHandler); | ||
@@ -1531,4 +1527,4 @@ return event; | ||
| */ | ||
| _isIgnored(path$1, stats) { | ||
| if (this.options.atomic && DOT_RE.test(path$1)) return true; | ||
| _isIgnored(path, stats) { | ||
| if (this.options.atomic && DOT_RE.test(path)) return true; | ||
| if (!this._userIgnored) { | ||
@@ -1539,6 +1535,6 @@ const { cwd } = this.options; | ||
| } | ||
| return this._userIgnored(path$1, stats); | ||
| return this._userIgnored(path, stats); | ||
| } | ||
| _isntIgnored(path$1, stat$2) { | ||
| return !this._isIgnored(path$1, stat$2); | ||
| _isntIgnored(path, stat) { | ||
| return !this._isIgnored(path, stat); | ||
| } | ||
@@ -1549,4 +1545,4 @@ /** | ||
| */ | ||
| _getWatchHelpers(path$1) { | ||
| return new WatchHelper(path$1, this.options.followSymlinks, this); | ||
| _getWatchHelpers(path) { | ||
| return new WatchHelper(path, this.options.followSymlinks, this); | ||
| } | ||
@@ -1577,8 +1573,8 @@ /** | ||
| _remove(directory, item, isDirectory) { | ||
| const path$1 = sp.join(directory, item); | ||
| const fullPath = sp.resolve(path$1); | ||
| isDirectory = isDirectory != null ? isDirectory : this._watched.has(path$1) || this._watched.has(fullPath); | ||
| if (!this._throttle("remove", path$1, 100)) return; | ||
| const path = sp.join(directory, item); | ||
| const fullPath = sp.resolve(path); | ||
| isDirectory = isDirectory != null ? isDirectory : this._watched.has(path) || this._watched.has(fullPath); | ||
| if (!this._throttle("remove", path, 100)) return; | ||
| if (!isDirectory && this._watched.size === 1) this.add(directory, item, true); | ||
| this._getWatchedDir(path$1).getChildren().forEach((nested) => this._remove(path$1, nested)); | ||
| this._getWatchedDir(path).getChildren().forEach((nested) => this._remove(path, nested)); | ||
| const parent = this._getWatchedDir(directory); | ||
@@ -1588,12 +1584,12 @@ const wasTracked = parent.has(item); | ||
| if (this._symlinkPaths.has(fullPath)) this._symlinkPaths.delete(fullPath); | ||
| let relPath = path$1; | ||
| if (this.options.cwd) relPath = sp.relative(this.options.cwd, path$1); | ||
| let relPath = path; | ||
| if (this.options.cwd) relPath = sp.relative(this.options.cwd, path); | ||
| if (this.options.awaitWriteFinish && this._pendingWrites.has(relPath)) { | ||
| if (this._pendingWrites.get(relPath).cancelWait() === EVENTS.ADD) return; | ||
| } | ||
| this._watched.delete(path$1); | ||
| this._watched.delete(path); | ||
| this._watched.delete(fullPath); | ||
| const eventName = isDirectory ? EVENTS.UNLINK_DIR : EVENTS.UNLINK; | ||
| if (wasTracked && !this._isIgnored(path$1)) this._emit(eventName, path$1); | ||
| this._closePath(path$1); | ||
| if (wasTracked && !this._isIgnored(path)) this._emit(eventName, path); | ||
| this._closePath(path); | ||
| } | ||
@@ -1603,6 +1599,6 @@ /** | ||
| */ | ||
| _closePath(path$1) { | ||
| this._closeFile(path$1); | ||
| const dir = sp.dirname(path$1); | ||
| this._getWatchedDir(dir).remove(sp.basename(path$1)); | ||
| _closePath(path) { | ||
| this._closeFile(path); | ||
| const dir = sp.dirname(path); | ||
| this._getWatchedDir(dir).remove(sp.basename(path)); | ||
| } | ||
@@ -1612,14 +1608,14 @@ /** | ||
| */ | ||
| _closeFile(path$1) { | ||
| const closers = this._closers.get(path$1); | ||
| _closeFile(path) { | ||
| const closers = this._closers.get(path); | ||
| if (!closers) return; | ||
| closers.forEach((closer) => closer()); | ||
| this._closers.delete(path$1); | ||
| this._closers.delete(path); | ||
| } | ||
| _addPathCloser(path$1, closer) { | ||
| _addPathCloser(path, closer) { | ||
| if (!closer) return; | ||
| let list = this._closers.get(path$1); | ||
| let list = this._closers.get(path); | ||
| if (!list) { | ||
| list = []; | ||
| this._closers.set(path$1, list); | ||
| this._closers.set(path, list); | ||
| } | ||
@@ -1641,3 +1637,3 @@ list.push(closer); | ||
| }); | ||
| stream.once(STR_END, () => { | ||
| stream.once("end", () => { | ||
| if (stream) { | ||
@@ -1669,4 +1665,3 @@ this._streams.delete(stream); | ||
| }; | ||
| //#endregion | ||
| export { watch$1 as n, chokidar_exports as t }; | ||
| export { watch$1 as n, chokidar_exports as t }; |
| import { a as toDecodedMap, c as decodedMappings, i as setSourceContent, l as traceSegment, n as maybeAddSegment, o as toEncodedMap, r as setIgnore, s as TraceMap, t as GenMapping } from "./resolve-uri+gen-mapping.mjs"; | ||
| //#region node_modules/.pnpm/@jridgewell+remapping@2.3.5/node_modules/@jridgewell/remapping/dist/remapping.mjs | ||
@@ -115,4 +114,3 @@ var SOURCELESS_MAPPING = /* @__PURE__ */ SegmentObject("", -1, -1, "", null, false); | ||
| } | ||
| //#endregion | ||
| export { remapping as t }; | ||
| export { remapping as t }; |
| import { C as decode, w as encode } from "../_build/common.mjs"; | ||
| //#region node_modules/.pnpm/@jridgewell+resolve-uri@3.1.2/node_modules/@jridgewell/resolve-uri/dist/resolve-uri.mjs | ||
@@ -64,13 +63,13 @@ const schemeRegex = /^[\w+.-]+:\/\//; | ||
| if (isSchemeRelativeUrl(input)) { | ||
| const url$1 = parseAbsoluteUrl("http:" + input); | ||
| url$1.scheme = ""; | ||
| url$1.type = 6; | ||
| return url$1; | ||
| const url = parseAbsoluteUrl("http:" + input); | ||
| url.scheme = ""; | ||
| url.type = 6; | ||
| return url; | ||
| } | ||
| if (isAbsolutePath(input)) { | ||
| const url$1 = parseAbsoluteUrl("http://foo.com" + input); | ||
| url$1.scheme = ""; | ||
| url$1.host = ""; | ||
| url$1.type = 5; | ||
| return url$1; | ||
| const url = parseAbsoluteUrl("http://foo.com" + input); | ||
| url.scheme = ""; | ||
| url.host = ""; | ||
| url.type = 5; | ||
| return url; | ||
| } | ||
@@ -167,3 +166,2 @@ if (isFileUrl(input)) return parseFileUrl(input); | ||
| } | ||
| //#endregion | ||
@@ -253,4 +251,2 @@ //#region node_modules/.pnpm/@jridgewell+trace-mapping@0.3.31/node_modules/@jridgewell/trace-mapping/dist/trace-mapping.mjs | ||
| } | ||
| var LEAST_UPPER_BOUND = -1; | ||
| var GREATEST_LOWER_BOUND = 1; | ||
| var TraceMap = class { | ||
@@ -269,4 +265,4 @@ constructor(map, mapUrl) { | ||
| this.ignoreList = parsed.ignoreList || parsed.x_google_ignoreList || void 0; | ||
| const resolve$1 = resolver(mapUrl, sourceRoot); | ||
| this.resolvedSources = sources.map(resolve$1); | ||
| const resolve = resolver(mapUrl, sourceRoot); | ||
| this.resolvedSources = sources.map(resolve); | ||
| const { mappings } = parsed; | ||
@@ -297,3 +293,3 @@ if (typeof mappings === "string") { | ||
| const segments = decoded[line]; | ||
| const index = traceSegmentInternal(segments, cast$1(map)._decodedMemo, line, column, GREATEST_LOWER_BOUND); | ||
| const index = traceSegmentInternal(segments, cast$1(map)._decodedMemo, line, column, 1); | ||
| return index === -1 ? null : segments[index]; | ||
@@ -303,8 +299,7 @@ } | ||
| let index = memoizedBinarySearch(segments, column, memo, line); | ||
| if (found) index = (bias === LEAST_UPPER_BOUND ? upperBound : lowerBound)(segments, column, index); | ||
| else if (bias === LEAST_UPPER_BOUND) index++; | ||
| if (found) index = (bias === -1 ? upperBound : lowerBound)(segments, column, index); | ||
| else if (bias === -1) index++; | ||
| if (index === -1 || index === segments.length) return -1; | ||
| return index; | ||
| } | ||
| //#endregion | ||
@@ -452,4 +447,3 @@ //#region node_modules/.pnpm/@jridgewell+gen-mapping@0.3.13/node_modules/@jridgewell/gen-mapping/dist/gen-mapping.mjs | ||
| } | ||
| //#endregion | ||
| export { toDecodedMap as a, decodedMappings as c, setSourceContent as i, traceSegment as l, maybeAddSegment as n, toEncodedMap as o, setIgnore as r, TraceMap as s, GenMapping as t }; | ||
| export { toDecodedMap as a, decodedMappings as c, setSourceContent as i, traceSegment as l, maybeAddSegment as n, toEncodedMap as o, setIgnore as r, TraceMap as s, GenMapping as t }; |
@@ -1,2 +0,2 @@ | ||
| //#region node_modules/.pnpm/rou3@0.7.12/node_modules/rou3/dist/index.d.mts | ||
| //#region node_modules/.pnpm/rou3@0.8.1/node_modules/rou3/dist/index.d.mts | ||
| interface RouterContext<T = unknown> { | ||
@@ -21,5 +21,6 @@ root: Node<T>; | ||
| //#endregion | ||
| //#region node_modules/.pnpm/rou3@0.7.12/node_modules/rou3/dist/compiler.d.mts | ||
| //#region node_modules/.pnpm/rou3@0.8.1/node_modules/rou3/dist/compiler.d.mts | ||
| interface RouterCompilerOptions<T = any> { | ||
| matchAll?: boolean; | ||
| normalize?: boolean; | ||
| serialize?: (data: T) => string; | ||
@@ -26,0 +27,0 @@ } |
+225
-51
@@ -1,2 +0,2 @@ | ||
| //#region node_modules/.pnpm/rou3@0.7.12/node_modules/rou3/dist/index.mjs | ||
| //#region node_modules/.pnpm/rou3@0.8.1/node_modules/rou3/dist/index.mjs | ||
| const NullProtoObj = /* @__PURE__ */ (() => { | ||
@@ -15,2 +15,139 @@ const e = function() {}; | ||
| } | ||
| function expandGroupDelimiters(path) { | ||
| let i = 0; | ||
| let depth = 0; | ||
| for (; i < path.length; i++) { | ||
| const c = path.charCodeAt(i); | ||
| if (c === 92) { | ||
| i++; | ||
| continue; | ||
| } | ||
| if (c === 40) { | ||
| depth++; | ||
| continue; | ||
| } | ||
| if (c === 41 && depth > 0) { | ||
| depth--; | ||
| continue; | ||
| } | ||
| if (c === 123 && depth === 0) break; | ||
| } | ||
| if (i >= path.length) return; | ||
| let j = i + 1; | ||
| depth = 0; | ||
| for (; j < path.length; j++) { | ||
| const c = path.charCodeAt(j); | ||
| if (c === 92) { | ||
| j++; | ||
| continue; | ||
| } | ||
| if (c === 40) { | ||
| depth++; | ||
| continue; | ||
| } | ||
| if (c === 41 && depth > 0) { | ||
| depth--; | ||
| continue; | ||
| } | ||
| if (c === 125 && depth === 0) break; | ||
| } | ||
| if (j >= path.length) return; | ||
| const mod = path[j + 1]; | ||
| const hasMod = mod === "?" || mod === "+" || mod === "*"; | ||
| const pre = path.slice(0, i); | ||
| const body = path.slice(i + 1, j); | ||
| const suf = path.slice(j + (hasMod ? 2 : 1)); | ||
| if (!hasMod) return [pre + body + suf]; | ||
| if (mod === "?") return [pre + body + suf, pre + suf]; | ||
| if (body.includes("/")) throw new Error("unsupported group repetition across segments"); | ||
| return [`${pre}(?:${body})${mod}${suf}`]; | ||
| } | ||
| const UNNAMED_GROUP_PREFIX$1 = "__rou3_unnamed_"; | ||
| const _unnamedGroupPrefixLength = 15; | ||
| function hasSegmentWildcard(segment) { | ||
| let depth = 0; | ||
| for (let i = 0; i < segment.length; i++) { | ||
| const ch = segment.charCodeAt(i); | ||
| if (ch === 92) { | ||
| i++; | ||
| continue; | ||
| } | ||
| if (ch === 40) { | ||
| depth++; | ||
| continue; | ||
| } | ||
| if (ch === 41 && depth > 0) { | ||
| depth--; | ||
| continue; | ||
| } | ||
| if (ch === 42 && depth === 0) return true; | ||
| } | ||
| return false; | ||
| } | ||
| function replaceSegmentWildcards(segment, unnamedStart, toGroupKey = toUnnamedGroupKey) { | ||
| let depth = 0; | ||
| let nextIndex = unnamedStart; | ||
| let replaced = ""; | ||
| for (let i = 0; i < segment.length; i++) { | ||
| const ch = segment.charCodeAt(i); | ||
| if (ch === 92) { | ||
| replaced += segment[i]; | ||
| if (i + 1 < segment.length) replaced += segment[++i]; | ||
| continue; | ||
| } | ||
| if (ch === 40) { | ||
| depth++; | ||
| replaced += segment[i]; | ||
| continue; | ||
| } | ||
| if (ch === 41 && depth > 0) { | ||
| depth--; | ||
| replaced += segment[i]; | ||
| continue; | ||
| } | ||
| if (ch === 42 && depth === 0) { | ||
| replaced += `(?<${toGroupKey(nextIndex++)}>[^/]*)`; | ||
| continue; | ||
| } | ||
| replaced += segment[i]; | ||
| } | ||
| return [replaced, nextIndex]; | ||
| } | ||
| function toUnnamedGroupKey(index) { | ||
| return `${UNNAMED_GROUP_PREFIX$1}${index}`; | ||
| } | ||
| function normalizeUnnamedGroupKey(key) { | ||
| return key.startsWith("__rou3_unnamed_") ? key.slice(_unnamedGroupPrefixLength) : key; | ||
| } | ||
| function encodeEscapes(path) { | ||
| return path.replace(/\\:/g, "�A").replace(/\\\(/g, "�B").replace(/\\\)/g, "�C").replace(/\\\{/g, "�D").replace(/\\\}/g, "�E"); | ||
| } | ||
| function decodeEscaped(segment) { | ||
| return segment.replace(/\uFFFD([A-E])/g, (_, c) => c === "A" ? ":" : c === "B" ? "(" : c === "C" ? ")" : c === "D" ? "{" : "}"); | ||
| } | ||
| function expandModifiers(segments) { | ||
| for (let i = 0; i < segments.length; i++) { | ||
| const m = segments[i].match(/^(.*:[\w-]+(?:\([^)]*\))?)([?+*])$/); | ||
| if (!m) continue; | ||
| const pre = segments.slice(0, i); | ||
| const suf = segments.slice(i + 1); | ||
| if (m[2] === "?") return ["/" + pre.concat(m[1]).concat(suf).join("/"), "/" + pre.concat(suf).join("/")]; | ||
| const name = m[1].match(/:([\w-]+)/)?.[1] || "_"; | ||
| const wc = "/" + [ | ||
| ...pre, | ||
| `**:${name}`, | ||
| ...suf | ||
| ].join("/"); | ||
| const without = "/" + [...pre, ...suf].join("/"); | ||
| return m[2] === "+" ? [wc] : [wc, without]; | ||
| } | ||
| } | ||
| function normalizePath(path) { | ||
| if (!path.includes("/.")) return path; | ||
| const r = []; | ||
| for (const s of path.split("/")) if (s === ".") continue; | ||
| else if (s === ".." && r.length > 1) r.pop(); | ||
| else r.push(s); | ||
| return r.join("/") || "/"; | ||
| } | ||
| function splitPath(path) { | ||
@@ -27,3 +164,3 @@ const [_, ...s] = path.split("/"); | ||
| const match = segment.match(name); | ||
| if (match) for (const key in match.groups) params[key] = match.groups[key]; | ||
| if (match) for (const key in match.groups) params[normalizeUnnamedGroupKey(key)] = match.groups[key]; | ||
| } | ||
@@ -39,4 +176,14 @@ } | ||
| if (path.charCodeAt(0) !== 47) path = `/${path}`; | ||
| path = path.replace(/\\:/g, "%3A"); | ||
| const groupExpanded = expandGroupDelimiters(path); | ||
| if (groupExpanded) { | ||
| for (const expandedPath of groupExpanded) addRoute(ctx, method, expandedPath, data); | ||
| return; | ||
| } | ||
| path = encodeEscapes(path); | ||
| const segments = splitPath(path); | ||
| const expanded = expandModifiers(segments); | ||
| if (expanded) { | ||
| for (const p of expanded) addRoute(ctx, method, p, data); | ||
| return; | ||
| } | ||
| let node = ctx.root; | ||
@@ -58,3 +205,3 @@ let _unnamedParamIndex = 0; | ||
| } | ||
| if (segment === "*" || segment.includes(":")) { | ||
| if (segment === "*" || segment.includes(":") || segment.includes("(") || hasSegmentWildcard(segment)) { | ||
| if (!node.param) node.param = { key: "*" }; | ||
@@ -64,7 +211,8 @@ node = node.param; | ||
| i, | ||
| `_${_unnamedParamIndex++}`, | ||
| String(_unnamedParamIndex++), | ||
| true | ||
| ]); | ||
| else if (segment.includes(":", 1)) { | ||
| const regexp = getParamRegexp(segment); | ||
| else if (segment.includes(":", 1) || segment.includes("(") || hasSegmentWildcard(segment) || !/^:[\w-]+$/.test(segment)) { | ||
| const [regexp, nextIndex] = getParamRegexp(segment, _unnamedParamIndex); | ||
| _unnamedParamIndex = nextIndex; | ||
| paramsRegexp[i] = regexp; | ||
@@ -86,2 +234,3 @@ node.hasRegexParam = true; | ||
| else if (segment === "\\*\\*") segment = segments[i] = "**"; | ||
| segment = segments[i] = decodeEscaped(segment); | ||
| const child = node.static?.[segment]; | ||
@@ -106,5 +255,22 @@ if (child) node = child; | ||
| } | ||
| function getParamRegexp(segment) { | ||
| const regex = segment.replace(/:(\w+)/g, (_, id) => `(?<${id}>[^/]+)`).replace(/\./g, "\\."); | ||
| return /* @__PURE__ */ new RegExp(`^${regex}$`); | ||
| function getParamRegexp(segment, unnamedStart = 0) { | ||
| let _i = unnamedStart; | ||
| let _s = "", _d = 0; | ||
| for (let j = 0; j < segment.length; j++) { | ||
| const c = segment.charCodeAt(j); | ||
| if (c === 40) _d++; | ||
| else if (c === 41 && _d > 0) _d--; | ||
| else if (c === 92 && _d === 0 && j + 1 < segment.length) { | ||
| const n = segment[j + 1]; | ||
| if (n !== ":" && n !== "(" && n !== "*" && n !== "\\") { | ||
| _s += "" + n; | ||
| j++; | ||
| continue; | ||
| } | ||
| } | ||
| _s += segment[j]; | ||
| } | ||
| [_s, _i] = replaceSegmentWildcards(_s, _i); | ||
| const regex = _s.replace(/:([\w-]+)(?:\(([^)]*)\))?/g, (_, id, p) => `(?<${id}>${p || "[^/]+"})`).replace(/\((?![?<])/g, () => `(?<${toUnnamedGroupKey(_i++)}>`).replace(/\./g, "\\.").replace(/\uFFFE(.)/g, (_, c) => /[.*+?^${}()|[\]\\]/.test(c) ? `\\${c}` : c); | ||
| return [new RegExp(`^${regex}$`), _i]; | ||
| } | ||
@@ -115,2 +281,3 @@ /** | ||
| function findRoute(ctx, method = "", path, opts) { | ||
| if (opts?.normalize) path = normalizePath(path); | ||
| if (path.charCodeAt(path.length - 1) === 47) path = path.slice(0, -1); | ||
@@ -123,3 +290,3 @@ const staticNode = ctx.static[path]; | ||
| const segments = splitPath(path); | ||
| const match = _lookupTree(ctx, ctx.root, method, segments, 0)?.[0]; | ||
| const match = _lookupTree(ctx.root, method, segments, 0)?.[0]; | ||
| if (match === void 0) return; | ||
@@ -132,3 +299,3 @@ if (opts?.params === false) return match; | ||
| } | ||
| function _lookupTree(ctx, node, method, segments, index) { | ||
| function _lookupTree(node, method, segments, index) { | ||
| if (index === segments.length) { | ||
@@ -159,3 +326,3 @@ if (node.methods) { | ||
| if (staticChild) { | ||
| const match = _lookupTree(ctx, staticChild, method, segments, index + 1); | ||
| const match = _lookupTree(staticChild, method, segments, index + 1); | ||
| if (match) return match; | ||
@@ -165,3 +332,3 @@ } | ||
| if (node.param) { | ||
| const match = _lookupTree(ctx, node.param, method, segments, index + 1); | ||
| const match = _lookupTree(node.param, method, segments, index + 1); | ||
| if (match) { | ||
@@ -181,5 +348,6 @@ if (node.param.hasRegexParam) { | ||
| function findAllRoutes(ctx, method = "", path, opts) { | ||
| if (opts?.normalize) path = normalizePath(path); | ||
| if (path.charCodeAt(path.length - 1) === 47) path = path.slice(0, -1); | ||
| const segments = splitPath(path); | ||
| const matches = _findAll(ctx, ctx.root, method, segments, 0); | ||
| const matches = _findAll(ctx.root, method, segments, 0); | ||
| if (opts?.params === false) return matches; | ||
@@ -193,3 +361,3 @@ return matches.map((m) => { | ||
| } | ||
| function _findAll(ctx, node, method, segments, index, matches = []) { | ||
| function _findAll(node, method, segments, index, matches = []) { | ||
| const segment = segments[index]; | ||
@@ -201,3 +369,3 @@ if (node.wildcard && node.wildcard.methods) { | ||
| if (node.param) { | ||
| _findAll(ctx, node.param, method, segments, index + 1, matches); | ||
| _findAll(node.param, method, segments, index + 1, matches); | ||
| if (index === segments.length && node.param.methods) { | ||
@@ -212,3 +380,3 @@ const match = node.param.methods[method] || node.param.methods[""]; | ||
| const staticChild = node.static?.[segment]; | ||
| if (staticChild) _findAll(ctx, staticChild, method, segments, index + 1, matches); | ||
| if (staticChild) _findAll(staticChild, method, segments, index + 1, matches); | ||
| if (index === segments.length && node.methods) { | ||
@@ -220,5 +388,5 @@ const match = node.methods[method] || node.methods[""]; | ||
| } | ||
| //#endregion | ||
| //#region node_modules/.pnpm/rou3@0.7.12/node_modules/rou3/dist/compiler.mjs | ||
| //#region node_modules/.pnpm/rou3@0.8.1/node_modules/rou3/dist/compiler.mjs | ||
| const UNNAMED_GROUP_PREFIX = "__rou3_unnamed_"; | ||
| /** | ||
@@ -250,14 +418,18 @@ * Compile the router instance into a compact runnable code. | ||
| let code = ""; | ||
| const staticNodes = /* @__PURE__ */ new Set(); | ||
| for (const key in ctx.router.static) { | ||
| const node = ctx.router.static[key]; | ||
| if (node?.methods) { | ||
| staticNodes.add(node); | ||
| code += `if(p===${JSON.stringify(key.replace(/\/$/, "") || "/")}){${compileMethodMatch(ctx, node.methods, [], -1)}}`; | ||
| { | ||
| let hasIf = false; | ||
| for (const key in ctx.router.static) { | ||
| const node = ctx.router.static[key]; | ||
| if (node?.methods) { | ||
| code += `${hasIf ? "else " : ""}if(p===${JSON.stringify(key.replace(/\/$/, "") || "/")}){${compileMethodMatch(ctx, node.methods, [], -1)}}`; | ||
| hasIf = true; | ||
| } | ||
| } | ||
| } | ||
| const match = compileNode(ctx, ctx.router.root, [], 0, staticNodes); | ||
| if (match) code += `let s=p.split("/"),l=s.length-1;${match}`; | ||
| const match = compileNode(ctx, ctx.router.root, [], 1); | ||
| if (match) code += `let s=p.split("/"),l=s.length;${match}`; | ||
| if (!code) return ctx.opts?.matchAll ? `return [];` : ""; | ||
| return `${ctx.opts?.matchAll ? `let r=[];` : ""}if(p.charCodeAt(p.length-1)===47)p=p.slice(0,-1)||"/";${code}${ctx.opts?.matchAll ? "return r;" : ""}`; | ||
| const normalizeHelper = code.includes("_normalizeGroups(") ? `const _prefix=${JSON.stringify(UNNAMED_GROUP_PREFIX)},_prefixLen=15;const _normalizeGroups=(g)=>{if(!g)return g;for(const k in g){if(k.startsWith(_prefix)){g[k.slice(_prefixLen)]=g[k];delete g[k]}}return g;};` : ""; | ||
| const normalizePathHelper = ctx.opts?.normalize ? `if(p.includes("/.")){let _r=[];for(let _v of p.split("/")){if(_v===".")continue;_v===".."&&_r.length>1?_r.pop():_r.push(_v)}p=_r.join("/")||"/"}` : ""; | ||
| return `${ctx.opts?.matchAll ? `let r=[];` : ""}${normalizeHelper}${normalizePathHelper}if(p.charCodeAt(p.length-1)===47)p=p.slice(0,-1)||"/";${code}${ctx.opts?.matchAll ? "return r;" : ""}`; | ||
| } | ||
@@ -268,3 +440,3 @@ function compileMethodMatch(ctx, methods, params, currentIdx) { | ||
| const matchers = methods[key]; | ||
| if (matchers && matchers?.length > 0) { | ||
| if (matchers && matchers.length > 0) { | ||
| if (key !== "") code += `if(m==="${key}")${matchers.length > 1 ? "{" : ""}`; | ||
@@ -283,3 +455,3 @@ const _matchers = matchers.map((m) => compileFinalMatch(ctx, m, currentIdx, params)).sort((a, b) => b.weight - a.weight); | ||
| if (paramsMap && paramsMap.length > 0) { | ||
| if (!paramsMap[paramsMap.length - 1][2] && currentIdx !== -1) conditions.push(`l>=${currentIdx}`); | ||
| if (!paramsMap[paramsMap.length - 1][2] && currentIdx !== -1) conditions.push(`l>${currentIdx}`); | ||
| for (let i = 0; i < paramsRegexp.length; i++) { | ||
@@ -293,3 +465,3 @@ const regexp = paramsRegexp[i]; | ||
| const map = paramsMap[i]; | ||
| ret += typeof map[1] === "string" ? `${JSON.stringify(map[1])}:${params[i]},` : `...(${map[1].toString()}.exec(${params[i]}))?.groups,`; | ||
| ret += typeof map[1] === "string" ? `${JSON.stringify(map[1])}:${params[i]},` : `..._normalizeGroups((${map[1].toString()}.exec(${params[i]}))?.groups),`; | ||
| } | ||
@@ -303,26 +475,29 @@ ret += "}"; | ||
| } | ||
| function compileNode(ctx, node, params, startIdx, staticNodes) { | ||
| let code = ""; | ||
| if (node.methods && !staticNodes.has(node)) { | ||
| const match = compileMethodMatch(ctx, node.methods, params, node.key === "*" ? startIdx : -1); | ||
| function compileNode(ctx, node, params, currentIdx) { | ||
| const hasLastOptionalParam = node.key === "*"; | ||
| let code = "", hasIf = false; | ||
| if (node.methods && params.length > 0) { | ||
| const match = compileMethodMatch(ctx, node.methods, params, hasLastOptionalParam ? currentIdx - 1 : -1); | ||
| if (match) { | ||
| const hasLastOptionalParam = node.key === "*"; | ||
| code += `if(l===${startIdx}${hasLastOptionalParam ? `||l===${startIdx - 1}` : ""}){${match}}`; | ||
| code += `if(l===${currentIdx}${hasLastOptionalParam ? `||l===${currentIdx - 1}` : ""}){${match}}`; | ||
| hasIf = true; | ||
| } | ||
| } | ||
| if (node.static) for (const key in node.static) { | ||
| const match = compileNode(ctx, node.static[key], params, startIdx + 1, staticNodes); | ||
| if (match) code += `if(s[${startIdx + 1}]===${JSON.stringify(key)}){${match}}`; | ||
| if (node.static) { | ||
| let staticCode = ""; | ||
| const notNeedBoundCheck = hasIf; | ||
| for (const key in node.static) { | ||
| const match = compileNode(ctx, node.static[key], params, currentIdx + 1); | ||
| if (match) { | ||
| staticCode += `${hasIf ? "else " : ""}if(s[${currentIdx}]===${JSON.stringify(key)}){${match}}`; | ||
| hasIf = true; | ||
| } | ||
| } | ||
| if (staticCode) code += notNeedBoundCheck ? staticCode : `if(l>${currentIdx}){${staticCode}}`; | ||
| } | ||
| if (node.param) { | ||
| const match = compileNode(ctx, node.param, [...params, `s[${startIdx + 1}]`], startIdx + 1, staticNodes); | ||
| if (match) code += match; | ||
| } | ||
| if (node.param) code += compileNode(ctx, node.param, params.concat(`s[${currentIdx}]`), currentIdx + 1); | ||
| if (node.wildcard) { | ||
| const { wildcard } = node; | ||
| if (wildcard.static || wildcard.param || wildcard.wildcard) throw new Error("Compiler mode does not support patterns after wildcard"); | ||
| if (wildcard.methods) { | ||
| const match = compileMethodMatch(ctx, wildcard.methods, [...params, `s.slice(${startIdx + 1}).join('/')`], startIdx); | ||
| if (match) code += match; | ||
| } | ||
| if (wildcard.methods) code += compileMethodMatch(ctx, wildcard.methods, params.concat(`s.slice(${currentIdx}).join('/')`), currentIdx); | ||
| } | ||
@@ -342,4 +517,3 @@ return code; | ||
| } | ||
| //#endregion | ||
| export { findRoute as a, findAllRoutes as i, addRoute as n, createRouter as r, compileRouterToString as t }; | ||
| export { findRoute as a, findAllRoutes as i, addRoute as n, createRouter as r, compileRouterToString as t }; |
@@ -1,4 +0,12 @@ | ||
| //#region node_modules/.pnpm/std-env@3.10.0/node_modules/std-env/dist/index.d.ts | ||
| type ProviderName = "" | "appveyor" | "aws_amplify" | "azure_pipelines" | "azure_static" | "appcircle" | "bamboo" | "bitbucket" | "bitrise" | "buddy" | "buildkite" | "circle" | "cirrus" | "cloudflare_pages" | "cloudflare_workers" | "codebuild" | "codefresh" | "drone" | "drone" | "dsari" | "github_actions" | "gitlab" | "gocd" | "layerci" | "hudson" | "jenkins" | "magnum" | "netlify" | "nevercode" | "render" | "sail" | "semaphore" | "screwdriver" | "shippable" | "solano" | "strider" | "teamcity" | "travis" | "vercel" | "appcenter" | "codesandbox" | "stackblitz" | "stormkit" | "cleavr" | "zeabur" | "codesphere" | "railway" | "deno-deploy" | "firebase_app_hosting"; | ||
| //#region node_modules/.pnpm/std-env@4.0.0/node_modules/std-env/dist/index.d.mts | ||
| //#endregion | ||
| //#region src/providers.d.ts | ||
| /** | ||
| * Represents the name of a CI/CD or Deployment provider. | ||
| */ | ||
| type ProviderName = (string & {}) | "appveyor" | "aws_amplify" | "azure_pipelines" | "azure_static" | "appcircle" | "bamboo" | "bitbucket" | "bitrise" | "buddy" | "buildkite" | "circle" | "cirrus" | "cloudflare_pages" | "cloudflare_workers" | "google_cloudrun" | "google_cloudrun_job" | "codebuild" | "codefresh" | "drone" | "drone" | "dsari" | "github_actions" | "gitlab" | "gocd" | "layerci" | "hudson" | "jenkins" | "magnum" | "netlify" | "nevercode" | "render" | "sail" | "semaphore" | "screwdriver" | "shippable" | "solano" | "strider" | "teamcity" | "travis" | "vercel" | "appcenter" | "codesandbox" | "stackblitz" | "stormkit" | "cleavr" | "zeabur" | "codesphere" | "railway" | "deno-deploy" | "firebase_app_hosting"; | ||
| /** | ||
| * Provides information about a CI/CD or Deployment provider, including its name and possibly other metadata. | ||
| */ | ||
| //#endregion | ||
| export { ProviderName as t }; |
+21
-40
| import fs, { promises } from "node:fs"; | ||
| import path from "node:path"; | ||
| import { createRequire } from "module"; | ||
| //#region node_modules/.pnpm/tsconfck@3.1.6_typescript@5.9.3/node_modules/tsconfck/src/util.js | ||
@@ -31,9 +30,9 @@ const POSIX_SEP_RE = new RegExp("\\" + path.posix.sep, "g"); | ||
| function makePromise() { | ||
| let resolve$1, reject; | ||
| let resolve, reject; | ||
| return { | ||
| promise: new Promise((res, rej) => { | ||
| resolve$1 = res; | ||
| resolve = res; | ||
| reject = rej; | ||
| }), | ||
| resolve: resolve$1, | ||
| resolve, | ||
| reject | ||
@@ -51,4 +50,4 @@ }; | ||
| if (cache && (cache.hasParseResult(tsconfig) || cache.hasParseResult(filename))) return tsconfig; | ||
| return promises.stat(tsconfig).then((stat$1) => { | ||
| if (stat$1.isFile() || stat$1.isFIFO()) return tsconfig; | ||
| return promises.stat(tsconfig).then((stat) => { | ||
| if (stat.isFile() || stat.isFIFO()) return tsconfig; | ||
| else throw new Error(`${filename} exists but is not a regular file.`); | ||
@@ -229,3 +228,2 @@ }); | ||
| } | ||
| //#endregion | ||
@@ -246,7 +244,7 @@ //#region node_modules/.pnpm/tsconfck@3.1.6_typescript@5.9.3/node_modules/tsconfck/src/find.js | ||
| if (cache?.hasConfigPath(dir, configName)) return cache.getConfigPath(dir, configName); | ||
| const { promise, resolve: resolve$1, reject } = makePromise(); | ||
| const { promise, resolve, reject } = makePromise(); | ||
| if (options?.root && !path.isAbsolute(options.root)) options.root = path.resolve(options.root); | ||
| findUp(dir, { | ||
| promise, | ||
| resolve: resolve$1, | ||
| resolve, | ||
| reject | ||
@@ -262,3 +260,3 @@ }, options); | ||
| */ | ||
| function findUp(dir, { resolve: resolve$1, reject, promise }, options) { | ||
| function findUp(dir, { resolve, reject, promise }, options) { | ||
| const { cache, root, configName } = options ?? {}; | ||
@@ -273,15 +271,15 @@ if (cache) if (cache.hasConfigPath(dir, configName)) { | ||
| } | ||
| if (cached?.then) cached.then(resolve$1).catch(reject); | ||
| else resolve$1(cached); | ||
| if (cached?.then) cached.then(resolve).catch(reject); | ||
| else resolve(cached); | ||
| } else cache.setConfigPath(dir, promise, configName); | ||
| const tsconfig = path.join(dir, options?.configName ?? "tsconfig.json"); | ||
| fs.stat(tsconfig, (err, stats) => { | ||
| if (stats && (stats.isFile() || stats.isFIFO())) resolve$1(tsconfig); | ||
| if (stats && (stats.isFile() || stats.isFIFO())) resolve(tsconfig); | ||
| else if (err?.code !== "ENOENT") reject(err); | ||
| else { | ||
| let parent; | ||
| if (root === dir || (parent = path.dirname(dir)) === dir) resolve$1(null); | ||
| if (root === dir || (parent = path.dirname(dir)) === dir) resolve(null); | ||
| else findUp(parent, { | ||
| promise, | ||
| resolve: resolve$1, | ||
| resolve, | ||
| reject | ||
@@ -292,17 +290,4 @@ }, options); | ||
| } | ||
| path.sep; | ||
| //#endregion | ||
| //#region node_modules/.pnpm/tsconfck@3.1.6_typescript@5.9.3/node_modules/tsconfck/src/find-all.js | ||
| /** | ||
| * @typedef WalkState | ||
| * @interface | ||
| * @property {string[]} files - files | ||
| * @property {number} calls - number of ongoing calls | ||
| * @property {(dir: string)=>boolean} skip - function to skip dirs | ||
| * @property {boolean} err - error flag | ||
| * @property {string[]} configNames - config file names | ||
| */ | ||
| const sep$1 = path.sep; | ||
| //#endregion | ||
| //#region node_modules/.pnpm/tsconfck@3.1.6_typescript@5.9.3/node_modules/tsconfck/src/to-json.js | ||
@@ -434,3 +419,2 @@ /** | ||
| } | ||
| //#endregion | ||
@@ -454,3 +438,3 @@ //#region node_modules/.pnpm/tsconfck@3.1.6_typescript@5.9.3/node_modules/tsconfck/src/parse.js | ||
| if (cache?.hasParseResult(filename)) return getParsedDeep(filename, cache, options); | ||
| const { resolve: resolve$1, reject, promise } = makePromise(); | ||
| const { resolve, reject, promise } = makePromise(); | ||
| cache?.setParseResult(filename, promise, true); | ||
@@ -460,3 +444,3 @@ try { | ||
| if (!tsconfigFile) { | ||
| resolve$1(not_found_result); | ||
| resolve(not_found_result); | ||
| return promise; | ||
@@ -471,3 +455,3 @@ } | ||
| replaceTokens(result); | ||
| resolve$1(resolveSolutionTSConfig(filename, result)); | ||
| resolve(resolveSolutionTSConfig(filename, result)); | ||
| } catch (e) { | ||
@@ -623,3 +607,3 @@ reject(e); | ||
| const relativePath = native2posix(path.relative(path.dirname(extending.tsconfigFile), path.dirname(extended.tsconfigFile))); | ||
| for (const key of Object.keys(extendedConfig).filter((key$1) => EXTENDABLE_KEYS.includes(key$1))) if (key === "compilerOptions") { | ||
| for (const key of Object.keys(extendedConfig).filter((key) => EXTENDABLE_KEYS.includes(key))) if (key === "compilerOptions") { | ||
| if (!extendingConfig.compilerOptions) extendingConfig.compilerOptions = {}; | ||
@@ -728,3 +712,2 @@ for (const option of Object.keys(extendedConfig.compilerOptions)) { | ||
| } | ||
| //#endregion | ||
@@ -738,3 +721,2 @@ //#region node_modules/.pnpm/tsconfck@3.1.6_typescript@5.9.3/node_modules/tsconfck/src/parse-native.js | ||
| } TSDiagnosticError */ | ||
| //#endregion | ||
@@ -823,4 +805,4 @@ //#region node_modules/.pnpm/tsconfck@3.1.6_typescript@5.9.3/node_modules/tsconfck/src/cache.js | ||
| this.#configPaths.set(key, configPath); | ||
| configPath.then((path$1) => { | ||
| if (this.#configPaths.get(key) === configPath) this.#configPaths.set(key, path$1); | ||
| configPath.then((path) => { | ||
| if (this.#configPaths.get(key) === configPath) this.#configPaths.set(key, path); | ||
| }).catch((e) => { | ||
@@ -845,4 +827,3 @@ if (this.#configPaths.get(key) === configPath) this.#configPaths.set(key, e); | ||
| }; | ||
| //#endregion | ||
| export { parse as n, TSConfckCache as t }; | ||
| export { parse as n, TSConfckCache as t }; |
| //#region node_modules/.pnpm/ultrahtml@1.6.0/node_modules/ultrahtml/dist/index.js | ||
| var S = Symbol("Fragment"), D = new Set([ | ||
| var D = new Set([ | ||
| "area", | ||
@@ -122,18 +122,7 @@ "base", | ||
| } | ||
| }, O = class { | ||
| constructor(t) { | ||
| this.callback = t; | ||
| } | ||
| visit(t, i, r) { | ||
| if (this.callback(t, i, r), Array.isArray(t.children)) for (let n = 0; n < t.children.length; n++) { | ||
| let a = t.children[n]; | ||
| this.visit(a, t, n); | ||
| } | ||
| } | ||
| }, p = Symbol("HTMLString"), M = Symbol("AttrString"), f = Symbol("RenderFn"); | ||
| }; | ||
| function z(e, t) { | ||
| return new T(t).visit(e); | ||
| } | ||
| //#endregion | ||
| export { z as n, P as t }; | ||
| export { z as n, P as t }; |
| import { t as MagicString } from "./magic-string.mjs"; | ||
| import { t as ESMExport } from "./mlly.mjs"; | ||
| import "vite"; | ||
| import "rolldown"; | ||
| import "rollup"; | ||
| import "webpack"; | ||
| import "@farmfe/core"; | ||
| import "@rspack/core"; | ||
| import "unloader"; | ||
@@ -30,3 +23,3 @@ //#region node_modules/.pnpm/unplugin-utils@0.3.1/node_modules/unplugin-utils/dist/index.d.ts | ||
| //#endregion | ||
| //#region node_modules/.pnpm/unimport@5.6.0/node_modules/unimport/dist/shared/unimport.C0UbTDPO.d.mts | ||
| //#region node_modules/.pnpm/unimport@6.0.1/node_modules/unimport/dist/shared/unimport.DCeIjgcw.d.mts | ||
| declare const builtinPresets: { | ||
@@ -191,3 +184,7 @@ '@vue/composition-api': InlinePreset; | ||
| interface UnimportMeta { | ||
| injectionUsage: Record<string, InjectionUsageRecord>; | ||
| injectionsUsageMap: Map<string, InjectionUsageRecord>; | ||
| /** | ||
| * @deprecated use `injectionsUsageMap` instead | ||
| */ | ||
| get injectionUsage(): Record<string, InjectionUsageRecord>; | ||
| } | ||
@@ -414,3 +411,3 @@ interface AddonsOptions { | ||
| //#endregion | ||
| //#region node_modules/.pnpm/unimport@5.6.0/node_modules/unimport/dist/unplugin.d.mts | ||
| //#region node_modules/.pnpm/unimport@6.0.1/node_modules/unimport/dist/unplugin.d.mts | ||
| interface UnimportPluginOptions extends UnimportOptions { | ||
@@ -417,0 +414,0 @@ include: FilterPattern; |
+389
-432
@@ -1,7 +0,6 @@ | ||
| import "./_common.mjs"; | ||
| import { $ as resolveModulePath, G as findFile, I as prettyPath$1, J as readGitConfig, K as findNearestFile, N as glob, R as writeFile$2, U as p, V as a, X as h, Y as readPackageJSON, at as join$1, ct as resolve$1, nt as dirname$1, st as relative$1, z as K } from "./_build/common.mjs"; | ||
| import { i as gr, n as Q } from "./_libs/confbox.mjs"; | ||
| import { B as f, F as prettyPath$1, G as findNearestFile, H as v, J as readPackageJSON, L as writeFile$2, M as glob, R, W as findFile, Y as i, Z as a, at as dirname$1, ct as join$1, dt as resolve$1, et as r, nt as resolveModulePath, q as readGitConfig, ut as relative$1 } from "./_build/common.mjs"; | ||
| import { r as resolveCompatibilityDatesFromEnv, t as formatCompatibilityDate } from "./_libs/compatx.mjs"; | ||
| import { t as importDep } from "./_chunks/utils.mjs"; | ||
| import { builtinModules } from "node:module"; | ||
| import consola$1 from "consola"; | ||
| import consola from "consola"; | ||
| import { existsSync, promises } from "node:fs"; | ||
@@ -11,7 +10,6 @@ import fsp, { readFile, writeFile } from "node:fs/promises"; | ||
| import { defu } from "defu"; | ||
| import { presetsDir, runtimeDir, version } from "nitro/meta"; | ||
| import { hasProtocol, joinURL, withLeadingSlash, withTrailingSlash, withoutLeadingSlash } from "ufo"; | ||
| import { presetsDir, runtimeDir, version } from "nitro/meta"; | ||
| import { colors } from "consola/utils"; | ||
| import { kebabCase } from "scule"; | ||
| //#region src/presets/_utils/preset.ts | ||
@@ -25,6 +23,3 @@ function defineNitroPreset(preset, meta) { | ||
| } | ||
| //#endregion | ||
| //#region src/presets/_nitro/base-worker.ts | ||
| const baseWorker = defineNitroPreset({ | ||
| var base_worker_default = [defineNitroPreset({ | ||
| entry: null, | ||
@@ -39,8 +34,4 @@ node: false, | ||
| inlineDynamicImports: true | ||
| }, { name: "base-worker" }); | ||
| var base_worker_default = [baseWorker]; | ||
| //#endregion | ||
| //#region src/presets/_nitro/nitro-dev.ts | ||
| const nitroDev = defineNitroPreset({ | ||
| }, { name: "base-worker" })]; | ||
| var nitro_dev_default = [defineNitroPreset({ | ||
| entry: "./_nitro/runtime/nitro-dev", | ||
@@ -64,8 +55,4 @@ output: { | ||
| dev: true | ||
| }); | ||
| var nitro_dev_default = [nitroDev]; | ||
| //#endregion | ||
| //#region src/presets/_nitro/nitro-prerender.ts | ||
| const nitroPrerender = defineNitroPreset({ | ||
| })]; | ||
| var nitro_prerender_default = [defineNitroPreset({ | ||
| entry: "./_nitro/runtime/nitro-prerenderer", | ||
@@ -75,8 +62,6 @@ serveStatic: true, | ||
| externals: { noTrace: true } | ||
| }, { name: "nitro-prerender" }); | ||
| var nitro_prerender_default = [nitroPrerender]; | ||
| }, { name: "nitro-prerender" })]; | ||
| //#endregion | ||
| //#region src/presets/_nitro/preset.ts | ||
| var preset_default$26 = [ | ||
| var preset_default$27 = [ | ||
| ...base_worker_default, | ||
@@ -86,50 +71,39 @@ ...nitro_dev_default, | ||
| ]; | ||
| //#endregion | ||
| //#region src/presets/_static/preset.ts | ||
| const _static = defineNitroPreset({ | ||
| static: true, | ||
| output: { | ||
| dir: "{{ rootDir }}/.output", | ||
| publicDir: "{{ output.dir }}/public" | ||
| }, | ||
| prerender: { crawlLinks: true }, | ||
| commands: { preview: "npx serve ./public" } | ||
| }, { | ||
| name: "static", | ||
| static: true | ||
| }); | ||
| const githubPages = defineNitroPreset({ | ||
| extends: "static", | ||
| commands: { deploy: "npx gh-pages --dotfiles -d ./public" }, | ||
| prerender: { routes: ["/", "/404.html"] }, | ||
| hooks: { async compiled(nitro) { | ||
| await fsp.writeFile(join$1(nitro.options.output.publicDir, ".nojekyll"), ""); | ||
| } } | ||
| }, { | ||
| name: "github-pages", | ||
| static: true | ||
| }); | ||
| const gitlabPages = defineNitroPreset({ | ||
| extends: "static", | ||
| prerender: { routes: ["/", "/404.html"] } | ||
| }, { | ||
| name: "gitlab-pages", | ||
| static: true | ||
| }); | ||
| var preset_default$25 = [ | ||
| _static, | ||
| githubPages, | ||
| gitlabPages | ||
| var preset_default$26 = [ | ||
| defineNitroPreset({ | ||
| static: true, | ||
| output: { | ||
| dir: "{{ rootDir }}/.output", | ||
| publicDir: "{{ output.dir }}/public" | ||
| }, | ||
| prerender: { crawlLinks: true }, | ||
| commands: { preview: "npx serve ./public" } | ||
| }, { | ||
| name: "static", | ||
| static: true | ||
| }), | ||
| defineNitroPreset({ | ||
| extends: "static", | ||
| commands: { deploy: "npx gh-pages --dotfiles -d ./public" }, | ||
| prerender: { routes: ["/", "/404.html"] }, | ||
| hooks: { async compiled(nitro) { | ||
| await fsp.writeFile(join$1(nitro.options.output.publicDir, ".nojekyll"), ""); | ||
| } } | ||
| }, { | ||
| name: "github-pages", | ||
| static: true | ||
| }), | ||
| defineNitroPreset({ | ||
| extends: "static", | ||
| prerender: { routes: ["/", "/404.html"] } | ||
| }, { | ||
| name: "gitlab-pages", | ||
| static: true | ||
| }) | ||
| ]; | ||
| //#endregion | ||
| //#region src/presets/alwaysdata/preset.ts | ||
| const alwaysdata = defineNitroPreset({ | ||
| var preset_default$25 = [defineNitroPreset({ | ||
| extends: "node-server", | ||
| serveStatic: true, | ||
| commands: { deploy: "rsync -rRt --info=progress2 ./ [account]@ssh-[account].alwaysdata.net:www/my-app" } | ||
| }, { name: "alwaysdata" }); | ||
| var preset_default$24 = [alwaysdata]; | ||
| }, { name: "alwaysdata" })]; | ||
| //#endregion | ||
@@ -142,5 +116,5 @@ //#region src/presets/aws-amplify/utils.ts | ||
| if (nitro.options.awsAmplify?.imageOptimization && !nitro.options.static) { | ||
| const { path: path$1, cacheControl } = nitro.options.awsAmplify?.imageOptimization || {}; | ||
| if (path$1) routes.push({ | ||
| path: path$1, | ||
| const { path, cacheControl } = nitro.options.awsAmplify?.imageOptimization || {}; | ||
| if (path) routes.push({ | ||
| path, | ||
| target: { | ||
@@ -198,6 +172,3 @@ kind: "ImageOptimization", | ||
| } | ||
| //#endregion | ||
| //#region src/presets/aws-amplify/preset.ts | ||
| const awsAmplify = defineNitroPreset({ | ||
| var preset_default$24 = [defineNitroPreset({ | ||
| entry: "./aws-amplify/runtime/aws-amplify", | ||
@@ -218,8 +189,4 @@ manifest: { deploymentId: process.env.AWS_JOB_ID }, | ||
| stdName: "aws_amplify" | ||
| }); | ||
| var preset_default$23 = [awsAmplify]; | ||
| //#endregion | ||
| //#region src/presets/aws-lambda/preset.ts | ||
| const awsLambda = defineNitroPreset({ | ||
| })]; | ||
| var preset_default$23 = [defineNitroPreset({ | ||
| entry: "./aws-lambda/runtime/aws-lambda", | ||
@@ -230,10 +197,8 @@ awsLambda: { streaming: false }, | ||
| } } | ||
| }, { name: "aws-lambda" }); | ||
| var preset_default$22 = [awsLambda]; | ||
| }, { name: "aws-lambda" })]; | ||
| //#endregion | ||
| //#region src/presets/_utils/fs.ts | ||
| function prettyPath(p$1, highlight = true) { | ||
| p$1 = relative$1(process.cwd(), p$1); | ||
| return highlight ? colors.cyan(p$1) : p$1; | ||
| function prettyPath(p, highlight = true) { | ||
| p = relative$1(process.cwd(), p); | ||
| return highlight ? colors.cyan(p) : p; | ||
| } | ||
@@ -243,5 +208,4 @@ async function writeFile$1(file, contents, log = false) { | ||
| await fsp.writeFile(file, contents, typeof contents === "string" ? "utf8" : void 0); | ||
| if (log) consola$1.info("Generated", prettyPath(file)); | ||
| if (log) consola.info("Generated", prettyPath(file)); | ||
| } | ||
| //#endregion | ||
@@ -335,6 +299,3 @@ //#region src/presets/azure/utils.ts | ||
| } | ||
| //#endregion | ||
| //#region src/presets/azure/preset.ts | ||
| const azureSWA = defineNitroPreset({ | ||
| var preset_default$22 = [defineNitroPreset({ | ||
| entry: "./azure/runtime/azure-swa", | ||
@@ -352,23 +313,10 @@ output: { | ||
| stdName: "azure_static" | ||
| }); | ||
| var preset_default$21 = [azureSWA]; | ||
| //#endregion | ||
| //#region src/presets/bun/preset.ts | ||
| const bun = defineNitroPreset({ | ||
| })]; | ||
| var preset_default$21 = [defineNitroPreset({ | ||
| entry: "./bun/runtime/bun", | ||
| serveStatic: true, | ||
| exportConditions: [ | ||
| "bun", | ||
| "node", | ||
| "import", | ||
| "default" | ||
| ], | ||
| exportConditions: ["bun"], | ||
| commands: { preview: "bun run ./server/index.mjs" } | ||
| }, { name: "bun" }); | ||
| var preset_default$20 = [bun]; | ||
| //#endregion | ||
| //#region src/presets/cleavr/preset.ts | ||
| const cleavr = defineNitroPreset({ | ||
| }, { name: "bun" })]; | ||
| var preset_default$20 = [defineNitroPreset({ | ||
| extends: "node-server", | ||
@@ -379,5 +327,3 @@ serveStatic: true | ||
| stdName: "cleavr" | ||
| }); | ||
| var preset_default$19 = [cleavr]; | ||
| })]; | ||
| //#endregion | ||
@@ -436,3 +382,2 @@ //#region src/presets/cloudflare/unenv/node-compat.ts | ||
| ]; | ||
| //#endregion | ||
@@ -461,3 +406,2 @@ //#region src/presets/cloudflare/unenv/preset.ts | ||
| }; | ||
| //#endregion | ||
@@ -491,3 +435,3 @@ //#region src/presets/cloudflare/utils.ts | ||
| "nitro.json", | ||
| ...routes.exclude.map((path$1) => withoutLeadingSlash(path$1.replace(/\/\*$/, "/**"))) | ||
| ...routes.exclude.map((path) => withoutLeadingSlash(path.replace(/\/\*$/, "/**"))) | ||
| ] | ||
@@ -499,4 +443,4 @@ }); | ||
| } | ||
| function comparePaths(a$1, b) { | ||
| return a$1.split("/").length - b.split("/").length || a$1.localeCompare(b); | ||
| function comparePaths(a, b) { | ||
| return a.split("/").length - b.split("/").length || a.localeCompare(b); | ||
| } | ||
@@ -506,5 +450,5 @@ async function writeCFHeaders(nitro, outdir) { | ||
| const contents = []; | ||
| const rules = Object.entries(nitro.options.routeRules).sort((a$1, b) => b[0].split(/\/(?!\*)/).length - a$1[0].split(/\/(?!\*)/).length); | ||
| for (const [path$1, routeRules] of rules.filter(([_, routeRules$1]) => routeRules$1.headers)) { | ||
| const headers = [joinURL(nitro.options.baseURL, path$1.replace("/**", "/*")), ...Object.entries({ ...routeRules.headers }).map(([header, value]) => ` ${header}: ${value}`)].join("\n"); | ||
| const rules = Object.entries(nitro.options.routeRules).sort((a, b) => b[0].split(/\/(?!\*)/).length - a[0].split(/\/(?!\*)/).length); | ||
| for (const [path, routeRules] of rules.filter(([_, routeRules]) => routeRules.headers)) { | ||
| const headers = [joinURL(nitro.options.baseURL, path.replace("/**", "/*")), ...Object.entries({ ...routeRules.headers }).map(([header, value]) => ` ${header}: ${value}`)].join("\n"); | ||
| contents.push(headers); | ||
@@ -526,4 +470,4 @@ } | ||
| const contents = [existsSync(join$1(nitro.options.output.publicDir, "404.html")) ? `${joinURL(nitro.options.baseURL, "/*")} ${joinURL(nitro.options.baseURL, "/404.html")} 404` : ""]; | ||
| const rules = Object.entries(nitro.options.routeRules).sort((a$1, b) => a$1[0].split(/\/(?!\*)/).length - b[0].split(/\/(?!\*)/).length); | ||
| for (const [key, routeRules] of rules.filter(([_, routeRules$1]) => routeRules$1.redirect)) { | ||
| const rules = Object.entries(nitro.options.routeRules).sort((a, b) => a[0].split(/\/(?!\*)/).length - b[0].split(/\/(?!\*)/).length); | ||
| for (const [key, routeRules] of rules.filter(([_, routeRules]) => routeRules.redirect)) { | ||
| const code = routeRules.redirect.status; | ||
@@ -552,5 +496,5 @@ const from = joinURL(nitro.options.baseURL, key.replace("/**", "/*")); | ||
| const extensionParsers = { | ||
| ".json": h, | ||
| ".jsonc": h, | ||
| ".toml": Q | ||
| ".json": r, | ||
| ".jsonc": r, | ||
| ".toml": i | ||
| }; | ||
@@ -605,2 +549,8 @@ async function readWranglerConfig(nitro) { | ||
| } | ||
| if (nitro.options.experimental.tasks && Object.keys(nitro.options.scheduledTasks || {}).length > 0 && cfTarget !== "pages") { | ||
| const schedules = Object.keys(nitro.options.scheduledTasks); | ||
| wranglerConfig.triggers = defu(wranglerConfig.triggers, { crons: [] }); | ||
| const existingCrons = new Set(wranglerConfig.triggers.crons); | ||
| for (const schedule of schedules) if (!existingCrons.has(schedule)) wranglerConfig.triggers.crons.push(schedule); | ||
| } | ||
| await writeFile$1(wranglerConfigPath, JSON.stringify(wranglerConfig, null, 2), true); | ||
@@ -616,3 +566,2 @@ const configPath = join$1(nitro.options.rootDir, ".wrangler/deploy/config.json"); | ||
| } | ||
| //#endregion | ||
@@ -661,3 +610,2 @@ //#region src/presets/cloudflare/dev.ts | ||
| } | ||
| //#endregion | ||
@@ -692,3 +640,2 @@ //#region src/presets/cloudflare/entry-exports.ts | ||
| } | ||
| //#endregion | ||
@@ -769,53 +716,50 @@ //#region src/presets/cloudflare/preset.ts | ||
| }); | ||
| const cloudflareModule = defineNitroPreset({ | ||
| extends: "base-worker", | ||
| entry: "./cloudflare/runtime/cloudflare-module", | ||
| output: { publicDir: "{{ output.dir }}/public/{{ baseURL }}" }, | ||
| exportConditions: ["workerd"], | ||
| minify: false, | ||
| commands: { | ||
| preview: "npx wrangler --cwd ./ dev", | ||
| deploy: "npx wrangler --cwd ./ deploy" | ||
| }, | ||
| rollupConfig: { output: { | ||
| format: "esm", | ||
| exports: "named", | ||
| inlineDynamicImports: false | ||
| } }, | ||
| wasm: { | ||
| lazy: false, | ||
| esmImport: true | ||
| }, | ||
| hooks: { | ||
| "build:before": async (nitro) => { | ||
| nitro.options.unenv.push(unenvCfExternals); | ||
| await enableNodeCompat(nitro); | ||
| await setupEntryExports(nitro); | ||
| var preset_default$19 = [ | ||
| cloudflarePages, | ||
| cloudflarePagesStatic, | ||
| defineNitroPreset({ | ||
| extends: "base-worker", | ||
| entry: "./cloudflare/runtime/cloudflare-module", | ||
| output: { publicDir: "{{ output.dir }}/public/{{ baseURL }}" }, | ||
| exportConditions: ["workerd"], | ||
| minify: false, | ||
| commands: { | ||
| preview: "npx wrangler --cwd ./ dev", | ||
| deploy: "npx wrangler --cwd ./ deploy" | ||
| }, | ||
| async compiled(nitro) { | ||
| await writeWranglerConfig(nitro, "module"); | ||
| await writeCFHeaders(nitro, "public"); | ||
| await writeFile$1(resolve$1(nitro.options.output.dir, "package.json"), JSON.stringify({ | ||
| private: true, | ||
| main: "./server/index.mjs" | ||
| }, null, 2)); | ||
| await writeFile$1(resolve$1(nitro.options.output.dir, "package-lock.json"), JSON.stringify({ lockfileVersion: 1 }, null, 2)); | ||
| rollupConfig: { output: { | ||
| format: "esm", | ||
| exports: "named", | ||
| inlineDynamicImports: false | ||
| } }, | ||
| wasm: { | ||
| lazy: false, | ||
| esmImport: true | ||
| }, | ||
| hooks: { | ||
| "build:before": async (nitro) => { | ||
| nitro.options.unenv.push(unenvCfExternals); | ||
| await enableNodeCompat(nitro); | ||
| await setupEntryExports(nitro); | ||
| }, | ||
| async compiled(nitro) { | ||
| await writeWranglerConfig(nitro, "module"); | ||
| await writeCFHeaders(nitro, "public"); | ||
| await writeFile$1(resolve$1(nitro.options.output.dir, "package.json"), JSON.stringify({ | ||
| private: true, | ||
| main: "./server/index.mjs" | ||
| }, null, 2)); | ||
| await writeFile$1(resolve$1(nitro.options.output.dir, "package-lock.json"), JSON.stringify({ lockfileVersion: 1 }, null, 2)); | ||
| } | ||
| } | ||
| } | ||
| }, { | ||
| name: "cloudflare-module", | ||
| stdName: "cloudflare_workers" | ||
| }); | ||
| const cloudflareDurable = defineNitroPreset({ | ||
| extends: "cloudflare-module", | ||
| entry: "./cloudflare/runtime/cloudflare-durable" | ||
| }, { name: "cloudflare-durable" }); | ||
| var preset_default$18 = [ | ||
| cloudflarePages, | ||
| cloudflarePagesStatic, | ||
| cloudflareModule, | ||
| cloudflareDurable, | ||
| }, { | ||
| name: "cloudflare-module", | ||
| stdName: "cloudflare_workers" | ||
| }), | ||
| defineNitroPreset({ | ||
| extends: "cloudflare-module", | ||
| entry: "./cloudflare/runtime/cloudflare-durable" | ||
| }, { name: "cloudflare-durable" }), | ||
| cloudflareDev | ||
| ]; | ||
| //#endregion | ||
@@ -892,3 +836,2 @@ //#region src/presets/deno/unenv/node-compat.ts | ||
| ]; | ||
| //#endregion | ||
@@ -908,6 +851,3 @@ //#region src/presets/deno/unenv/preset.ts | ||
| }; | ||
| //#endregion | ||
| //#region src/presets/deno/preset.ts | ||
| const denoDeploy = defineNitroPreset({ | ||
| var preset_default$18 = [defineNitroPreset({ | ||
| entry: "./deno/runtime/deno-deploy", | ||
@@ -920,3 +860,3 @@ manifest: { deploymentId: process.env.DENO_DEPLOYMENT_ID }, | ||
| preview: "", | ||
| deploy: "cd ./ && deployctl deploy --project=<project_name> server/index.ts" | ||
| deploy: "cd ./ && deno run -A jsr:@deno/deployctl deploy server/index.ts" | ||
| }, | ||
@@ -933,4 +873,3 @@ unenv: unenvDeno, | ||
| } | ||
| }, { name: "deno-deploy" }); | ||
| const denoServer = defineNitroPreset({ | ||
| }, { name: "deno-deploy" }), defineNitroPreset({ | ||
| entry: "./deno/runtime/deno-server", | ||
@@ -950,16 +889,8 @@ serveStatic: true, | ||
| name: "deno-server" | ||
| }); | ||
| var preset_default$17 = [denoDeploy, denoServer]; | ||
| //#endregion | ||
| //#region src/presets/digitalocean/preset.ts | ||
| const digitalOcean = defineNitroPreset({ | ||
| })]; | ||
| var preset_default$17 = [defineNitroPreset({ | ||
| extends: "node-server", | ||
| serveStatic: true | ||
| }, { name: "digital-ocean" }); | ||
| var preset_default$16 = [digitalOcean]; | ||
| //#endregion | ||
| //#region src/presets/firebase/preset.ts | ||
| const firebaseAppHosting = defineNitroPreset({ | ||
| }, { name: "digital-ocean" })]; | ||
| var preset_default$16 = [defineNitroPreset({ | ||
| extends: "node-server", | ||
@@ -969,3 +900,3 @@ serveStatic: true, | ||
| const serverEntry = join$1(nitro.options.output.serverDir, "index.mjs"); | ||
| await writeFile$1(join$1(nitro.options.rootDir, ".apphosting/bundle.yaml"), gr({ | ||
| await writeFile$1(join$1(nitro.options.rootDir, ".apphosting/bundle.yaml"), a({ | ||
| version: "v1", | ||
@@ -988,26 +919,12 @@ runConfig: { | ||
| stdName: "firebase_app_hosting" | ||
| }); | ||
| var preset_default$15 = [firebaseAppHosting]; | ||
| //#endregion | ||
| //#region src/presets/flightcontrol/preset.ts | ||
| const flightControl = defineNitroPreset({ | ||
| })]; | ||
| var preset_default$15 = [defineNitroPreset({ | ||
| extends: "node-server", | ||
| serveStatic: true | ||
| }, { name: "flight-control" }); | ||
| var preset_default$14 = [flightControl]; | ||
| //#endregion | ||
| //#region src/presets/genezio/preset.ts | ||
| const genezio = defineNitroPreset({ extends: "aws_lambda" }, { name: "genezio" }); | ||
| var preset_default$13 = [genezio]; | ||
| //#endregion | ||
| //#region src/presets/heroku/preset.ts | ||
| const heroku = defineNitroPreset({ | ||
| }, { name: "flight-control" })]; | ||
| var preset_default$14 = [defineNitroPreset({ extends: "aws_lambda" }, { name: "genezio" })]; | ||
| var preset_default$13 = [defineNitroPreset({ | ||
| extends: "node-server", | ||
| serveStatic: true | ||
| }, { name: "heroku" }); | ||
| var preset_default$12 = [heroku]; | ||
| }, { name: "heroku" })]; | ||
| //#endregion | ||
@@ -1029,3 +946,3 @@ //#region src/presets/iis/utils.ts | ||
| async function iisnodeXmlTemplate(nitro) { | ||
| const path$1 = resolve$1(nitro.options.rootDir, "web.config"); | ||
| const path = resolve$1(nitro.options.rootDir, "web.config"); | ||
| const originalString = `<?xml version="1.0" encoding="utf-8"?> | ||
@@ -1096,4 +1013,4 @@ <!-- | ||
| `; | ||
| if (existsSync(path$1)) { | ||
| const fileString = await readFile(path$1, "utf8"); | ||
| if (existsSync(path)) { | ||
| const fileString = await readFile(path, "utf8"); | ||
| const originalWebConfig = await parseXmlDoc(originalString); | ||
@@ -1107,3 +1024,3 @@ const fileWebConfig = await parseXmlDoc(fileString); | ||
| async function iisXmlTemplate(nitro) { | ||
| const path$1 = resolve$1(nitro.options.rootDir, "web.config"); | ||
| const path = resolve$1(nitro.options.rootDir, "web.config"); | ||
| const originalString = `<?xml version="1.0" encoding="UTF-8"?> | ||
@@ -1124,4 +1041,4 @@ <configuration> | ||
| `; | ||
| if (existsSync(path$1)) { | ||
| const fileString = await readFile(path$1, "utf8"); | ||
| if (existsSync(path)) { | ||
| const fileString = await readFile(path, "utf8"); | ||
| const originalWebConfig = await parseXmlDoc(originalString); | ||
@@ -1148,6 +1065,3 @@ const fileWebConfig = await parseXmlDoc(fileString); | ||
| } | ||
| //#endregion | ||
| //#region src/presets/iis/preset.ts | ||
| const iisHandler = defineNitroPreset({ | ||
| var preset_default$12 = [defineNitroPreset({ | ||
| extends: "node-server", | ||
@@ -1158,4 +1072,3 @@ serveStatic: true, | ||
| } } | ||
| }, { name: "iis-handler" }); | ||
| const iisNode = defineNitroPreset({ | ||
| }, { name: "iis-handler" }), defineNitroPreset({ | ||
| extends: "node-server", | ||
@@ -1166,13 +1079,7 @@ serveStatic: true, | ||
| } } | ||
| }, { name: "iis-node" }); | ||
| var preset_default$11 = [iisHandler, iisNode]; | ||
| //#endregion | ||
| //#region src/presets/koyeb/preset.ts | ||
| const koyeb = defineNitroPreset({ | ||
| }, { name: "iis-node" })]; | ||
| var preset_default$11 = [defineNitroPreset({ | ||
| extends: "node-server", | ||
| serveStatic: true | ||
| }, { name: "koyeb" }); | ||
| var preset_default$10 = [koyeb]; | ||
| }, { name: "koyeb" })]; | ||
| //#endregion | ||
@@ -1187,4 +1094,4 @@ //#region src/presets/netlify/utils.ts | ||
| } | ||
| const rules = Object.entries(nitro.options.routeRules).sort((a$1, b) => a$1[0].split(/\/(?!\*)/).length - b[0].split(/\/(?!\*)/).length); | ||
| for (const [key, routeRules] of rules.filter(([_, routeRules$1]) => routeRules$1.redirect)) { | ||
| const rules = Object.entries(nitro.options.routeRules).sort((a, b) => a[0].split(/\/(?!\*)/).length - b[0].split(/\/(?!\*)/).length); | ||
| for (const [key, routeRules] of rules.filter(([_, routeRules]) => routeRules.redirect)) { | ||
| let code = routeRules.redirect.status; | ||
@@ -1209,5 +1116,5 @@ if (code === 307) code = 302; | ||
| let contents = ""; | ||
| const rules = Object.entries(nitro.options.routeRules).sort((a$1, b) => b[0].split(/\/(?!\*)/).length - a$1[0].split(/\/(?!\*)/).length); | ||
| for (const [path$1, routeRules] of rules.filter(([_, routeRules$1]) => routeRules$1.headers)) { | ||
| const headers = [path$1.replace("/**", "/*"), ...Object.entries({ ...routeRules.headers }).map(([header, value]) => ` ${header}: ${value}`)].join("\n"); | ||
| const rules = Object.entries(nitro.options.routeRules).sort((a, b) => b[0].split(/\/(?!\*)/).length - a[0].split(/\/(?!\*)/).length); | ||
| for (const [path, routeRules] of rules.filter(([_, routeRules]) => routeRules.headers)) { | ||
| const headers = [path.replace("/**", "/*"), ...Object.entries({ ...routeRules.headers }).map(([header, value]) => ` ${header}: ${value}`)].join("\n"); | ||
| contents += headers + "\n"; | ||
@@ -1227,3 +1134,3 @@ } | ||
| function getStaticPaths(publicAssets, baseURL) { | ||
| return ["/.netlify/*", ...publicAssets.filter((a$1) => a$1.fallthrough !== true && a$1.baseURL && a$1.baseURL !== "/").map((a$1) => joinURL(baseURL, a$1.baseURL, "*"))]; | ||
| return ["/.netlify/*", ...publicAssets.filter((a) => a.fallthrough !== true && a.baseURL && a.baseURL !== "/").map((a) => joinURL(baseURL, a.baseURL, "*"))]; | ||
| } | ||
@@ -1247,89 +1154,82 @@ function generateNetlifyFunction(nitro) { | ||
| } | ||
| //#endregion | ||
| //#region src/presets/netlify/preset.ts | ||
| const netlify = defineNitroPreset({ | ||
| entry: "./netlify/runtime/netlify", | ||
| manifest: { deploymentId: process.env.DEPLOY_ID }, | ||
| output: { | ||
| dir: "{{ rootDir }}/.netlify/functions-internal", | ||
| publicDir: "{{ rootDir }}/dist/{{ baseURL }}" | ||
| }, | ||
| prerender: { autoSubfolderIndex: false }, | ||
| rollupConfig: { output: { entryFileNames: "main.mjs" } }, | ||
| hooks: { async compiled(nitro) { | ||
| await writeHeaders(nitro); | ||
| await writeRedirects(nitro); | ||
| await promises.writeFile(join$1(nitro.options.output.dir, "server", "server.mjs"), generateNetlifyFunction(nitro)); | ||
| if (nitro.options.netlify?.images) { | ||
| nitro.options.netlify.config ||= {}; | ||
| nitro.options.netlify.config.images ||= nitro.options.netlify?.images; | ||
| } | ||
| if (Object.keys(nitro.options.netlify?.config || {}).length > 0) { | ||
| const configPath = join$1(nitro.options.output.dir, "../deploy/v1/config.json"); | ||
| await promises.mkdir(dirname$1(configPath), { recursive: true }); | ||
| await promises.writeFile(configPath, JSON.stringify(nitro.options.netlify?.config), "utf8"); | ||
| } | ||
| } } | ||
| }, { | ||
| name: "netlify", | ||
| stdName: "netlify" | ||
| }); | ||
| const netlifyEdge = defineNitroPreset({ | ||
| extends: "base-worker", | ||
| entry: "./netlify/runtime/netlify-edge", | ||
| manifest: { deploymentId: process.env.DEPLOY_ID }, | ||
| exportConditions: ["netlify"], | ||
| output: { | ||
| serverDir: "{{ rootDir }}/.netlify/edge-functions/server", | ||
| publicDir: "{{ rootDir }}/dist/{{ baseURL }}" | ||
| }, | ||
| prerender: { autoSubfolderIndex: false }, | ||
| rollupConfig: { output: { | ||
| entryFileNames: "server.js", | ||
| format: "esm" | ||
| } }, | ||
| unenv: unenvDeno, | ||
| hooks: { async compiled(nitro) { | ||
| await writeHeaders(nitro); | ||
| await writeRedirects(nitro); | ||
| const manifest = { | ||
| version: 1, | ||
| functions: [{ | ||
| path: "/*", | ||
| excludedPath: getStaticPaths(nitro.options.publicAssets, nitro.options.baseURL), | ||
| name: "edge server handler", | ||
| function: "server", | ||
| generator: getGeneratorString(nitro) | ||
| }] | ||
| }; | ||
| const manifestPath = join$1(nitro.options.rootDir, ".netlify/edge-functions/manifest.json"); | ||
| await promises.mkdir(dirname$1(manifestPath), { recursive: true }); | ||
| await promises.writeFile(manifestPath, JSON.stringify(manifest, null, 2)); | ||
| } } | ||
| }, { name: "netlify-edge" }); | ||
| const netlifyStatic = defineNitroPreset({ | ||
| extends: "static", | ||
| manifest: { deploymentId: process.env.DEPLOY_ID }, | ||
| output: { | ||
| dir: "{{ rootDir }}/dist", | ||
| publicDir: "{{ rootDir }}/dist/{{ baseURL }}" | ||
| }, | ||
| prerender: { autoSubfolderIndex: false }, | ||
| commands: { preview: "npx serve ./" }, | ||
| hooks: { async compiled(nitro) { | ||
| await writeHeaders(nitro); | ||
| await writeRedirects(nitro); | ||
| } } | ||
| }, { | ||
| name: "netlify-static", | ||
| stdName: "netlify", | ||
| static: true | ||
| }); | ||
| var preset_default$9 = [ | ||
| netlify, | ||
| netlifyEdge, | ||
| netlifyStatic | ||
| var preset_default$10 = [ | ||
| defineNitroPreset({ | ||
| entry: "./netlify/runtime/netlify", | ||
| manifest: { deploymentId: process.env.DEPLOY_ID }, | ||
| output: { | ||
| dir: "{{ rootDir }}/.netlify/functions-internal", | ||
| publicDir: "{{ rootDir }}/dist/{{ baseURL }}" | ||
| }, | ||
| prerender: { autoSubfolderIndex: false }, | ||
| rollupConfig: { output: { entryFileNames: "main.mjs" } }, | ||
| hooks: { async compiled(nitro) { | ||
| await writeHeaders(nitro); | ||
| await writeRedirects(nitro); | ||
| await promises.writeFile(join$1(nitro.options.output.dir, "server", "server.mjs"), generateNetlifyFunction(nitro)); | ||
| if (nitro.options.netlify?.images) { | ||
| nitro.options.netlify.config ||= {}; | ||
| nitro.options.netlify.config.images ||= nitro.options.netlify?.images; | ||
| } | ||
| if (Object.keys(nitro.options.netlify?.config || {}).length > 0) { | ||
| const configPath = join$1(nitro.options.output.dir, "../deploy/v1/config.json"); | ||
| await promises.mkdir(dirname$1(configPath), { recursive: true }); | ||
| await promises.writeFile(configPath, JSON.stringify(nitro.options.netlify?.config), "utf8"); | ||
| } | ||
| } } | ||
| }, { | ||
| name: "netlify", | ||
| stdName: "netlify" | ||
| }), | ||
| defineNitroPreset({ | ||
| extends: "base-worker", | ||
| entry: "./netlify/runtime/netlify-edge", | ||
| manifest: { deploymentId: process.env.DEPLOY_ID }, | ||
| exportConditions: ["netlify"], | ||
| output: { | ||
| serverDir: "{{ rootDir }}/.netlify/edge-functions/server", | ||
| publicDir: "{{ rootDir }}/dist/{{ baseURL }}" | ||
| }, | ||
| prerender: { autoSubfolderIndex: false }, | ||
| rollupConfig: { output: { | ||
| entryFileNames: "server.js", | ||
| format: "esm" | ||
| } }, | ||
| unenv: unenvDeno, | ||
| hooks: { async compiled(nitro) { | ||
| await writeHeaders(nitro); | ||
| await writeRedirects(nitro); | ||
| const manifest = { | ||
| version: 1, | ||
| functions: [{ | ||
| path: "/*", | ||
| excludedPath: getStaticPaths(nitro.options.publicAssets, nitro.options.baseURL), | ||
| name: "edge server handler", | ||
| function: "server", | ||
| generator: getGeneratorString(nitro) | ||
| }] | ||
| }; | ||
| const manifestPath = join$1(nitro.options.rootDir, ".netlify/edge-functions/manifest.json"); | ||
| await promises.mkdir(dirname$1(manifestPath), { recursive: true }); | ||
| await promises.writeFile(manifestPath, JSON.stringify(manifest, null, 2)); | ||
| } } | ||
| }, { name: "netlify-edge" }), | ||
| defineNitroPreset({ | ||
| extends: "static", | ||
| manifest: { deploymentId: process.env.DEPLOY_ID }, | ||
| output: { | ||
| dir: "{{ rootDir }}/dist", | ||
| publicDir: "{{ rootDir }}/dist/{{ baseURL }}" | ||
| }, | ||
| prerender: { autoSubfolderIndex: false }, | ||
| commands: { preview: "npx serve ./" }, | ||
| hooks: { async compiled(nitro) { | ||
| await writeHeaders(nitro); | ||
| await writeRedirects(nitro); | ||
| } } | ||
| }, { | ||
| name: "netlify-static", | ||
| stdName: "netlify", | ||
| static: true | ||
| }) | ||
| ]; | ||
| //#endregion | ||
@@ -1368,39 +1268,23 @@ //#region src/presets/node/cluster.ts | ||
| } | ||
| //#endregion | ||
| //#region src/presets/node/preset.ts | ||
| const nodeServer = defineNitroPreset({ | ||
| entry: "./node/runtime/node-server", | ||
| serveStatic: true, | ||
| commands: { preview: "node ./server/index.mjs" } | ||
| }, { | ||
| name: "node-server", | ||
| aliases: ["node"] | ||
| }); | ||
| const nodeMiddleware = defineNitroPreset({ entry: "./node/runtime/node-middleware" }, { name: "node-middleware" }); | ||
| var preset_default$8 = [ | ||
| nodeServer, | ||
| var preset_default$9 = [ | ||
| defineNitroPreset({ | ||
| entry: "./node/runtime/node-server", | ||
| serveStatic: true, | ||
| commands: { preview: "node ./server/index.mjs" } | ||
| }, { | ||
| name: "node-server", | ||
| aliases: ["node"] | ||
| }), | ||
| nodeCluster, | ||
| nodeMiddleware | ||
| defineNitroPreset({ entry: "./node/runtime/node-middleware" }, { name: "node-middleware" }) | ||
| ]; | ||
| //#endregion | ||
| //#region src/presets/platform.sh/preset.ts | ||
| const platformSh = defineNitroPreset({ | ||
| var preset_default$8 = [defineNitroPreset({ | ||
| extends: "node-server", | ||
| serveStatic: true | ||
| }, { name: "platform-sh" }); | ||
| var preset_default$7 = [platformSh]; | ||
| //#endregion | ||
| //#region src/presets/render.com/preset.ts | ||
| const renderCom = defineNitroPreset({ | ||
| }, { name: "platform-sh" })]; | ||
| var preset_default$7 = [defineNitroPreset({ | ||
| extends: "node-server", | ||
| serveStatic: true | ||
| }, { name: "render-com" }); | ||
| var preset_default$6 = [renderCom]; | ||
| //#endregion | ||
| //#region src/presets/standard/preset.ts | ||
| const standard = defineNitroPreset({ | ||
| }, { name: "render-com" })]; | ||
| var preset_default$6 = [defineNitroPreset({ | ||
| entry: "./standard/runtime/server", | ||
@@ -1417,8 +1301,4 @@ serveStatic: false, | ||
| } | ||
| }, { name: "standard" }); | ||
| var preset_default$5 = [standard]; | ||
| //#endregion | ||
| //#region src/presets/stormkit/preset.ts | ||
| const stormkit = defineNitroPreset({ | ||
| }, { name: "standard" })]; | ||
| var preset_default$5 = [defineNitroPreset({ | ||
| entry: "./stormkit/runtime/stormkit", | ||
@@ -1432,12 +1312,22 @@ output: { | ||
| stdName: "stormkit" | ||
| }); | ||
| var preset_default$4 = [stormkit]; | ||
| })]; | ||
| //#endregion | ||
| //#region src/presets/vercel/runtime/isr.ts | ||
| const ISR_URL_PARAM = "__isr_route"; | ||
| //#endregion | ||
| //#region src/presets/vercel/utils.ts | ||
| const SUPPORTED_NODE_VERSIONS = [20, 22]; | ||
| const SUPPORTED_NODE_VERSIONS = [ | ||
| 20, | ||
| 22, | ||
| 24 | ||
| ]; | ||
| const UNSUPPORTED_PROXY_OPTIONS = [ | ||
| "headers", | ||
| "forwardHeaders", | ||
| "filterHeaders", | ||
| "fetchOptions", | ||
| "cookieDomainRewrite", | ||
| "cookiePathRewrite", | ||
| "onResponse" | ||
| ]; | ||
| const FALLBACK_ROUTE = "/__server"; | ||
@@ -1472,3 +1362,3 @@ const ISR_SUFFIX = "-isr"; | ||
| if (o11Routes.length === 0) return; | ||
| const _getRouteRules = (path$1) => defu({}, ...nitro.routing.routeRules.matchAll("", path$1).reverse()); | ||
| const _getRouteRules = (path) => defu({}, ...nitro.routing.routeRules.matchAll("", path).reverse()); | ||
| for (const route of o11Routes) { | ||
@@ -1487,9 +1377,14 @@ if (_getRouteRules(route.src).isr) continue; | ||
| function generateBuildConfig(nitro, o11Routes) { | ||
| const rules = Object.entries(nitro.options.routeRules).sort((a$1, b) => b[0].split(/\/(?!\*)/).length - a$1[0].split(/\/(?!\*)/).length); | ||
| const rules = Object.entries(nitro.options.routeRules).sort((a, b) => b[0].split(/\/(?!\*)/).length - a[0].split(/\/(?!\*)/).length); | ||
| const cdnProxyPaths = new Set(rules.filter(([_, routeRules]) => routeRules.proxy && canUseVercelRewrite(routeRules.proxy)).map(([path]) => path)); | ||
| const config = defu(nitro.options.vercel?.config, { | ||
| version: 3, | ||
| framework: { | ||
| name: nitro.options.framework.name, | ||
| version: nitro.options.framework.version | ||
| }, | ||
| overrides: { ...Object.fromEntries((nitro._prerenderedRoutes?.filter((r) => r.fileName !== r.route) || []).map(({ route, fileName }) => [withoutLeadingSlash(fileName), { path: route.replace(/^\//, "") }])) }, | ||
| routes: [ | ||
| ...rules.filter(([_, routeRules]) => routeRules.redirect || routeRules.headers).map(([path$1, routeRules]) => { | ||
| let route = { src: path$1.replace("/**", "/(.*)") }; | ||
| ...rules.filter(([path, routeRules]) => (routeRules.redirect || routeRules.headers) && !cdnProxyPaths.has(path)).map(([path, routeRules]) => { | ||
| let route = { src: path.replace("/**", "/(.*)") }; | ||
| if (routeRules.redirect) route = defu(route, { | ||
@@ -1502,2 +1397,11 @@ status: routeRules.redirect.status, | ||
| }), | ||
| ...rules.filter(([path]) => cdnProxyPaths.has(path)).map(([path, routeRules]) => { | ||
| const proxy = routeRules.proxy; | ||
| const route = { | ||
| src: path.replace("/**", "/(.*)"), | ||
| dest: proxy.to.replace("/**", "/$1") | ||
| }; | ||
| if (routeRules.headers) route.headers = routeRules.headers; | ||
| return route; | ||
| }), | ||
| ...nitro.options.vercel?.skewProtection && nitro.options.manifest?.deploymentId ? [{ | ||
@@ -1521,2 +1425,9 @@ src: "/.*", | ||
| }); | ||
| if (nitro.options.experimental.tasks && Object.keys(nitro.options.scheduledTasks || {}).length > 0) { | ||
| const cronPath = nitro.options.vercel.cronHandlerRoute || "/_vercel/cron"; | ||
| config.crons = [...Object.keys(nitro.options.scheduledTasks).map((schedule) => ({ | ||
| path: cronPath, | ||
| schedule | ||
| })), ...config.crons || []]; | ||
| } | ||
| if (nitro.options.static) return config; | ||
@@ -1560,3 +1471,3 @@ config.routes.push(...nitro.options.routeRules["/"]?.isr ? [{ | ||
| } | ||
| if (hasLegacyOptions && !a) nitro.logger.warn("Nitro now uses `isr` option to configure ISR behavior on Vercel. Backwards-compatible support for `static` and `swr` options within the Vercel Build Options API will be removed in the future versions. Set `future.nativeSWR: true` nitro config disable this warning."); | ||
| if (hasLegacyOptions && !v) nitro.logger.warn("Nitro now uses `isr` option to configure ISR behavior on Vercel. Backwards-compatible support for `static` and `swr` options within the Vercel Build Options API will be removed in the future versions. Set `future.nativeSWR: true` nitro config disable this warning."); | ||
| } | ||
@@ -1569,3 +1480,3 @@ async function resolveVercelRuntime(nitro) { | ||
| const systemNodeVersion = getSystemNodeVersion(); | ||
| runtime = `nodejs${SUPPORTED_NODE_VERSIONS.find((version$1) => version$1 >= systemNodeVersion) ?? SUPPORTED_NODE_VERSIONS.at(-1)}.x`; | ||
| runtime = `nodejs${SUPPORTED_NODE_VERSIONS.find((version) => version >= systemNodeVersion) ?? SUPPORTED_NODE_VERSIONS.at(-1)}.x`; | ||
| } | ||
@@ -1584,5 +1495,16 @@ nitro.options.vercel ??= {}; | ||
| } | ||
| /** | ||
| * Check if a proxy rule can be offloaded to a Vercel CDN rewrite. | ||
| * A proxy is eligible when it targets an external URL and uses no | ||
| * ProxyOptions that Vercel's routing layer cannot handle at the edge. | ||
| */ | ||
| function canUseVercelRewrite(proxy) { | ||
| if (!proxy?.to) return false; | ||
| if (!/^https?:\/\//.test(proxy.to.replace(/\/\*\*$/, ""))) return false; | ||
| for (const key of UNSUPPORTED_PROXY_OPTIONS) if (proxy[key] !== void 0) return false; | ||
| return true; | ||
| } | ||
| function getObservabilityRoutes(nitro) { | ||
| if ((nitro.options.compatibilityDate.vercel || nitro.options.compatibilityDate.default) < "2025-07-15") return []; | ||
| const routePatterns = [...new Set([...nitro.options.ssrRoutes || [], ...[...nitro.scannedHandlers, ...nitro.options.handlers].filter((h$1) => !h$1.middleware && h$1.route).map((h$1) => h$1.route)])]; | ||
| const routePatterns = [...new Set([...nitro.options.ssrRoutes || [], ...[...nitro.scannedHandlers, ...nitro.options.handlers].filter((h) => !h.middleware && h.route).map((h) => h.route)])]; | ||
| const staticRoutes = []; | ||
@@ -1601,3 +1523,3 @@ const dynamicRoutes = []; | ||
| function normalizeRoutes(routes) { | ||
| return routes.sort((a$1, b) => b.localeCompare(a$1)).map((route) => ({ | ||
| return routes.sort((a, b) => b.localeCompare(a)).map((route) => ({ | ||
| src: normalizeRouteSrc(route), | ||
@@ -1638,12 +1560,12 @@ dest: normalizeRouteDest(route) | ||
| }; | ||
| if (prerenderConfig.allowQuery && !prerenderConfig.allowQuery.includes(ISR_URL_PARAM)) prerenderConfig.allowQuery.push(ISR_URL_PARAM); | ||
| if (prerenderConfig.allowQuery && !prerenderConfig.allowQuery.includes("__isr_route")) prerenderConfig.allowQuery.push(ISR_URL_PARAM); | ||
| await writeFile$1(filename, JSON.stringify(prerenderConfig, null, 2)); | ||
| } | ||
| //#endregion | ||
| //#region src/presets/vercel/preset.ts | ||
| const vercel = defineNitroPreset({ | ||
| var preset_default$4 = [defineNitroPreset({ | ||
| entry: "./vercel/runtime/vercel.{format}", | ||
| manifest: { deploymentId: process.env.VERCEL_DEPLOYMENT_ID }, | ||
| vercel: { skewProtection: !!process.env.VERCEL_SKEW_PROTECTION_ENABLED }, | ||
| vercel: { | ||
| skewProtection: !!process.env.VERCEL_SKEW_PROTECTION_ENABLED, | ||
| cronHandlerRoute: "/_vercel/cron" | ||
| }, | ||
| output: { | ||
@@ -1665,5 +1587,10 @@ dir: "{{ rootDir }}/.vercel/output", | ||
| let serverFormat = nitro.options.vercel?.entryFormat; | ||
| if (!serverFormat) serverFormat = nitro.routing.routes.routes.flatMap((r) => r.data).some((h$1) => h$1.format === "node") ? "node" : "web"; | ||
| if (!serverFormat) serverFormat = nitro.routing.routes.routes.flatMap((r) => r.data).some((h) => h.format === "node") ? "node" : "web"; | ||
| logger.info(`Using \`${serverFormat}\` entry format.`); | ||
| nitro.options.entry = nitro.options.entry.replace("{format}", serverFormat); | ||
| if (nitro.options.experimental.tasks && Object.keys(nitro.options.scheduledTasks || {}).length > 0) nitro.options.handlers.push({ | ||
| route: nitro.options.vercel.cronHandlerRoute || "/_vercel/cron", | ||
| lazy: true, | ||
| handler: join$1(presetsDir, "vercel/runtime/cron-handler") | ||
| }); | ||
| }, | ||
@@ -1680,4 +1607,3 @@ "rollup:before": (nitro) => { | ||
| stdName: "vercel" | ||
| }); | ||
| const vercelStatic = defineNitroPreset({ | ||
| }), defineNitroPreset({ | ||
| extends: "static", | ||
@@ -1703,8 +1629,4 @@ manifest: { deploymentId: process.env.VERCEL_DEPLOYMENT_ID }, | ||
| static: true | ||
| }); | ||
| var preset_default$3 = [vercel, vercelStatic]; | ||
| //#endregion | ||
| //#region src/presets/winterjs/preset.ts | ||
| const winterjs = defineNitroPreset({ | ||
| })]; | ||
| var preset_default$3 = [defineNitroPreset({ | ||
| extends: "base-worker", | ||
@@ -1716,8 +1638,4 @@ entry: "./winterjs/runtime/winterjs", | ||
| commands: { preview: "wasmer run wasmer/winterjs --forward-host-env --net --mapdir app:./ app/server/index.mjs" } | ||
| }, { name: "winterjs" }); | ||
| var preset_default$2 = [winterjs]; | ||
| //#endregion | ||
| //#region src/presets/zeabur/preset.ts | ||
| const zeabur = defineNitroPreset({ | ||
| }, { name: "winterjs" })]; | ||
| var preset_default$2 = [defineNitroPreset({ | ||
| entry: "./zeabur/runtime/zeabur", | ||
@@ -1748,4 +1666,3 @@ output: { | ||
| stdName: "zeabur" | ||
| }); | ||
| const zeaburStatic = defineNitroPreset({ | ||
| }), defineNitroPreset({ | ||
| extends: "static", | ||
@@ -1760,12 +1677,55 @@ output: { | ||
| static: true | ||
| }); | ||
| var preset_default$1 = [zeabur, zeaburStatic]; | ||
| })]; | ||
| //#endregion | ||
| //#region src/presets/zerops/preset.ts | ||
| const zerops = defineNitroPreset({ | ||
| //#region src/presets/zephyr/preset.ts | ||
| const LOGGER_TAG = "zephyr-nitro-preset"; | ||
| var preset_default$1 = [defineNitroPreset({ | ||
| extends: "base-worker", | ||
| entry: "./zephyr/runtime/server", | ||
| output: { publicDir: "{{ output.dir }}/client/{{ baseURL }}" }, | ||
| exportConditions: ["node"], | ||
| minify: false, | ||
| rollupConfig: { output: { | ||
| format: "esm", | ||
| exports: "named", | ||
| inlineDynamicImports: false | ||
| } }, | ||
| wasm: { | ||
| lazy: false, | ||
| esmImport: true | ||
| }, | ||
| hooks: { | ||
| "build:before": (nitro) => { | ||
| nitro.options.unenv.push(unenvCfExternals, unenvCfNodeCompat); | ||
| }, | ||
| compiled: async (nitro) => { | ||
| try { | ||
| if (!globalThis.__nitroDeploying__ && !nitro.options.zephyr?.deployOnBuild) { | ||
| nitro.logger.info(`[${LOGGER_TAG}] Zephyr deploy skipped on build.`); | ||
| return; | ||
| } | ||
| const { deploymentUrl } = await (await importDep({ | ||
| id: "zephyr-agent", | ||
| reason: "deploying to Zephyr", | ||
| dir: nitro.options.rootDir | ||
| })).uploadOutputToZephyr({ | ||
| rootDir: nitro.options.rootDir, | ||
| outputDir: nitro.options.output.dir, | ||
| baseURL: nitro.options.baseURL, | ||
| publicDir: resolve$1(nitro.options.output.dir, nitro.options.output.publicDir) | ||
| }); | ||
| if (deploymentUrl) nitro.logger.success(`[${LOGGER_TAG}] Zephyr deployment succeeded: ${deploymentUrl}`); | ||
| else nitro.logger.success(`[${LOGGER_TAG}] Zephyr deployment succeeded.`); | ||
| globalThis.__nitroDeployed__ = true; | ||
| } catch (error) { | ||
| if (error instanceof Error) throw error; | ||
| throw new TypeError(`[${LOGGER_TAG}] ${String(error)}`); | ||
| } | ||
| } | ||
| } | ||
| }, { name: "zephyr" })]; | ||
| var preset_default = [defineNitroPreset({ | ||
| extends: "node-server", | ||
| serveStatic: true | ||
| }, { name: "zerops" }); | ||
| const zeropsStatic = defineNitroPreset({ | ||
| }, { name: "zerops" }), defineNitroPreset({ | ||
| extends: "static", | ||
@@ -1779,8 +1739,7 @@ output: { | ||
| static: true | ||
| }); | ||
| var preset_default = [zerops, zeropsStatic]; | ||
| })]; | ||
| //#endregion | ||
| //#region src/presets/_all.gen.ts | ||
| var _all_gen_default = [ | ||
| ...preset_default$27, | ||
| ...preset_default$26, | ||
@@ -1814,3 +1773,2 @@ ...preset_default$25, | ||
| ]; | ||
| //#endregion | ||
@@ -1825,21 +1783,21 @@ //#region src/presets/_resolve.ts | ||
| if (name === ".") return; | ||
| const _name = kebabCase(name) || p; | ||
| const _name = kebabCase(name) || f; | ||
| const _compatDates = opts.compatibilityDate ? resolveCompatibilityDatesFromEnv(opts.compatibilityDate) : false; | ||
| const matches = _all_gen_default.filter((preset$1) => { | ||
| const matches = _all_gen_default.filter((preset) => { | ||
| if (![ | ||
| preset$1._meta.name, | ||
| preset$1._meta.stdName, | ||
| ...preset$1._meta.aliases || [] | ||
| preset._meta.name, | ||
| preset._meta.stdName, | ||
| ...preset._meta.aliases || [] | ||
| ].filter(Boolean).includes(_name)) return false; | ||
| if (opts.dev && !preset$1._meta.dev || !opts.dev && preset$1._meta.dev) return false; | ||
| if (opts.dev && !preset._meta.dev || !opts.dev && preset._meta.dev) return false; | ||
| if (_compatDates) { | ||
| const _date = _compatDates[_stdProviderMap[preset$1._meta.stdName]] || _compatDates[preset$1._meta.stdName] || _compatDates[preset$1._meta.name] || _compatDates.default; | ||
| if (_date && preset$1._meta.compatibilityDate && new Date(preset$1._meta.compatibilityDate) > new Date(_date)) return false; | ||
| const _date = _compatDates[_stdProviderMap[preset._meta.stdName]] || _compatDates[preset._meta.stdName] || _compatDates[preset._meta.name] || _compatDates.default; | ||
| if (_date && preset._meta.compatibilityDate && new Date(preset._meta.compatibilityDate) > new Date(_date)) return false; | ||
| } | ||
| return true; | ||
| }).sort((a$1, b) => { | ||
| const aDate = new Date(a$1._meta.compatibilityDate || 0); | ||
| }).sort((a, b) => { | ||
| const aDate = new Date(a._meta.compatibilityDate || 0); | ||
| return new Date(b._meta.compatibilityDate || 0) > aDate ? 1 : -1; | ||
| }); | ||
| const preset = matches.find((p$1) => (p$1._meta.static || false) === (opts?.static || false)) || matches[0]; | ||
| const preset = matches.find((p) => (p._meta.static || false) === (opts?.static || false)) || matches[0]; | ||
| if (typeof preset === "function") return preset(); | ||
@@ -1851,6 +1809,6 @@ if (!name && !preset) { | ||
| bun: "bun" | ||
| }[K] || "node", opts); | ||
| }[R] || "node", opts); | ||
| } | ||
| if (name && !preset) { | ||
| const options = _all_gen_default.filter((p$1) => p$1._meta.name === name || p$1._meta.stdName === name || p$1._meta.aliases?.includes(name)).sort((a$1, b) => (a$1._meta.compatibilityDate || 0) > (b._meta.compatibilityDate || 0) ? 1 : -1); | ||
| const options = _all_gen_default.filter((p) => p._meta.name === name || p._meta.stdName === name || p._meta.aliases?.includes(name)).sort((a, b) => (a._meta.compatibilityDate || 0) > (b._meta.compatibilityDate || 0) ? 1 : -1); | ||
| if (options.length > 0) { | ||
@@ -1866,4 +1824,3 @@ let msg = `Preset "${name}" cannot be resolved with current compatibilityDate: ${formatCompatibilityDate(_compatDates || "")}.\n\n`; | ||
| } | ||
| //#endregion | ||
| export { resolvePreset }; | ||
| export { resolvePreset }; |
| import { t as NitroDevApp } from "./_dev.mjs"; | ||
| import { IncomingMessage, OutgoingMessage } from "node:http"; | ||
| import { Duplex } from "node:stream"; | ||
| import { RunnerMessageListener, RunnerRPCHooks } from "env-runner"; | ||
| import { IncomingMessage } from "node:http"; | ||
| import { Server, ServerOptions } from "srvx"; | ||
| import { LoadConfigOptions, Nitro, NitroBuildInfo, NitroConfig, NitroOptions, RunnerMessageListener, RunnerRPCHooks, TaskEvent, TaskRunnerOptions } from "nitro/types"; | ||
| import { LoadConfigOptions, Nitro, NitroBuildInfo, NitroConfig, NitroOptions, TaskEvent, TaskRunnerOptions } from "nitro/types"; | ||
| import { Socket } from "node:net"; | ||
@@ -39,3 +40,3 @@ //#region src/nitro.d.ts | ||
| constructor(nitro: Nitro); | ||
| upgrade(req: IncomingMessage, socket: OutgoingMessage<IncomingMessage> | Duplex, head: any): Promise<any>; | ||
| upgrade(req: IncomingMessage, socket: Socket, head: any): Promise<void>; | ||
| listen(opts?: Partial<Omit<ServerOptions, "fetch">>): Server; | ||
@@ -42,0 +43,0 @@ close(): Promise<void>; |
+12
-6
@@ -1,9 +0,15 @@ | ||
| import "./_common.mjs"; | ||
| import { D as prepare, M as build, O as copyPublicAssets, _ as writeTypes, m as getBuildInfo } from "./_build/common.mjs"; | ||
| import "./_libs/rc9+c12+dotenv.mjs"; | ||
| import "./_libs/c12+rc9.mjs"; | ||
| import { D as copyPublicAssets, E as prepare, _ as writeTypes, j as build, m as getBuildInfo } from "./_build/common.mjs"; | ||
| import "./_libs/compatx.mjs"; | ||
| import "./_libs/klona.mjs"; | ||
| import { a as loadOptions, i as createNitro, n as runTask, r as prerender, t as listTasks } from "./_chunks/nitro.mjs"; | ||
| import "./_libs/escape-string-regexp.mjs"; | ||
| import "./_libs/tsconfck.mjs"; | ||
| import "./_chunks/nitro2.mjs"; | ||
| import { n as createDevServer } from "./_chunks/dev.mjs"; | ||
| export { build, copyPublicAssets, createDevServer, createNitro, getBuildInfo, listTasks, loadOptions, prepare, prerender, runTask, writeTypes }; | ||
| import "./_libs/rou3.mjs"; | ||
| import "./_libs/readdirp+chokidar.mjs"; | ||
| import "./_libs/perfect-debounce.mjs"; | ||
| import "./_libs/httpxy.mjs"; | ||
| import { n as createDevServer } from "./_dev.mjs"; | ||
| import "./_libs/ultrahtml.mjs"; | ||
| export { build, copyPublicAssets, createDevServer, createNitro, getBuildInfo, listTasks, loadOptions, prepare, prerender, runTask, writeTypes }; |
@@ -1,7 +0,25 @@ | ||
| import { ct as resolve } from "../../_build/common.mjs"; | ||
| import { dt as resolve } from "../../_build/common.mjs"; | ||
| import { t as defineCommand } from "../../_libs/citty.mjs"; | ||
| import { t as commonArgs } from "./common.mjs"; | ||
| import { build, copyPublicAssets, createNitro, prepare, prerender } from "nitro/builder"; | ||
| //#region src/cli/commands/build.ts | ||
| const buildArgs = { | ||
| ...commonArgs, | ||
| minify: { | ||
| type: "boolean", | ||
| description: "Minify the output (overrides preset defaults you can also use `--no-minify` to disable)." | ||
| }, | ||
| preset: { | ||
| type: "string", | ||
| description: "The build preset to use (you can also use `NITRO_PRESET` environment variable)." | ||
| }, | ||
| builder: { | ||
| type: "string", | ||
| description: "The builder to use (you can also use `NITRO_BUILDER` environment variable)." | ||
| }, | ||
| compatibilityDate: { | ||
| type: "string", | ||
| description: "The date to use for preset compatibility (you can also use `NITRO_COMPATIBILITY_DATE` environment variable)." | ||
| } | ||
| }; | ||
| var build_default = defineCommand({ | ||
@@ -12,21 +30,3 @@ meta: { | ||
| }, | ||
| args: { | ||
| ...commonArgs, | ||
| minify: { | ||
| type: "boolean", | ||
| description: "Minify the output (overrides preset defaults you can also use `--no-minify` to disable)." | ||
| }, | ||
| preset: { | ||
| type: "string", | ||
| description: "The build preset to use (you can also use `NITRO_PRESET` environment variable)." | ||
| }, | ||
| builder: { | ||
| type: "string", | ||
| description: "The builder to use (you can also use `NITRO_BUILDER` environment variable)." | ||
| }, | ||
| compatibilityDate: { | ||
| type: "string", | ||
| description: "The date to use for preset compatibility (you can also use `NITRO_COMPATIBILITY_DATE` environment variable)." | ||
| } | ||
| }, | ||
| args: buildArgs, | ||
| async run({ args }) { | ||
@@ -47,4 +47,3 @@ const nitro = await createNitro({ | ||
| }); | ||
| //#endregion | ||
| export { build_default as default }; | ||
| export { buildArgs, build_default as default }; |
@@ -13,4 +13,3 @@ //#region src/cli/common.ts | ||
| }; | ||
| //#endregion | ||
| export { commonArgs as t }; | ||
| export { commonArgs as t }; |
@@ -1,9 +0,10 @@ | ||
| import { ct as resolve } from "../../_build/common.mjs"; | ||
| import "../../_libs/rc9+c12+dotenv.mjs"; | ||
| import { t as NitroDevServer } from "../../_chunks/dev.mjs"; | ||
| import { dt as resolve } from "../../_build/common.mjs"; | ||
| import "../../_libs/readdirp+chokidar.mjs"; | ||
| import "../../_libs/perfect-debounce.mjs"; | ||
| import "../../_libs/httpxy.mjs"; | ||
| import { t as NitroDevServer } from "../../_dev.mjs"; | ||
| import { t as defineCommand } from "../../_libs/citty.mjs"; | ||
| import { t as commonArgs } from "./common.mjs"; | ||
| import { consola } from "consola"; | ||
| import { consola as consola$1 } from "consola"; | ||
| import { build, createNitro, prepare } from "nitro/builder"; | ||
| //#region src/cli/commands/dev.ts | ||
@@ -32,3 +33,3 @@ const hmrKeyRe = /^runtimeConfig\.|routeRules\./; | ||
| if (nitro) { | ||
| consola.info("Restarting dev server..."); | ||
| consola$1.info("Restarting dev server..."); | ||
| if ("unwatch" in nitro.options._c12) await nitro.options._c12.unwatch(); | ||
@@ -46,3 +47,3 @@ await nitro.close(); | ||
| if (diff.length === 0) return; | ||
| consola.info("Nitro config updated:\n" + diff.map((entry) => ` ${entry.toString()}`).join("\n")); | ||
| consola$1.info("Nitro config updated:\n" + diff.map((entry) => ` ${entry.toString()}`).join("\n")); | ||
| await (diff.every((e) => hmrKeyRe.test(e.key)) ? nitro.updateConfig(newConfig.config || {}) : reload()); | ||
@@ -62,4 +63,3 @@ } } | ||
| }); | ||
| //#endregion | ||
| export { dev_default as default }; | ||
| export { dev_default as default }; |
@@ -1,6 +0,5 @@ | ||
| import { ct as resolve } from "../../_build/common.mjs"; | ||
| import { dt as resolve } from "../../_build/common.mjs"; | ||
| import { t as defineCommand } from "../../_libs/citty.mjs"; | ||
| import { consola } from "consola"; | ||
| import { consola as consola$1 } from "consola"; | ||
| import { listTasks, loadOptions } from "nitro/builder"; | ||
| //#region src/cli/commands/task/list.ts | ||
@@ -22,7 +21,6 @@ var list_default = defineCommand({ | ||
| }); | ||
| for (const [name, task] of Object.entries(tasks)) consola.log(` - \`${name}\`${task.meta?.description ? ` - ${task.meta.description}` : ""}`); | ||
| for (const [name, task] of Object.entries(tasks)) consola$1.log(` - \`${name}\`${task.meta?.description ? ` - ${task.meta.description}` : ""}`); | ||
| } | ||
| }); | ||
| //#endregion | ||
| export { list_default as default }; | ||
| export { list_default as default }; |
@@ -1,6 +0,5 @@ | ||
| import { ct as resolve } from "../../_build/common.mjs"; | ||
| import { dt as resolve } from "../../_build/common.mjs"; | ||
| import { t as defineCommand } from "../../_libs/citty.mjs"; | ||
| import { t as commonArgs } from "./common.mjs"; | ||
| import { createNitro, writeTypes } from "nitro/builder"; | ||
| //#region src/cli/commands/prepare.ts | ||
@@ -17,4 +16,3 @@ var prepare_default = defineCommand({ | ||
| }); | ||
| //#endregion | ||
| export { prepare_default as default }; | ||
| export { prepare_default as default }; |
@@ -1,7 +0,6 @@ | ||
| import { ct as resolve } from "../../_build/common.mjs"; | ||
| import { dt as resolve } from "../../_build/common.mjs"; | ||
| import { t as defineCommand } from "../../_libs/citty.mjs"; | ||
| import { consola } from "consola"; | ||
| import { consola as consola$1 } from "consola"; | ||
| import destr from "destr"; | ||
| import { loadOptions, runTask } from "nitro/builder"; | ||
| //#region src/cli/commands/task/run.ts | ||
@@ -31,6 +30,6 @@ var run_default = defineCommand({ | ||
| const options = await loadOptions({ rootDir: cwd }).catch(() => void 0); | ||
| consola.info(`Running task \`${args.name}\`...`); | ||
| consola$1.info(`Running task \`${args.name}\`...`); | ||
| let payload = destr(args.payload || "{}"); | ||
| if (typeof payload !== "object") { | ||
| consola.error(`Invalid payload: \`${args.payload}\` (it should be a valid JSON object)`); | ||
| consola$1.error(`Invalid payload: \`${args.payload}\` (it should be a valid JSON object)`); | ||
| payload = void 0; | ||
@@ -47,5 +46,5 @@ } | ||
| }); | ||
| consola.success("Result:", result); | ||
| consola$1.success("Result:", result); | ||
| } catch (error) { | ||
| consola.error(`Failed to run task \`${args.name}\`: ${error}`); | ||
| consola$1.error(`Failed to run task \`${args.name}\`: ${error}`); | ||
| process.exit(1); | ||
@@ -55,4 +54,3 @@ } | ||
| }); | ||
| //#endregion | ||
| export { run_default as default }; | ||
| export { run_default as default }; |
| import { t as defineCommand } from "../../_libs/citty.mjs"; | ||
| //#region src/cli/commands/task/index.ts | ||
@@ -14,4 +13,3 @@ var task_default = defineCommand({ | ||
| }); | ||
| //#endregion | ||
| export { task_default as default }; | ||
| export { task_default as default }; |
| #!/usr/bin/env node | ||
| import { n as runMain, t as defineCommand } from "../_libs/citty.mjs"; | ||
| import { version } from "nitro/meta"; | ||
| //#region src/cli/index.ts | ||
@@ -15,8 +14,10 @@ runMain(defineCommand({ | ||
| build: () => import("./_chunks/build.mjs").then((r) => r.default), | ||
| deploy: () => import("./_chunks/deploy.mjs").then((r) => r.default), | ||
| prepare: () => import("./_chunks/prepare.mjs").then((r) => r.default), | ||
| task: () => import("./_chunks/task.mjs").then((r) => r.default) | ||
| task: () => import("./_chunks/task.mjs").then((r) => r.default), | ||
| preview: () => import("./_chunks/preview.mjs").then((r) => r.default), | ||
| docs: () => import("./_chunks/docs.mjs").then((r) => r.default) | ||
| } | ||
| })); | ||
| //#endregion | ||
| export { }; | ||
| export {}; |
@@ -633,2 +633,10 @@ import { | ||
| } | ||
| .dumper-dump pre code, .dumper-dump pre code span { | ||
| display: inline; | ||
| margin: 0; | ||
| padding: 0; | ||
| } | ||
| .dumper-toggle { | ||
| padding: 0 !important; | ||
| } | ||
| .dumper-dump pre samp { | ||
@@ -635,0 +643,0 @@ position: relative; |
| { | ||
| "name": "@poppinss/dumper", | ||
| "version": "0.6.5", | ||
| "version": "0.7.0", | ||
| "description": "Pretty print JavaScript data types in the terminal and the browser", | ||
@@ -5,0 +5,0 @@ "main": "build/index.js", |
@@ -1,1 +0,1 @@ | ||
| function h(n,t,e,r,s,i,a,l){return h.fromTZ(h.tp(n,t,e,r,s,i,a),l)}h.fromTZISO=(n,t,e)=>h.fromTZ(k(n,t),e);h.fromTZ=function(n,t){let e=new Date(Date.UTC(n.y,n.m-1,n.d,n.h,n.i,n.s)),r=D(n.tz,e),s=new Date(e.getTime()-r),i=D(n.tz,s);if(i-r===0)return s;{let a=new Date(e.getTime()-i),l=D(n.tz,a);if(l-i===0)return a;if(!t&&l-i>0)return a;if(t)throw new Error("Invalid date passed to fromTZ()");return s}};h.toTZ=function(n,t){let e=n.toLocaleString("en-US",{timeZone:t}).replace(/[\u202f]/," "),r=new Date(e);return{y:r.getFullYear(),m:r.getMonth()+1,d:r.getDate(),h:r.getHours(),i:r.getMinutes(),s:r.getSeconds(),tz:t}};h.tp=(n,t,e,r,s,i,a)=>({y:n,m:t,d:e,h:r,i:s,s:i,tz:a});function D(n,t=new Date){let e=t.toLocaleString("en-US",{timeZone:n,timeZoneName:"shortOffset"}).split(" ").slice(-1)[0],r=t.toLocaleString("en-US").replace(/[\u202f]/," ");return Date.parse(`${r} GMT`)-Date.parse(`${r} ${e}`)}function k(n,t){let e=new Date(Date.parse(n));if(isNaN(e))throw new Error("minitz: Invalid ISO8601 passed to parser.");let r=n.substring(9);return n.includes("Z")||r.includes("-")||r.includes("+")?h.tp(e.getUTCFullYear(),e.getUTCMonth()+1,e.getUTCDate(),e.getUTCHours(),e.getUTCMinutes(),e.getUTCSeconds(),"Etc/UTC"):h.tp(e.getFullYear(),e.getMonth()+1,e.getDate(),e.getHours(),e.getMinutes(),e.getSeconds(),t)}h.minitz=h;var b=32,p=31|b,v=[1,2,4,8,16],d=class{pattern;timezone;second;minute;hour;day;month;dayOfWeek;lastDayOfMonth;starDOM;starDOW;constructor(t,e){this.pattern=t,this.timezone=e,this.second=Array(60).fill(0),this.minute=Array(60).fill(0),this.hour=Array(24).fill(0),this.day=Array(31).fill(0),this.month=Array(12).fill(0),this.dayOfWeek=Array(7).fill(0),this.lastDayOfMonth=!1,this.starDOM=!1,this.starDOW=!1,this.parse()}parse(){if(!(typeof this.pattern=="string"||this.pattern instanceof String))throw new TypeError("CronPattern: Pattern has to be of type string.");this.pattern.indexOf("@")>=0&&(this.pattern=this.handleNicknames(this.pattern).trim());let t=this.pattern.replace(/\s+/g," ").split(" ");if(t.length<5||t.length>6)throw new TypeError("CronPattern: invalid configuration format ('"+this.pattern+"'), exactly five or six space separated parts are required.");if(t.length===5&&t.unshift("0"),t[3].indexOf("L")>=0&&(t[3]=t[3].replace("L",""),this.lastDayOfMonth=!0),t[3]=="*"&&(this.starDOM=!0),t[4].length>=3&&(t[4]=this.replaceAlphaMonths(t[4])),t[5].length>=3&&(t[5]=this.replaceAlphaDays(t[5])),t[5]=="*"&&(this.starDOW=!0),this.pattern.indexOf("?")>=0){let e=new f(new Date,this.timezone).getDate(!0);t[0]=t[0].replace("?",e.getSeconds().toString()),t[1]=t[1].replace("?",e.getMinutes().toString()),t[2]=t[2].replace("?",e.getHours().toString()),this.starDOM||(t[3]=t[3].replace("?",e.getDate().toString())),t[4]=t[4].replace("?",(e.getMonth()+1).toString()),this.starDOW||(t[5]=t[5].replace("?",e.getDay().toString()))}this.throwAtIllegalCharacters(t),this.partToArray("second",t[0],0,1),this.partToArray("minute",t[1],0,1),this.partToArray("hour",t[2],0,1),this.partToArray("day",t[3],-1,1),this.partToArray("month",t[4],-1,1),this.partToArray("dayOfWeek",t[5],0,p),this.dayOfWeek[7]&&(this.dayOfWeek[0]=this.dayOfWeek[7])}partToArray(t,e,r,s){let i=this[t],a=t==="day"&&this.lastDayOfMonth;if(e===""&&!a)throw new TypeError("CronPattern: configuration entry "+t+" ("+e+") is empty, check for trailing spaces.");if(e==="*")return i.fill(s);let l=e.split(",");if(l.length>1)for(let o=0;o<l.length;o++)this.partToArray(t,l[o],r,s);else e.indexOf("-")!==-1&&e.indexOf("/")!==-1?this.handleRangeWithStepping(e,t,r,s):e.indexOf("-")!==-1?this.handleRange(e,t,r,s):e.indexOf("/")!==-1?this.handleStepping(e,t,r,s):e!==""&&this.handleNumber(e,t,r,s)}throwAtIllegalCharacters(t){for(let e=0;e<t.length;e++)if((e===5?/[^/*0-9,\-#L]+/:/[^/*0-9,-]+/).test(t[e]))throw new TypeError("CronPattern: configuration entry "+e+" ("+t[e]+") contains illegal characters.")}handleNumber(t,e,r,s){let i=this.extractNth(t,e),a=parseInt(i[0],10)+r;if(isNaN(a))throw new TypeError("CronPattern: "+e+" is not a number: '"+t+"'");this.setPart(e,a,i[1]||s)}setPart(t,e,r){if(!Object.prototype.hasOwnProperty.call(this,t))throw new TypeError("CronPattern: Invalid part specified: "+t);if(t==="dayOfWeek"){if(e===7&&(e=0),e<0||e>6)throw new RangeError("CronPattern: Invalid value for dayOfWeek: "+e);this.setNthWeekdayOfMonth(e,r);return}if(t==="second"||t==="minute"){if(e<0||e>=60)throw new RangeError("CronPattern: Invalid value for "+t+": "+e)}else if(t==="hour"){if(e<0||e>=24)throw new RangeError("CronPattern: Invalid value for "+t+": "+e)}else if(t==="day"){if(e<0||e>=31)throw new RangeError("CronPattern: Invalid value for "+t+": "+e)}else if(t==="month"&&(e<0||e>=12))throw new RangeError("CronPattern: Invalid value for "+t+": "+e);this[t][e]=r}handleRangeWithStepping(t,e,r,s){let i=this.extractNth(t,e),a=i[0].match(/^(\d+)-(\d+)\/(\d+)$/);if(a===null)throw new TypeError("CronPattern: Syntax error, illegal range with stepping: '"+t+"'");let[,l,o,u]=a,c=parseInt(l,10)+r,w=parseInt(o,10)+r,C=parseInt(u,10);if(isNaN(c))throw new TypeError("CronPattern: Syntax error, illegal lower range (NaN)");if(isNaN(w))throw new TypeError("CronPattern: Syntax error, illegal upper range (NaN)");if(isNaN(C))throw new TypeError("CronPattern: Syntax error, illegal stepping: (NaN)");if(C===0)throw new TypeError("CronPattern: Syntax error, illegal stepping: 0");if(C>this[e].length)throw new TypeError("CronPattern: Syntax error, steps cannot be greater than maximum value of part ("+this[e].length+")");if(c>w)throw new TypeError("CronPattern: From value is larger than to value: '"+t+"'");for(let T=c;T<=w;T+=C)this.setPart(e,T,i[1]||s)}extractNth(t,e){let r=t,s;if(r.includes("#")){if(e!=="dayOfWeek")throw new Error("CronPattern: nth (#) only allowed in day-of-week field");s=r.split("#")[1],r=r.split("#")[0]}return[r,s]}handleRange(t,e,r,s){let i=this.extractNth(t,e),a=i[0].split("-");if(a.length!==2)throw new TypeError("CronPattern: Syntax error, illegal range: '"+t+"'");let l=parseInt(a[0],10)+r,o=parseInt(a[1],10)+r;if(isNaN(l))throw new TypeError("CronPattern: Syntax error, illegal lower range (NaN)");if(isNaN(o))throw new TypeError("CronPattern: Syntax error, illegal upper range (NaN)");if(l>o)throw new TypeError("CronPattern: From value is larger than to value: '"+t+"'");for(let u=l;u<=o;u++)this.setPart(e,u,i[1]||s)}handleStepping(t,e,r,s){let i=this.extractNth(t,e),a=i[0].split("/");if(a.length!==2)throw new TypeError("CronPattern: Syntax error, illegal stepping: '"+t+"'");a[0]===""&&(a[0]="*");let l=0;a[0]!=="*"&&(l=parseInt(a[0],10)+r);let o=parseInt(a[1],10);if(isNaN(o))throw new TypeError("CronPattern: Syntax error, illegal stepping: (NaN)");if(o===0)throw new TypeError("CronPattern: Syntax error, illegal stepping: 0");if(o>this[e].length)throw new TypeError("CronPattern: Syntax error, max steps for part is ("+this[e].length+")");for(let u=l;u<this[e].length;u+=o)this.setPart(e,u,i[1]||s)}replaceAlphaDays(t){return t.replace(/-sun/gi,"-7").replace(/sun/gi,"0").replace(/mon/gi,"1").replace(/tue/gi,"2").replace(/wed/gi,"3").replace(/thu/gi,"4").replace(/fri/gi,"5").replace(/sat/gi,"6")}replaceAlphaMonths(t){return t.replace(/jan/gi,"1").replace(/feb/gi,"2").replace(/mar/gi,"3").replace(/apr/gi,"4").replace(/may/gi,"5").replace(/jun/gi,"6").replace(/jul/gi,"7").replace(/aug/gi,"8").replace(/sep/gi,"9").replace(/oct/gi,"10").replace(/nov/gi,"11").replace(/dec/gi,"12")}handleNicknames(t){let e=t.trim().toLowerCase();return e==="@yearly"||e==="@annually"?"0 0 1 1 *":e==="@monthly"?"0 0 1 * *":e==="@weekly"?"0 0 * * 0":e==="@daily"?"0 0 * * *":e==="@hourly"?"0 * * * *":t}setNthWeekdayOfMonth(t,e){if(typeof e!="number"&&e==="L")this.dayOfWeek[t]=this.dayOfWeek[t]|b;else if(e===p)this.dayOfWeek[t]=p;else if(e<6&&e>0)this.dayOfWeek[t]=this.dayOfWeek[t]|v[e-1];else throw new TypeError(`CronPattern: nth weekday out of range, should be 1-5 or L. Value: ${e}, Type: ${typeof e}`)}};var O=[31,28,31,30,31,30,31,31,30,31,30,31],m=[["month","year",0],["day","month",-1],["hour","day",0],["minute","hour",0],["second","minute",0]],f=class n{tz;ms;second;minute;hour;day;month;year;constructor(t,e){if(this.tz=e,t&&t instanceof Date)if(!isNaN(t))this.fromDate(t);else throw new TypeError("CronDate: Invalid date passed to CronDate constructor");else if(t===void 0)this.fromDate(new Date);else if(t&&typeof t=="string")this.fromString(t);else if(t instanceof n)this.fromCronDate(t);else throw new TypeError("CronDate: Invalid type ("+typeof t+") passed to CronDate constructor")}isNthWeekdayOfMonth(t,e,r,s){let a=new Date(Date.UTC(t,e,r)).getUTCDay(),l=0;for(let o=1;o<=r;o++)new Date(Date.UTC(t,e,o)).getUTCDay()===a&&l++;if(s&p&&v[l-1]&s)return!0;if(s&b){let o=new Date(Date.UTC(t,e+1,0)).getUTCDate();for(let u=r+1;u<=o;u++)if(new Date(Date.UTC(t,e,u)).getUTCDay()===a)return!1;return!0}return!1}fromDate(t){if(this.tz!==void 0)if(typeof this.tz=="number")this.ms=t.getUTCMilliseconds(),this.second=t.getUTCSeconds(),this.minute=t.getUTCMinutes()+this.tz,this.hour=t.getUTCHours(),this.day=t.getUTCDate(),this.month=t.getUTCMonth(),this.year=t.getUTCFullYear(),this.apply();else{let e=h.toTZ(t,this.tz);this.ms=t.getMilliseconds(),this.second=e.s,this.minute=e.i,this.hour=e.h,this.day=e.d,this.month=e.m-1,this.year=e.y}else this.ms=t.getMilliseconds(),this.second=t.getSeconds(),this.minute=t.getMinutes(),this.hour=t.getHours(),this.day=t.getDate(),this.month=t.getMonth(),this.year=t.getFullYear()}fromCronDate(t){this.tz=t.tz,this.year=t.year,this.month=t.month,this.day=t.day,this.hour=t.hour,this.minute=t.minute,this.second=t.second,this.ms=t.ms}apply(){if(this.month>11||this.day>O[this.month]||this.hour>59||this.minute>59||this.second>59||this.hour<0||this.minute<0||this.second<0){let t=new Date(Date.UTC(this.year,this.month,this.day,this.hour,this.minute,this.second,this.ms));return this.ms=t.getUTCMilliseconds(),this.second=t.getUTCSeconds(),this.minute=t.getUTCMinutes(),this.hour=t.getUTCHours(),this.day=t.getUTCDate(),this.month=t.getUTCMonth(),this.year=t.getUTCFullYear(),!0}else return!1}fromString(t){if(typeof this.tz=="number"){let e=h.fromTZISO(t);this.ms=e.getUTCMilliseconds(),this.second=e.getUTCSeconds(),this.minute=e.getUTCMinutes(),this.hour=e.getUTCHours(),this.day=e.getUTCDate(),this.month=e.getUTCMonth(),this.year=e.getUTCFullYear(),this.apply()}else return this.fromDate(h.fromTZISO(t,this.tz))}findNext(t,e,r,s){let i=this[e],a;r.lastDayOfMonth&&(this.month!==1?a=O[this.month]:a=new Date(Date.UTC(this.year,this.month+1,0,0,0,0,0)).getUTCDate());let l=!r.starDOW&&e=="day"?new Date(Date.UTC(this.year,this.month,1,0,0,0,0)).getUTCDay():void 0;for(let o=this[e]+s;o<r[e].length;o++){let u=r[e][o];if(e==="day"&&r.lastDayOfMonth&&o-s==a&&(u=1),e==="day"&&!r.starDOW){let c=r.dayOfWeek[(l+(o-s-1))%7];if(c&&c&p)c=this.isNthWeekdayOfMonth(this.year,this.month,o-s,c)?1:0;else if(c)throw new Error(`CronDate: Invalid value for dayOfWeek encountered. ${c}`);t.legacyMode&&!r.starDOM?u=u||c:u=u&&c}if(u)return this[e]=o-s,i!==this[e]?2:1}return 3}recurse(t,e,r){let s=this.findNext(e,m[r][0],t,m[r][2]);if(s>1){let i=r+1;for(;i<m.length;)this[m[i][0]]=-m[i][2],i++;if(s===3)return this[m[r][1]]++,this[m[r][0]]=-m[r][2],this.apply(),this.recurse(t,e,0);if(this.apply())return this.recurse(t,e,r-1)}return r+=1,r>=m.length?this:this.year>=3e3?null:this.recurse(t,e,r)}increment(t,e,r){return this.second+=e.interval!==void 0&&e.interval>1&&r?e.interval:1,this.ms=0,this.apply(),this.recurse(t,e,0)}getDate(t){return t||this.tz===void 0?new Date(this.year,this.month,this.day,this.hour,this.minute,this.second,this.ms):typeof this.tz=="number"?new Date(Date.UTC(this.year,this.month,this.day,this.hour,this.minute-this.tz,this.second,this.ms)):h.fromTZ(h.tp(this.year,this.month+1,this.day,this.hour,this.minute,this.second,this.tz),!1)}getTime(){return this.getDate(!1).getTime()}};function N(n){if(n===void 0&&(n={}),delete n.name,n.legacyMode=n.legacyMode===void 0?!0:n.legacyMode,n.paused=n.paused===void 0?!1:n.paused,n.maxRuns=n.maxRuns===void 0?1/0:n.maxRuns,n.catch=n.catch===void 0?!1:n.catch,n.interval=n.interval===void 0?0:parseInt(n.interval.toString(),10),n.utcOffset=n.utcOffset===void 0?void 0:parseInt(n.utcOffset.toString(),10),n.unref=n.unref===void 0?!1:n.unref,n.startAt&&(n.startAt=new f(n.startAt,n.timezone)),n.stopAt&&(n.stopAt=new f(n.stopAt,n.timezone)),n.interval!==null){if(isNaN(n.interval))throw new Error("CronOptions: Supplied value for interval is not a number");if(n.interval<0)throw new Error("CronOptions: Supplied value for interval can not be negative")}if(n.utcOffset!==void 0){if(isNaN(n.utcOffset))throw new Error("CronOptions: Invalid value passed for utcOffset, should be number representing minutes offset from UTC.");if(n.utcOffset<-870||n.utcOffset>870)throw new Error("CronOptions: utcOffset out of bounds.");if(n.utcOffset!==void 0&&n.timezone)throw new Error("CronOptions: Combining 'utcOffset' with 'timezone' is not allowed.")}if(n.unref!==!0&&n.unref!==!1)throw new Error("CronOptions: Unref should be either true, false or undefined(false).");return n}function g(n){return Object.prototype.toString.call(n)==="[object Function]"||typeof n=="function"||n instanceof Function}function S(n){return g(n)}function P(n){typeof Deno<"u"&&typeof Deno.unrefTimer<"u"?Deno.unrefTimer(n):n&&typeof n.unref<"u"&&n.unref()}var _=30*1e3,y=[],R=class{name;options;_states;fn;constructor(t,e,r){let s,i;if(g(e))i=e;else if(typeof e=="object")s=e;else if(e!==void 0)throw new Error("Cron: Invalid argument passed for optionsIn. Should be one of function, or object (options).");if(g(r))i=r;else if(typeof r=="object")s=r;else if(r!==void 0)throw new Error("Cron: Invalid argument passed for funcIn. Should be one of function, or object (options).");if(this.name=s?.name,this.options=N(s),this._states={kill:!1,blocking:!1,previousRun:void 0,currentRun:void 0,once:void 0,currentTimeout:void 0,maxRuns:s?s.maxRuns:void 0,paused:s?s.paused:!1,pattern:new d("* * * * *")},t&&(t instanceof Date||typeof t=="string"&&t.indexOf(":")>0)?this._states.once=new f(t,this.options.timezone||this.options.utcOffset):this._states.pattern=new d(t,this.options.timezone),this.name){if(y.find(l=>l.name===this.name))throw new Error("Cron: Tried to initialize new named job '"+this.name+"', but name already taken.");y.push(this)}return i!==void 0&&S(i)&&(this.fn=i,this.schedule()),this}nextRun(t){let e=this._next(t);return e?e.getDate(!1):null}nextRuns(t,e){this._states.maxRuns!==void 0&&t>this._states.maxRuns&&(t=this._states.maxRuns);let r=[],s=e||this._states.currentRun||void 0;for(;t--&&(s=this.nextRun(s));)r.push(s);return r}getPattern(){return this._states.pattern?this._states.pattern.pattern:void 0}isRunning(){let t=this.nextRun(this._states.currentRun),e=!this._states.paused,r=this.fn!==void 0,s=!this._states.kill;return e&&r&&s&&t!==null}isStopped(){return this._states.kill}isBusy(){return this._states.blocking}currentRun(){return this._states.currentRun?this._states.currentRun.getDate():null}previousRun(){return this._states.previousRun?this._states.previousRun.getDate():null}msToNext(t){let e=this._next(t);return e?t instanceof f||t instanceof Date?e.getTime()-t.getTime():e.getTime()-new f(t).getTime():null}stop(){this._states.kill=!0,this._states.currentTimeout&&clearTimeout(this._states.currentTimeout);let t=y.indexOf(this);t>=0&&y.splice(t,1)}pause(){return this._states.paused=!0,!this._states.kill}resume(){return this._states.paused=!1,!this._states.kill}schedule(t){if(t&&this.fn)throw new Error("Cron: It is not allowed to schedule two functions using the same Croner instance.");t&&(this.fn=t);let e=this.msToNext(),r=this.nextRun(this._states.currentRun);return e==null||isNaN(e)||r===null?this:(e>_&&(e=_),this._states.currentTimeout=setTimeout(()=>this._checkTrigger(r),e),this._states.currentTimeout&&this.options.unref&&P(this._states.currentTimeout),this)}async _trigger(t){if(this._states.blocking=!0,this._states.currentRun=new f(void 0,this.options.timezone||this.options.utcOffset),this.options.catch)try{this.fn!==void 0&&await this.fn(this,this.options.context)}catch(e){g(this.options.catch)&&this.options.catch(e,this)}else this.fn!==void 0&&await this.fn(this,this.options.context);this._states.previousRun=new f(t,this.options.timezone||this.options.utcOffset),this._states.blocking=!1}async trigger(){await this._trigger()}runsLeft(){return this._states.maxRuns}_checkTrigger(t){let e=new Date,r=!this._states.paused&&e.getTime()>=t.getTime(),s=this._states.blocking&&this.options.protect;r&&!s?(this._states.maxRuns!==void 0&&this._states.maxRuns--,this._trigger()):r&&s&&g(this.options.protect)&&setTimeout(()=>this.options.protect(this),0),this.schedule()}_next(t){let e=!!(t||this._states.currentRun),r=!1;!t&&this.options.startAt&&this.options.interval&&([t,e]=this._calculatePreviousRun(t,e),r=!t),t=new f(t,this.options.timezone||this.options.utcOffset),this.options.startAt&&t&&t.getTime()<this.options.startAt.getTime()&&(t=this.options.startAt);let s=this._states.once||new f(t,this.options.timezone||this.options.utcOffset);return!r&&s!==this._states.once&&(s=s.increment(this._states.pattern,this.options,e)),this._states.once&&this._states.once.getTime()<=t.getTime()||s===null||this._states.maxRuns!==void 0&&this._states.maxRuns<=0||this._states.kill||this.options.stopAt&&s.getTime()>=this.options.stopAt.getTime()?null:s}_calculatePreviousRun(t,e){let r=new f(void 0,this.options.timezone||this.options.utcOffset),s=t;if(this.options.startAt.getTime()<=r.getTime()){s=this.options.startAt;let i=s.getTime()+this.options.interval*1e3;for(;i<=r.getTime();)s=new f(s,this.options.timezone||this.options.utcOffset).increment(this._states.pattern,this.options,!0),i=s.getTime()+this.options.interval*1e3;e=!0}return s===null&&(s=void 0),[s,e]}};export{R as Cron,f as CronDate,d as CronPattern,y as scheduledJobs}; | ||
| function T(s){return Date.UTC(s.y,s.m-1,s.d,s.h,s.i,s.s)}function D(s,e){return s.y===e.y&&s.m===e.m&&s.d===e.d&&s.h===e.h&&s.i===e.i&&s.s===e.s}function A(s,e){let t=new Date(Date.parse(s));if(isNaN(t))throw new Error("Invalid ISO8601 passed to timezone parser.");let r=s.substring(9);return r.includes("Z")||r.includes("+")||r.includes("-")?b(t.getUTCFullYear(),t.getUTCMonth()+1,t.getUTCDate(),t.getUTCHours(),t.getUTCMinutes(),t.getUTCSeconds(),"Etc/UTC"):b(t.getFullYear(),t.getMonth()+1,t.getDate(),t.getHours(),t.getMinutes(),t.getSeconds(),e)}function v(s,e,t){return k(A(s,e),t)}function k(s,e){let t=new Date(T(s)),r=g(t,s.tz),n=T(s),i=T(r),a=n-i,o=new Date(t.getTime()+a),h=g(o,s.tz);if(D(h,s)){let u=new Date(o.getTime()-36e5),d=g(u,s.tz);return D(d,s)?u:o}let l=new Date(o.getTime()+T(s)-T(h)),y=g(l,s.tz);if(D(y,s))return l;if(e)throw new Error("Invalid date passed to fromTZ()");return o.getTime()>l.getTime()?o:l}function g(s,e){let t,r;try{t=new Intl.DateTimeFormat("en-US",{timeZone:e,year:"numeric",month:"numeric",day:"numeric",hour:"numeric",minute:"numeric",second:"numeric",hour12:!1}),r=t.formatToParts(s)}catch(i){let a=i instanceof Error?i.message:String(i);throw new RangeError(`toTZ: Invalid timezone '${e}' or date. Please provide a valid IANA timezone (e.g., 'America/New_York', 'Europe/Stockholm'). Original error: ${a}`)}let n={year:0,month:0,day:0,hour:0,minute:0,second:0};for(let i of r)(i.type==="year"||i.type==="month"||i.type==="day"||i.type==="hour"||i.type==="minute"||i.type==="second")&&(n[i.type]=parseInt(i.value,10));if(isNaN(n.year)||isNaN(n.month)||isNaN(n.day)||isNaN(n.hour)||isNaN(n.minute)||isNaN(n.second))throw new Error(`toTZ: Failed to parse all date components from timezone '${e}'. This may indicate an invalid date or timezone configuration. Parsed components: ${JSON.stringify(n)}`);return n.hour===24&&(n.hour=0),{y:n.year,m:n.month,d:n.day,h:n.hour,i:n.minute,s:n.second,tz:e}}function b(s,e,t,r,n,i,a){return{y:s,m:e,d:t,h:r,i:n,s:i,tz:a}}var O=[1,2,4,8,16],C=class{pattern;timezone;mode;alternativeWeekdays;sloppyRanges;second;minute;hour;day;month;dayOfWeek;year;lastDayOfMonth;lastWeekday;nearestWeekdays;starDOM;starDOW;starYear;useAndLogic;constructor(e,t,r){this.pattern=e,this.timezone=t,this.mode=r?.mode??"auto",this.alternativeWeekdays=r?.alternativeWeekdays??!1,this.sloppyRanges=r?.sloppyRanges??!1,this.second=Array(60).fill(0),this.minute=Array(60).fill(0),this.hour=Array(24).fill(0),this.day=Array(31).fill(0),this.month=Array(12).fill(0),this.dayOfWeek=Array(7).fill(0),this.year=Array(1e4).fill(0),this.lastDayOfMonth=!1,this.lastWeekday=!1,this.nearestWeekdays=Array(31).fill(0),this.starDOM=!1,this.starDOW=!1,this.starYear=!1,this.useAndLogic=!1,this.parse()}parse(){if(!(typeof this.pattern=="string"||this.pattern instanceof String))throw new TypeError("CronPattern: Pattern has to be of type string.");this.pattern.indexOf("@")>=0&&(this.pattern=this.handleNicknames(this.pattern).trim());let e=this.pattern.match(/\S+/g)||[""],t=e.length;if(e.length<5||e.length>7)throw new TypeError("CronPattern: invalid configuration format ('"+this.pattern+"'), exactly five, six, or seven space separated parts are required.");if(this.mode!=="auto"){let n;switch(this.mode){case"5-part":n=5;break;case"6-part":n=6;break;case"7-part":n=7;break;case"5-or-6-parts":n=[5,6];break;case"6-or-7-parts":n=[6,7];break;default:n=0}if(!(Array.isArray(n)?n.includes(t):t===n)){let a=Array.isArray(n)?n.join(" or "):n.toString();throw new TypeError(`CronPattern: mode '${this.mode}' requires exactly ${a} parts, but pattern '${this.pattern}' has ${t} parts.`)}}if(e.length===5&&e.unshift("0"),e.length===6&&e.push("*"),e[3].toUpperCase()==="LW"?(this.lastWeekday=!0,e[3]=""):e[3].toUpperCase().indexOf("L")>=0&&(e[3]=e[3].replace(/L/gi,""),this.lastDayOfMonth=!0),e[3]=="*"&&(this.starDOM=!0),e[6]=="*"&&(this.starYear=!0),e[4].length>=3&&(e[4]=this.replaceAlphaMonths(e[4])),e[5].length>=3&&(e[5]=this.alternativeWeekdays?this.replaceAlphaDaysQuartz(e[5]):this.replaceAlphaDays(e[5])),e[5].startsWith("+")&&(this.useAndLogic=!0,e[5]=e[5].substring(1),e[5]===""))throw new TypeError("CronPattern: Day-of-week field cannot be empty after '+' modifier.");switch(e[5]=="*"&&(this.starDOW=!0),this.pattern.indexOf("?")>=0&&(e[0]=e[0].replace(/\?/g,"*"),e[1]=e[1].replace(/\?/g,"*"),e[2]=e[2].replace(/\?/g,"*"),e[3]=e[3].replace(/\?/g,"*"),e[4]=e[4].replace(/\?/g,"*"),e[5]=e[5].replace(/\?/g,"*"),e[6]&&(e[6]=e[6].replace(/\?/g,"*"))),this.mode){case"5-part":e[0]="0",e[6]="*";break;case"6-part":e[6]="*";break;case"5-or-6-parts":e[6]="*";break;case"6-or-7-parts":break;case"7-part":case"auto":break}this.throwAtIllegalCharacters(e),this.partToArray("second",e[0],0,1),this.partToArray("minute",e[1],0,1),this.partToArray("hour",e[2],0,1),this.partToArray("day",e[3],-1,1),this.partToArray("month",e[4],-1,1);let r=this.alternativeWeekdays?-1:0;this.partToArray("dayOfWeek",e[5],r,63),this.partToArray("year",e[6],0,1),!this.alternativeWeekdays&&this.dayOfWeek[7]&&(this.dayOfWeek[0]=this.dayOfWeek[7])}partToArray(e,t,r,n){let i=this[e],a=e==="day"&&this.lastDayOfMonth,o=e==="day"&&this.lastWeekday;if(t===""&&!a&&!o)throw new TypeError("CronPattern: configuration entry "+e+" ("+t+") is empty, check for trailing spaces.");if(t==="*")return i.fill(n);let h=t.split(",");if(h.length>1)for(let l=0;l<h.length;l++)this.partToArray(e,h[l],r,n);else t.indexOf("-")!==-1&&t.indexOf("/")!==-1?this.handleRangeWithStepping(t,e,r,n):t.indexOf("-")!==-1?this.handleRange(t,e,r,n):t.indexOf("/")!==-1?this.handleStepping(t,e,r,n):t!==""&&this.handleNumber(t,e,r,n)}throwAtIllegalCharacters(e){for(let t=0;t<e.length;t++)if((t===3?/[^/*0-9,\-WwLl]+/:t===5?/[^/*0-9,\-#Ll]+/:/[^/*0-9,\-]+/).test(e[t]))throw new TypeError("CronPattern: configuration entry "+t+" ("+e[t]+") contains illegal characters.")}handleNumber(e,t,r,n){let i=this.extractNth(e,t),a=e.toUpperCase().includes("W");if(t!=="day"&&a)throw new TypeError("CronPattern: Nearest weekday modifier (W) only allowed in day-of-month.");a&&(t="nearestWeekdays");let o=parseInt(i[0],10)+r;if(isNaN(o))throw new TypeError("CronPattern: "+t+" is not a number: '"+e+"'");this.setPart(t,o,i[1]||n)}setPart(e,t,r){if(!Object.prototype.hasOwnProperty.call(this,e))throw new TypeError("CronPattern: Invalid part specified: "+e);if(e==="dayOfWeek"){if(t===7&&(t=0),t<0||t>6)throw new RangeError("CronPattern: Invalid value for dayOfWeek: "+t);this.setNthWeekdayOfMonth(t,r);return}if(e==="second"||e==="minute"){if(t<0||t>=60)throw new RangeError("CronPattern: Invalid value for "+e+": "+t)}else if(e==="hour"){if(t<0||t>=24)throw new RangeError("CronPattern: Invalid value for "+e+": "+t)}else if(e==="day"||e==="nearestWeekdays"){if(t<0||t>=31)throw new RangeError("CronPattern: Invalid value for "+e+": "+t)}else if(e==="month"){if(t<0||t>=12)throw new RangeError("CronPattern: Invalid value for "+e+": "+t)}else if(e==="year"&&(t<1||t>=1e4))throw new RangeError("CronPattern: Invalid value for "+e+": "+t+" (supported range: 1-9999)");this[e][t]=r}validateNotNaN(e,t){if(isNaN(e))throw new TypeError(t)}validateRange(e,t,r,n,i){if(e>t)throw new TypeError("CronPattern: From value is larger than to value: '"+i+"'");if(r!==void 0){if(r===0)throw new TypeError("CronPattern: Syntax error, illegal stepping: 0");if(r>this[n].length)throw new TypeError("CronPattern: Syntax error, steps cannot be greater than maximum value of part ("+this[n].length+")")}}handleRangeWithStepping(e,t,r,n){if(e.toUpperCase().includes("W"))throw new TypeError("CronPattern: Syntax error, W is not allowed in ranges with stepping.");let i=this.extractNth(e,t),a=i[0].match(/^(\d+)-(\d+)\/(\d+)$/);if(a===null)throw new TypeError("CronPattern: Syntax error, illegal range with stepping: '"+e+"'");let[,o,h,l]=a,y=parseInt(o,10)+r,u=parseInt(h,10)+r,d=parseInt(l,10);this.validateNotNaN(y,"CronPattern: Syntax error, illegal lower range (NaN)"),this.validateNotNaN(u,"CronPattern: Syntax error, illegal upper range (NaN)"),this.validateNotNaN(d,"CronPattern: Syntax error, illegal stepping: (NaN)"),this.validateRange(y,u,d,t,e);for(let c=y;c<=u;c+=d)this.setPart(t,c,i[1]||n)}extractNth(e,t){let r=e,n;if(r.includes("#")){if(t!=="dayOfWeek")throw new Error("CronPattern: nth (#) only allowed in day-of-week field");n=r.split("#")[1],r=r.split("#")[0]}else if(r.toUpperCase().endsWith("L")){if(t!=="dayOfWeek")throw new Error("CronPattern: L modifier only allowed in day-of-week field (use L alone for day-of-month)");n="L",r=r.slice(0,-1)}return[r,n]}handleRange(e,t,r,n){if(e.toUpperCase().includes("W"))throw new TypeError("CronPattern: Syntax error, W is not allowed in a range.");let i=this.extractNth(e,t),a=i[0].split("-");if(a.length!==2)throw new TypeError("CronPattern: Syntax error, illegal range: '"+e+"'");let o=parseInt(a[0],10)+r,h=parseInt(a[1],10)+r;this.validateNotNaN(o,"CronPattern: Syntax error, illegal lower range (NaN)"),this.validateNotNaN(h,"CronPattern: Syntax error, illegal upper range (NaN)"),this.validateRange(o,h,void 0,t,e);for(let l=o;l<=h;l++)this.setPart(t,l,i[1]||n)}handleStepping(e,t,r,n){if(e.toUpperCase().includes("W"))throw new TypeError("CronPattern: Syntax error, W is not allowed in parts with stepping.");let i=this.extractNth(e,t),a=i[0].split("/");if(a.length!==2)throw new TypeError("CronPattern: Syntax error, illegal stepping: '"+e+"'");if(this.sloppyRanges)a[0]===""&&(a[0]="*");else{if(a[0]==="")throw new TypeError("CronPattern: Syntax error, stepping with missing prefix ('"+e+"') is not allowed. Use wildcard (*/step) or range (min-max/step) instead.");if(a[0]!=="*")throw new TypeError("CronPattern: Syntax error, stepping with numeric prefix ('"+e+"') is not allowed. Use wildcard (*/step) or range (min-max/step) instead.")}let o=0;a[0]!=="*"&&(o=parseInt(a[0],10)+r);let h=parseInt(a[1],10);this.validateNotNaN(h,"CronPattern: Syntax error, illegal stepping: (NaN)"),this.validateRange(0,this[t].length-1,h,t,e);for(let l=o;l<this[t].length;l+=h)this.setPart(t,l,i[1]||n)}replaceAlphaDays(e){return e.replace(/-sun/gi,"-7").replace(/sun/gi,"0").replace(/mon/gi,"1").replace(/tue/gi,"2").replace(/wed/gi,"3").replace(/thu/gi,"4").replace(/fri/gi,"5").replace(/sat/gi,"6")}replaceAlphaDaysQuartz(e){return e.replace(/sun/gi,"1").replace(/mon/gi,"2").replace(/tue/gi,"3").replace(/wed/gi,"4").replace(/thu/gi,"5").replace(/fri/gi,"6").replace(/sat/gi,"7")}replaceAlphaMonths(e){return e.replace(/jan/gi,"1").replace(/feb/gi,"2").replace(/mar/gi,"3").replace(/apr/gi,"4").replace(/may/gi,"5").replace(/jun/gi,"6").replace(/jul/gi,"7").replace(/aug/gi,"8").replace(/sep/gi,"9").replace(/oct/gi,"10").replace(/nov/gi,"11").replace(/dec/gi,"12")}handleNicknames(e){let t=e.trim().toLowerCase();if(t==="@yearly"||t==="@annually")return"0 0 1 1 *";if(t==="@monthly")return"0 0 1 * *";if(t==="@weekly")return"0 0 * * 0";if(t==="@daily"||t==="@midnight")return"0 0 * * *";if(t==="@hourly")return"0 * * * *";if(t==="@reboot")throw new TypeError("CronPattern: @reboot is not supported in this environment. This is an event-based trigger that requires system startup detection.");return e}setNthWeekdayOfMonth(e,t){if(typeof t!="number"&&t.toUpperCase()==="L")this.dayOfWeek[e]=this.dayOfWeek[e]|32;else if(t===63)this.dayOfWeek[e]=63;else if(t<6&&t>0)this.dayOfWeek[e]=this.dayOfWeek[e]|O[t-1];else throw new TypeError(`CronPattern: nth weekday out of range, should be 1-5 or L. Value: ${t}, Type: ${typeof t}`)}};var P=[31,28,31,30,31,30,31,31,30,31,30,31],f=[["month","year",0],["day","month",-1],["hour","day",0],["minute","hour",0],["second","minute",0]],m=class s{tz;ms;second;minute;hour;day;month;year;constructor(e,t){if(this.tz=t,e&&e instanceof Date)if(!isNaN(e))this.fromDate(e);else throw new TypeError("CronDate: Invalid date passed to CronDate constructor");else if(e==null)this.fromDate(new Date);else if(e&&typeof e=="string")this.fromString(e);else if(e instanceof s)this.fromCronDate(e);else throw new TypeError("CronDate: Invalid type ("+typeof e+") passed to CronDate constructor")}getLastDayOfMonth(e,t){return t!==1?P[t]:new Date(Date.UTC(e,t+1,0)).getUTCDate()}getLastWeekday(e,t){let r=this.getLastDayOfMonth(e,t),i=new Date(Date.UTC(e,t,r)).getUTCDay();return i===0?r-2:i===6?r-1:r}getNearestWeekday(e,t,r){let n=this.getLastDayOfMonth(e,t);if(r>n)return-1;let a=new Date(Date.UTC(e,t,r)).getUTCDay();return a===0?r===n?r-2:r+1:a===6?r===1?r+2:r-1:r}isNthWeekdayOfMonth(e,t,r,n){let a=new Date(Date.UTC(e,t,r)).getUTCDay(),o=0;for(let h=1;h<=r;h++)new Date(Date.UTC(e,t,h)).getUTCDay()===a&&o++;if(n&63&&O[o-1]&n)return!0;if(n&32){let h=this.getLastDayOfMonth(e,t);for(let l=r+1;l<=h;l++)if(new Date(Date.UTC(e,t,l)).getUTCDay()===a)return!1;return!0}return!1}fromDate(e){if(this.tz!==void 0)if(typeof this.tz=="number")this.ms=e.getUTCMilliseconds(),this.second=e.getUTCSeconds(),this.minute=e.getUTCMinutes()+this.tz,this.hour=e.getUTCHours(),this.day=e.getUTCDate(),this.month=e.getUTCMonth(),this.year=e.getUTCFullYear(),this.apply();else try{let t=g(e,this.tz);this.ms=e.getMilliseconds(),this.second=t.s,this.minute=t.i,this.hour=t.h,this.day=t.d,this.month=t.m-1,this.year=t.y}catch(t){let r=t instanceof Error?t.message:String(t);throw new TypeError(`CronDate: Failed to convert date to timezone '${this.tz}'. This may happen with invalid timezone names or dates. Original error: ${r}`)}else this.ms=e.getMilliseconds(),this.second=e.getSeconds(),this.minute=e.getMinutes(),this.hour=e.getHours(),this.day=e.getDate(),this.month=e.getMonth(),this.year=e.getFullYear()}fromCronDate(e){this.tz=e.tz,this.year=e.year,this.month=e.month,this.day=e.day,this.hour=e.hour,this.minute=e.minute,this.second=e.second,this.ms=e.ms}apply(){if(this.month>11||this.month<0||this.day>P[this.month]||this.day<1||this.hour>59||this.minute>59||this.second>59||this.hour<0||this.minute<0||this.second<0){let e=new Date(Date.UTC(this.year,this.month,this.day,this.hour,this.minute,this.second,this.ms));return this.ms=e.getUTCMilliseconds(),this.second=e.getUTCSeconds(),this.minute=e.getUTCMinutes(),this.hour=e.getUTCHours(),this.day=e.getUTCDate(),this.month=e.getUTCMonth(),this.year=e.getUTCFullYear(),!0}else return!1}fromString(e){if(typeof this.tz=="number"){let t=v(e);this.ms=t.getUTCMilliseconds(),this.second=t.getUTCSeconds(),this.minute=t.getUTCMinutes(),this.hour=t.getUTCHours(),this.day=t.getUTCDate(),this.month=t.getUTCMonth(),this.year=t.getUTCFullYear(),this.apply()}else return this.fromDate(v(e,this.tz))}findNext(e,t,r,n){return this._findMatch(e,t,r,n,1)}_findMatch(e,t,r,n,i){let a=this[t],o;r.lastDayOfMonth&&(o=this.getLastDayOfMonth(this.year,this.month));let h=!r.starDOW&&t=="day"?new Date(Date.UTC(this.year,this.month,1,0,0,0,0)).getUTCDay():void 0,l=this[t]+n,y=i===1?u=>u<r[t].length:u=>u>=0;for(let u=l;y(u);u+=i){let d=r[t][u];if(t==="day"&&!d){for(let c=0;c<r.nearestWeekdays.length;c++)if(r.nearestWeekdays[c]){let M=this.getNearestWeekday(this.year,this.month,c-n);if(M===-1)continue;if(M===u-n){d=1;break}}}if(t==="day"&&r.lastWeekday){let c=this.getLastWeekday(this.year,this.month);u-n===c&&(d=1)}if(t==="day"&&r.lastDayOfMonth&&u-n==o&&(d=1),t==="day"&&!r.starDOW){let c=r.dayOfWeek[(h+(u-n-1))%7];if(c&&c&63)c=this.isNthWeekdayOfMonth(this.year,this.month,u-n,c)?1:0;else if(c)throw new Error(`CronDate: Invalid value for dayOfWeek encountered. ${c}`);r.useAndLogic?d=d&&c:!e.domAndDow&&!r.starDOM?d=d||c:d=d&&c}if(d)return this[t]=u-n,a!==this[t]?2:1}return 3}recurse(e,t,r){if(r===0&&!e.starYear){if(this.year>=0&&this.year<e.year.length&&e.year[this.year]===0){let i=-1;for(let a=this.year+1;a<e.year.length&&a<1e4;a++)if(e.year[a]===1){i=a;break}if(i===-1)return null;this.year=i,this.month=0,this.day=1,this.hour=0,this.minute=0,this.second=0,this.ms=0}if(this.year>=1e4)return null}let n=this.findNext(t,f[r][0],e,f[r][2]);if(n>1){let i=r+1;for(;i<f.length;)this[f[i][0]]=-f[i][2],i++;if(n===3){if(this[f[r][1]]++,this[f[r][0]]=-f[r][2],this.apply(),r===0&&!e.starYear){for(;this.year>=0&&this.year<e.year.length&&e.year[this.year]===0&&this.year<1e4;)this.year++;if(this.year>=1e4||this.year>=e.year.length)return null}return this.recurse(e,t,0)}else if(this.apply())return this.recurse(e,t,r-1)}return r+=1,r>=f.length?this:(e.starYear?this.year>=3e3:this.year>=1e4)?null:this.recurse(e,t,r)}increment(e,t,r){return this.second+=t.interval!==void 0&&t.interval>1&&r?t.interval:1,this.ms=0,this.apply(),this.recurse(e,t,0)}decrement(e,t){return this.second-=t.interval!==void 0&&t.interval>1?t.interval:1,this.ms=0,this.apply(),this.recurseBackward(e,t,0,0)}recurseBackward(e,t,r,n=0){if(n>1e4)return null;if(r===0&&!e.starYear){if(this.year>=0&&this.year<e.year.length&&e.year[this.year]===0){let a=-1;for(let o=this.year-1;o>=0;o--)if(e.year[o]===1){a=o;break}if(a===-1)return null;this.year=a,this.month=11,this.day=31,this.hour=23,this.minute=59,this.second=59,this.ms=0}if(this.year<0)return null}let i=this.findPrevious(t,f[r][0],e,f[r][2]);if(i>1){let a=r+1;for(;a<f.length;){let o=f[a][0],h=f[a][2],l=this.getMaxPatternValue(o,e,h);this[o]=l,a++}if(i===3){if(this[f[r][1]]--,r===0){let y=this.getLastDayOfMonth(this.year,this.month);this.day>y&&(this.day=y)}if(r===1)if(this.day<=0)this.day=1;else{let y=this.year,u=this.month;for(;u<0;)u+=12,y--;for(;u>11;)u-=12,y++;let d=u!==1?P[u]:new Date(Date.UTC(y,u+1,0)).getUTCDate();this.day>d&&(this.day=d)}this.apply();let o=f[r][0],h=f[r][2],l=this.getMaxPatternValue(o,e,h);if(o==="day"){let y=this.getLastDayOfMonth(this.year,this.month);this[o]=Math.min(l,y)}else this[o]=l;if(this.apply(),r===0){let y=f[1][2],u=this.getMaxPatternValue("day",e,y),d=this.getLastDayOfMonth(this.year,this.month),c=Math.min(u,d);c!==this.day&&(this.day=c,this.hour=this.getMaxPatternValue("hour",e,f[2][2]),this.minute=this.getMaxPatternValue("minute",e,f[3][2]),this.second=this.getMaxPatternValue("second",e,f[4][2]))}if(r===0&&!e.starYear){for(;this.year>=0&&this.year<e.year.length&&e.year[this.year]===0;)this.year--;if(this.year<0)return null}return this.recurseBackward(e,t,0,n+1)}else if(this.apply())return this.recurseBackward(e,t,r-1,n+1)}return r+=1,r>=f.length?this:this.year<0?null:this.recurseBackward(e,t,r,n+1)}getMaxPatternValue(e,t,r){if(e==="day"&&t.lastDayOfMonth)return this.getLastDayOfMonth(this.year,this.month);if(e==="day"&&!t.starDOW)return this.getLastDayOfMonth(this.year,this.month);for(let n=t[e].length-1;n>=0;n--)if(t[e][n])return n-r;return t[e].length-1-r}findPrevious(e,t,r,n){return this._findMatch(e,t,r,n,-1)}getDate(e){return e||this.tz===void 0?new Date(this.year,this.month,this.day,this.hour,this.minute,this.second,this.ms):typeof this.tz=="number"?new Date(Date.UTC(this.year,this.month,this.day,this.hour,this.minute-this.tz,this.second,this.ms)):k(b(this.year,this.month+1,this.day,this.hour,this.minute,this.second,this.tz),!1)}getTime(){return this.getDate(!1).getTime()}match(e,t){if(!e.starYear&&(this.year<0||this.year>=e.year.length||e.year[this.year]===0))return!1;for(let r=0;r<f.length;r++){let n=f[r][0],i=f[r][2],a=this[n];if(a+i<0||a+i>=e[n].length)return!1;let o=e[n][a+i];if(n==="day"){if(!o){for(let h=0;h<e.nearestWeekdays.length;h++)if(e.nearestWeekdays[h]){let l=this.getNearestWeekday(this.year,this.month,h-i);if(l!==-1&&l===a){o=1;break}}}if(e.lastWeekday){let h=this.getLastWeekday(this.year,this.month);a===h&&(o=1)}if(e.lastDayOfMonth){let h=this.getLastDayOfMonth(this.year,this.month);a===h&&(o=1)}if(!e.starDOW){let h=new Date(Date.UTC(this.year,this.month,1,0,0,0,0)).getUTCDay(),l=e.dayOfWeek[(h+(a-1))%7];l&&l&63&&(l=this.isNthWeekdayOfMonth(this.year,this.month,a,l)?1:0),e.useAndLogic?o=o&&l:!t.domAndDow&&!e.starDOM?o=o||l:o=o&&l}}if(!o)return!1}return!0}};function R(s){if(s===void 0&&(s={}),delete s.name,s.legacyMode!==void 0&&s.domAndDow===void 0?s.domAndDow=!s.legacyMode:s.domAndDow===void 0&&(s.domAndDow=!1),s.legacyMode=!s.domAndDow,s.paused=s.paused===void 0?!1:s.paused,s.maxRuns=s.maxRuns===void 0?1/0:s.maxRuns,s.catch=s.catch===void 0?!1:s.catch,s.interval=s.interval===void 0?0:parseInt(s.interval.toString(),10),s.utcOffset=s.utcOffset===void 0?void 0:parseInt(s.utcOffset.toString(),10),s.dayOffset=s.dayOffset===void 0?0:parseInt(s.dayOffset.toString(),10),s.unref=s.unref===void 0?!1:s.unref,s.mode=s.mode===void 0?"auto":s.mode,s.alternativeWeekdays=s.alternativeWeekdays===void 0?!1:s.alternativeWeekdays,s.sloppyRanges=s.sloppyRanges===void 0?!1:s.sloppyRanges,!["auto","5-part","6-part","7-part","5-or-6-parts","6-or-7-parts"].includes(s.mode))throw new Error("CronOptions: mode must be one of 'auto', '5-part', '6-part', '7-part', '5-or-6-parts', or '6-or-7-parts'.");if(s.startAt&&(s.startAt=new m(s.startAt,s.timezone)),s.stopAt&&(s.stopAt=new m(s.stopAt,s.timezone)),s.interval!==null){if(isNaN(s.interval))throw new Error("CronOptions: Supplied value for interval is not a number");if(s.interval<0)throw new Error("CronOptions: Supplied value for interval can not be negative")}if(s.utcOffset!==void 0){if(isNaN(s.utcOffset))throw new Error("CronOptions: Invalid value passed for utcOffset, should be number representing minutes offset from UTC.");if(s.utcOffset<-870||s.utcOffset>870)throw new Error("CronOptions: utcOffset out of bounds.");if(s.utcOffset!==void 0&&s.timezone)throw new Error("CronOptions: Combining 'utcOffset' with 'timezone' is not allowed.")}if(s.unref!==!0&&s.unref!==!1)throw new Error("CronOptions: Unref should be either true, false or undefined(false).");if(s.dayOffset!==void 0&&s.dayOffset!==0&&isNaN(s.dayOffset))throw new Error("CronOptions: Invalid value passed for dayOffset, should be a number representing days to offset.");return s}function p(s){return Object.prototype.toString.call(s)==="[object Function]"||typeof s=="function"||s instanceof Function}function _(s){return p(s)}function x(s){typeof Deno<"u"&&typeof Deno.unrefTimer<"u"?Deno.unrefTimer(s):s&&typeof s.unref<"u"&&s.unref()}var W=30*1e3,w=[],E=class{name;options;_states;fn;getTz(){return this.options.timezone||this.options.utcOffset}applyDayOffset(e){if(this.options.dayOffset!==void 0&&this.options.dayOffset!==0){let t=this.options.dayOffset*24*60*60*1e3;return new Date(e.getTime()+t)}return e}constructor(e,t,r){let n,i;if(p(t))i=t;else if(typeof t=="object")n=t;else if(t!==void 0)throw new Error("Cron: Invalid argument passed for optionsIn. Should be one of function, or object (options).");if(p(r))i=r;else if(typeof r=="object")n=r;else if(r!==void 0)throw new Error("Cron: Invalid argument passed for funcIn. Should be one of function, or object (options).");if(this.name=n?.name,this.options=R(n),this._states={kill:!1,blocking:!1,previousRun:void 0,currentRun:void 0,once:void 0,currentTimeout:void 0,maxRuns:n?n.maxRuns:void 0,paused:n?n.paused:!1,pattern:new C("* * * * *",void 0,{mode:"auto"})},e&&(e instanceof Date||typeof e=="string"&&e.indexOf(":")>0)?this._states.once=new m(e,this.getTz()):this._states.pattern=new C(e,this.options.timezone,{mode:this.options.mode,alternativeWeekdays:this.options.alternativeWeekdays,sloppyRanges:this.options.sloppyRanges}),this.name){if(w.find(o=>o.name===this.name))throw new Error("Cron: Tried to initialize new named job '"+this.name+"', but name already taken.");w.push(this)}return i!==void 0&&_(i)&&(this.fn=i,this.schedule()),this}nextRun(e){let t=this._next(e);return t?this.applyDayOffset(t.getDate(!1)):null}nextRuns(e,t){this._states.maxRuns!==void 0&&e>this._states.maxRuns&&(e=this._states.maxRuns);let r=t||this._states.currentRun||void 0;return this._enumerateRuns(e,r,"next")}previousRuns(e,t){return this._enumerateRuns(e,t||void 0,"previous")}_enumerateRuns(e,t,r){let n=[],i=t?new m(t,this.getTz()):null,a=r==="next"?this._next:this._previous;for(;e--;){let o=a.call(this,i);if(!o)break;let h=o.getDate(!1);n.push(this.applyDayOffset(h)),i=o}return n}match(e){if(this._states.once){let r=new m(e,this.getTz());r.ms=0;let n=new m(this._states.once,this.getTz());return n.ms=0,r.getTime()===n.getTime()}let t=new m(e,this.getTz());return t.ms=0,t.match(this._states.pattern,this.options)}getPattern(){if(!this._states.once)return this._states.pattern?this._states.pattern.pattern:void 0}getOnce(){return this._states.once?this._states.once.getDate():null}isRunning(){let e=this.nextRun(this._states.currentRun),t=!this._states.paused,r=this.fn!==void 0,n=!this._states.kill;return t&&r&&n&&e!==null}isStopped(){return this._states.kill}isBusy(){return this._states.blocking}currentRun(){return this._states.currentRun?this._states.currentRun.getDate():null}previousRun(){return this._states.previousRun?this._states.previousRun.getDate():null}msToNext(e){let t=this._next(e);return t?e instanceof m||e instanceof Date?t.getTime()-e.getTime():t.getTime()-new m(e).getTime():null}stop(){this._states.kill=!0,this._states.currentTimeout&&clearTimeout(this._states.currentTimeout);let e=w.indexOf(this);e>=0&&w.splice(e,1)}pause(){return this._states.paused=!0,!this._states.kill}resume(){return this._states.paused=!1,!this._states.kill}schedule(e){if(e&&this.fn)throw new Error("Cron: It is not allowed to schedule two functions using the same Croner instance.");e&&(this.fn=e);let t=this.msToNext(),r=this.nextRun(this._states.currentRun);return t==null||isNaN(t)||r===null?this:(t>W&&(t=W),this._states.currentTimeout=setTimeout(()=>this._checkTrigger(r),t),this._states.currentTimeout&&this.options.unref&&x(this._states.currentTimeout),this)}async _trigger(e){this._states.blocking=!0,this._states.currentRun=new m(void 0,this.getTz());try{if(this.options.catch)try{this.fn!==void 0&&await this.fn(this,this.options.context)}catch(t){if(p(this.options.catch))try{this.options.catch(t,this)}catch{}}else this.fn!==void 0&&await this.fn(this,this.options.context)}finally{this._states.previousRun=new m(e,this.getTz()),this._states.blocking=!1}}async trigger(){await this._trigger()}runsLeft(){return this._states.maxRuns}_checkTrigger(e){let t=new Date,r=!this._states.paused&&t.getTime()>=e.getTime(),n=this._states.blocking&&this.options.protect;r&&!n?(this._states.maxRuns!==void 0&&this._states.maxRuns--,this._trigger()):r&&n&&p(this.options.protect)&&setTimeout(()=>this.options.protect(this),0),this.schedule()}_next(e){let t=!!(e||this._states.currentRun),r=!1;!e&&this.options.startAt&&this.options.interval&&([e,t]=this._calculatePreviousRun(e,t),r=!e),e=new m(e,this.getTz()),this.options.startAt&&e&&e.getTime()<this.options.startAt.getTime()&&(e=this.options.startAt);let n=this._states.once||new m(e,this.getTz());return!r&&n!==this._states.once&&(n=n.increment(this._states.pattern,this.options,t)),this._states.once&&this._states.once.getTime()<=e.getTime()||n===null||this._states.maxRuns!==void 0&&this._states.maxRuns<=0||this._states.kill||this.options.stopAt&&n.getTime()>=this.options.stopAt.getTime()?null:n}_previous(e){let t=new m(e,this.getTz());this.options.stopAt&&t.getTime()>this.options.stopAt.getTime()&&(t=this.options.stopAt);let r=new m(t,this.getTz());return this._states.once?this._states.once.getTime()<t.getTime()?this._states.once:null:(r=r.decrement(this._states.pattern,this.options),r===null||this.options.startAt&&r.getTime()<this.options.startAt.getTime()?null:r)}_calculatePreviousRun(e,t){let r=new m(void 0,this.getTz()),n=e;if(this.options.startAt.getTime()<=r.getTime()){n=this.options.startAt;let i=n.getTime()+this.options.interval*1e3;for(;i<=r.getTime();)n=new m(n,this.getTz()).increment(this._states.pattern,this.options,!0),i=n.getTime()+this.options.interval*1e3;t=!0}return n===null&&(n=void 0),[n,t]}};export{E as Cron,m as CronDate,C as CronPattern,w as scheduledJobs}; |
@@ -19,2 +19,12 @@ { | ||
| }, | ||
| "funding": [ | ||
| { | ||
| "type": "other", | ||
| "url": "https://paypal.me/hexagonpp" | ||
| }, | ||
| { | ||
| "type": "github", | ||
| "url": "https://github.com/sponsors/hexagon" | ||
| } | ||
| ], | ||
| "files": [ | ||
@@ -63,3 +73,3 @@ "dist/*.js", | ||
| "license": "MIT", | ||
| "version": "9.1.0" | ||
| "version": "10.0.1" | ||
| } |
@@ -1,359 +0,212 @@ | ||
| import { | ||
| Layout | ||
| } from "./chunk-CM7DWJNZ.js"; | ||
| import { | ||
| ErrorCause | ||
| } from "./chunk-X53OIOJH.js"; | ||
| import { | ||
| ErrorInfo | ||
| } from "./chunk-OIJ3WD7L.js"; | ||
| import { | ||
| ErrorMetadata | ||
| } from "./chunk-P36L72PL.js"; | ||
| import { | ||
| ErrorStack | ||
| } from "./chunk-EJH674NB.js"; | ||
| import { | ||
| ErrorStackSource | ||
| } from "./chunk-7QV3D5YX.js"; | ||
| import "./chunk-4L7RY2JA.js"; | ||
| import { | ||
| Header | ||
| } from "./chunk-AUGPHE32.js"; | ||
| import { | ||
| BaseComponent | ||
| } from "./chunk-PE3GG3TN.js"; | ||
| // src/youch.ts | ||
| import { n as BaseComponent } from "./public_dir-C5bujZKB.js"; | ||
| import { Header } from "./src/templates/header/main.js"; | ||
| import { Layout } from "./src/templates/layout/main.js"; | ||
| import "./helpers-B9BQYaS6.js"; | ||
| import { ErrorInfo } from "./src/templates/error_info/main.js"; | ||
| import { ErrorCause } from "./src/templates/error_cause/main.js"; | ||
| import { ErrorStack } from "./src/templates/error_stack/main.js"; | ||
| import { ErrorMetadata } from "./src/templates/error_metadata/main.js"; | ||
| import { ErrorStackSource } from "./src/templates/error_stack_source/main.js"; | ||
| import { parse } from "cookie-es"; | ||
| import { ErrorParser } from "youch-core"; | ||
| // src/metadata.ts | ||
| import { createScript, createStyleSheet } from "@poppinss/dumper/html"; | ||
| var Metadata = class { | ||
| #groups = {}; | ||
| /** | ||
| * Converts value to an array (if not an array already) | ||
| */ | ||
| #toArray(value) { | ||
| return Array.isArray(value) ? value : [value]; | ||
| } | ||
| /** | ||
| * Define a group, its sections and their rows. In case of | ||
| * existing groups/sections, the new data will be merged | ||
| * with the existing data | ||
| */ | ||
| group(name, sections) { | ||
| this.#groups[name] = this.#groups[name] ?? {}; | ||
| Object.keys(sections).forEach((section) => { | ||
| if (!this.#groups[name][section]) { | ||
| this.#groups[name][section] = sections[section]; | ||
| } else { | ||
| this.#groups[name][section] = this.#toArray(this.#groups[name][section]); | ||
| this.#groups[name][section].push(...this.#toArray(sections[section])); | ||
| } | ||
| }); | ||
| return this; | ||
| } | ||
| /** | ||
| * Returns the existing metadata groups, sections and | ||
| * rows. | ||
| */ | ||
| toJSON() { | ||
| return this.#groups; | ||
| } | ||
| #groups = {}; | ||
| #toArray(value) { | ||
| return Array.isArray(value) ? value : [value]; | ||
| } | ||
| group(name, sections) { | ||
| this.#groups[name] = this.#groups[name] ?? {}; | ||
| Object.keys(sections).forEach((section) => { | ||
| if (!this.#groups[name][section]) this.#groups[name][section] = sections[section]; | ||
| else { | ||
| this.#groups[name][section] = this.#toArray(this.#groups[name][section]); | ||
| this.#groups[name][section].push(...this.#toArray(sections[section])); | ||
| } | ||
| }); | ||
| return this; | ||
| } | ||
| toJSON() { | ||
| return this.#groups; | ||
| } | ||
| }; | ||
| // src/templates.ts | ||
| import { createScript, createStyleSheet } from "@poppinss/dumper/html"; | ||
| var Templates = class { | ||
| constructor(devMode) { | ||
| this.devMode = devMode; | ||
| this.#knownTemplates = { | ||
| layout: new Layout(devMode), | ||
| header: new Header(devMode), | ||
| errorInfo: new ErrorInfo(devMode), | ||
| errorStack: new ErrorStack(devMode), | ||
| errorStackSource: new ErrorStackSource(devMode), | ||
| errorCause: new ErrorCause(devMode), | ||
| errorMetadata: new ErrorMetadata(devMode) | ||
| }; | ||
| } | ||
| #knownTemplates; | ||
| #styles = /* @__PURE__ */ new Map([["global", createStyleSheet()]]); | ||
| #scripts = /* @__PURE__ */ new Map([["global", createScript()]]); | ||
| /** | ||
| * Returns a collection of style and script tags to dump | ||
| * inside the document HEAD. | ||
| */ | ||
| #getStylesAndScripts(cspNonce) { | ||
| let customInjectedStyles = ""; | ||
| let globalScript = ""; | ||
| const styles = []; | ||
| const scripts = []; | ||
| const cspNonceAttr = cspNonce ? ` nonce="${cspNonce}"` : ""; | ||
| this.#styles.forEach((bucket, name) => { | ||
| if (name === "injected") { | ||
| customInjectedStyles = `<style id="${name}-styles"${cspNonceAttr}>${bucket}</style>`; | ||
| } else { | ||
| styles.push(`<style id="${name}-styles"${cspNonceAttr}>${bucket}</style>`); | ||
| } | ||
| }); | ||
| this.#scripts.forEach((bucket, name) => { | ||
| if (name === "global") { | ||
| globalScript = `<script id="${name}-script"${cspNonceAttr}>${bucket}</script>`; | ||
| } | ||
| scripts.push(`<script id="${name}-script"${cspNonceAttr}>${bucket}</script>`); | ||
| }); | ||
| return { | ||
| styles: `${styles.join("\n")} | ||
| ${customInjectedStyles}`, | ||
| scripts: scripts.join("\n"), | ||
| globalScript | ||
| }; | ||
| } | ||
| /** | ||
| * Collects styles and scripts for components as we render | ||
| * them. | ||
| */ | ||
| async #collectStylesAndScripts(templateName) { | ||
| if (!this.#styles.has(templateName)) { | ||
| const styles = await this.#knownTemplates[templateName].getStyles(); | ||
| if (styles) { | ||
| this.#styles.set(templateName, styles); | ||
| } | ||
| } | ||
| if (!this.#scripts.has(templateName)) { | ||
| const script = await this.#knownTemplates[templateName].getScript(); | ||
| if (script) { | ||
| this.#scripts.set(templateName, script); | ||
| } | ||
| } | ||
| } | ||
| /** | ||
| * Returns the HTML for a given template | ||
| */ | ||
| async #tmplToHTML(templateName, props) { | ||
| const component = this.#knownTemplates[templateName]; | ||
| if (!component) { | ||
| throw new Error(`Invalid template "${templateName}"`); | ||
| } | ||
| await this.#collectStylesAndScripts(templateName); | ||
| return component.toHTML(props); | ||
| } | ||
| /** | ||
| * Returns the ANSI output for a given template | ||
| */ | ||
| async #tmplToANSI(templateName, props) { | ||
| const component = this.#knownTemplates[templateName]; | ||
| if (!component) { | ||
| throw new Error(`Invalid template "${templateName}"`); | ||
| } | ||
| return component.toANSI(props); | ||
| } | ||
| /** | ||
| * Define a custom component to be used in place of the default component. | ||
| * Overriding components allows you control the HTML layout, styles and | ||
| * the frontend scripts of an HTML fragment. | ||
| */ | ||
| use(templateName, component) { | ||
| this.#knownTemplates[templateName] = component; | ||
| return this; | ||
| } | ||
| /** | ||
| * Inject custom styles to the document. Injected styles are | ||
| * always placed after the global and the components style | ||
| * tags. | ||
| */ | ||
| injectStyles(cssFragment) { | ||
| let injectedStyles = this.#styles.get("injected") ?? ""; | ||
| injectedStyles += ` | ||
| ${cssFragment}`; | ||
| this.#styles.set("injected", injectedStyles); | ||
| return this; | ||
| } | ||
| /** | ||
| * Returns the HTML output for the given parsed error | ||
| */ | ||
| async toHTML(props) { | ||
| const html = await this.#tmplToHTML("layout", { | ||
| title: props.title, | ||
| ide: props.ide, | ||
| cspNonce: props.cspNonce, | ||
| children: async () => { | ||
| const header = await this.#tmplToHTML("header", props); | ||
| const info = await this.#tmplToHTML("errorInfo", props); | ||
| const stackTrace = await this.#tmplToHTML("errorStack", { | ||
| ide: process.env.EDITOR ?? "vscode", | ||
| sourceCodeRenderer: (error, frame) => { | ||
| return this.#tmplToHTML("errorStackSource", { | ||
| error, | ||
| frame, | ||
| ide: props.ide, | ||
| cspNonce: props.cspNonce | ||
| }); | ||
| }, | ||
| ...props | ||
| }); | ||
| const cause = await this.#tmplToHTML("errorCause", props); | ||
| const metadata = await this.#tmplToHTML("errorMetadata", props); | ||
| return `${header}${info}${stackTrace}${cause}${metadata}`; | ||
| } | ||
| }); | ||
| const { globalScript, scripts, styles } = this.#getStylesAndScripts(props.cspNonce); | ||
| return html.replace("<!-- STYLES -->", styles).replace("<!-- SCRIPTS -->", scripts).replace("<!-- GLOBAL SCRIPT -->", globalScript); | ||
| } | ||
| /** | ||
| * Returns the ANSI output to be printed on the terminal | ||
| */ | ||
| async toANSI(props) { | ||
| const ansiOutput = await this.#tmplToANSI("layout", { | ||
| title: props.title, | ||
| children: async () => { | ||
| const header = await this.#tmplToANSI("header", {}); | ||
| const info = await this.#tmplToANSI("errorInfo", props); | ||
| const stackTrace = await this.#tmplToANSI("errorStack", { | ||
| ide: process.env.EDITOR ?? "vscode", | ||
| sourceCodeRenderer: (error, frame) => { | ||
| return this.#tmplToANSI("errorStackSource", { | ||
| error, | ||
| frame | ||
| }); | ||
| }, | ||
| ...props | ||
| }); | ||
| const cause = await this.#tmplToANSI("errorCause", props); | ||
| const metadata = await this.#tmplToANSI("errorMetadata", props); | ||
| return `${header}${info}${stackTrace}${cause}${metadata}`; | ||
| } | ||
| }); | ||
| return ansiOutput; | ||
| } | ||
| #knownTemplates; | ||
| #styles = new Map([["global", createStyleSheet()]]); | ||
| #scripts = new Map([["global", createScript()]]); | ||
| constructor(devMode) { | ||
| this.devMode = devMode; | ||
| this.#knownTemplates = { | ||
| layout: new Layout(devMode), | ||
| header: new Header(devMode), | ||
| errorInfo: new ErrorInfo(devMode), | ||
| errorStack: new ErrorStack(devMode), | ||
| errorStackSource: new ErrorStackSource(devMode), | ||
| errorCause: new ErrorCause(devMode), | ||
| errorMetadata: new ErrorMetadata(devMode) | ||
| }; | ||
| } | ||
| #getStylesAndScripts(cspNonce) { | ||
| let customInjectedStyles = ""; | ||
| let globalScript = ""; | ||
| const styles = []; | ||
| const scripts = []; | ||
| const cspNonceAttr = cspNonce ? ` nonce="${cspNonce}"` : ""; | ||
| this.#styles.forEach((bucket, name) => { | ||
| if (name === "injected") customInjectedStyles = `<style id="${name}-styles"${cspNonceAttr}>${bucket}</style>`; | ||
| else styles.push(`<style id="${name}-styles"${cspNonceAttr}>${bucket}</style>`); | ||
| }); | ||
| this.#scripts.forEach((bucket, name) => { | ||
| if (name === "global") globalScript = `<script id="${name}-script"${cspNonceAttr}>${bucket}<\/script>`; | ||
| scripts.push(`<script id="${name}-script"${cspNonceAttr}>${bucket}<\/script>`); | ||
| }); | ||
| return { | ||
| styles: `${styles.join("\n")}\n${customInjectedStyles}`, | ||
| scripts: scripts.join("\n"), | ||
| globalScript | ||
| }; | ||
| } | ||
| async #collectStylesAndScripts(templateName) { | ||
| if (!this.#styles.has(templateName)) { | ||
| const styles = await this.#knownTemplates[templateName].getStyles(); | ||
| if (styles) this.#styles.set(templateName, styles); | ||
| } | ||
| if (!this.#scripts.has(templateName)) { | ||
| const script = await this.#knownTemplates[templateName].getScript(); | ||
| if (script) this.#scripts.set(templateName, script); | ||
| } | ||
| } | ||
| async #tmplToHTML(templateName, props) { | ||
| const component = this.#knownTemplates[templateName]; | ||
| if (!component) throw new Error(`Invalid template "${templateName}"`); | ||
| await this.#collectStylesAndScripts(templateName); | ||
| return component.toHTML(props); | ||
| } | ||
| async #tmplToANSI(templateName, props) { | ||
| const component = this.#knownTemplates[templateName]; | ||
| if (!component) throw new Error(`Invalid template "${templateName}"`); | ||
| return component.toANSI(props); | ||
| } | ||
| use(templateName, component) { | ||
| this.#knownTemplates[templateName] = component; | ||
| return this; | ||
| } | ||
| injectStyles(cssFragment) { | ||
| let injectedStyles = this.#styles.get("injected") ?? ""; | ||
| injectedStyles += `\n${cssFragment}`; | ||
| this.#styles.set("injected", injectedStyles); | ||
| return this; | ||
| } | ||
| async toHTML(props) { | ||
| const html = await this.#tmplToHTML("layout", { | ||
| title: props.title, | ||
| ide: props.ide, | ||
| cspNonce: props.cspNonce, | ||
| children: async () => { | ||
| return `${await this.#tmplToHTML("header", props)}${await this.#tmplToHTML("errorInfo", props)}${await this.#tmplToHTML("errorStack", { | ||
| ide: process.env.EDITOR ?? "vscode", | ||
| sourceCodeRenderer: (error, frame) => { | ||
| return this.#tmplToHTML("errorStackSource", { | ||
| error, | ||
| frame, | ||
| ide: props.ide, | ||
| cspNonce: props.cspNonce | ||
| }); | ||
| }, | ||
| ...props | ||
| })}${await this.#tmplToHTML("errorCause", props)}${await this.#tmplToHTML("errorMetadata", props)}`; | ||
| } | ||
| }); | ||
| const { globalScript, scripts, styles } = this.#getStylesAndScripts(props.cspNonce); | ||
| return html.replace("<!-- STYLES -->", styles).replace("<!-- SCRIPTS -->", scripts).replace("<!-- GLOBAL SCRIPT -->", globalScript); | ||
| } | ||
| async toANSI(props) { | ||
| return await this.#tmplToANSI("layout", { | ||
| title: props.title, | ||
| children: async () => { | ||
| return `${await this.#tmplToANSI("header", {})}${await this.#tmplToANSI("errorInfo", props)}${await this.#tmplToANSI("errorStack", { | ||
| ide: process.env.EDITOR ?? "vscode", | ||
| sourceCodeRenderer: (error, frame) => { | ||
| return this.#tmplToANSI("errorStackSource", { | ||
| error, | ||
| frame | ||
| }); | ||
| }, | ||
| ...props | ||
| })}${await this.#tmplToANSI("errorCause", props)}${await this.#tmplToANSI("errorMetadata", props)}`; | ||
| } | ||
| }); | ||
| } | ||
| }; | ||
| // src/youch.ts | ||
| var Youch = class { | ||
| /** | ||
| * Properties to be shared with the Error parser | ||
| */ | ||
| #sourceLoader; | ||
| #parsers = []; | ||
| #transformers = []; | ||
| /** | ||
| * Manage templates used for converting error to the HTML | ||
| * output | ||
| */ | ||
| templates = new Templates(false); | ||
| /** | ||
| * Define metadata to be displayed alongside the error output | ||
| */ | ||
| metadata = new Metadata(); | ||
| /** | ||
| * Creates an instance of the ErrorParser and applies the | ||
| * source loader, parsers and transformers on it | ||
| */ | ||
| #createErrorParser(options) { | ||
| const errorParser = new ErrorParser(options); | ||
| if (this.#sourceLoader) { | ||
| errorParser.defineSourceLoader(this.#sourceLoader); | ||
| } | ||
| this.#parsers.forEach((parser) => errorParser.useParser(parser)); | ||
| this.#transformers.forEach((transformer) => errorParser.useTransformer(transformer)); | ||
| return errorParser; | ||
| } | ||
| /** | ||
| * Defines the request properties as a metadata group | ||
| */ | ||
| #defineRequestMetadataGroup(request) { | ||
| if (!request || Object.keys(request).length === 0) { | ||
| return; | ||
| } | ||
| this.metadata.group("Request", { | ||
| ...request.url ? { | ||
| url: { | ||
| key: "URL", | ||
| value: request.url | ||
| } | ||
| } : {}, | ||
| ...request.method ? { | ||
| method: { | ||
| key: "Method", | ||
| value: request.method | ||
| } | ||
| } : {}, | ||
| ...request.headers ? { | ||
| headers: Object.keys(request.headers).map((key) => { | ||
| const value = request.headers[key]; | ||
| return { | ||
| key, | ||
| value: key === "cookie" ? { ...parse(value) } : value | ||
| }; | ||
| }) | ||
| } : {} | ||
| }); | ||
| } | ||
| /** | ||
| * Define custom implementation for loading the source code | ||
| * of a stack frame. | ||
| */ | ||
| defineSourceLoader(loader) { | ||
| this.#sourceLoader = loader; | ||
| return this; | ||
| } | ||
| /** | ||
| * Define a custom parser. Parsers are executed before the | ||
| * error gets parsed and provides you with an option to | ||
| * modify the error | ||
| */ | ||
| useParser(parser) { | ||
| this.#parsers.push(parser); | ||
| return this; | ||
| } | ||
| /** | ||
| * Define a custom transformer. Transformers are executed | ||
| * after the error has been parsed and can mutate the | ||
| * properties of the parsed error. | ||
| */ | ||
| useTransformer(transformer) { | ||
| this.#transformers.push(transformer); | ||
| return this; | ||
| } | ||
| /** | ||
| * Parses error to JSON | ||
| */ | ||
| async toJSON(error, options) { | ||
| options = { ...options }; | ||
| return this.#createErrorParser({ offset: options.offset }).parse(error); | ||
| } | ||
| /** | ||
| * Render error to HTML | ||
| */ | ||
| async toHTML(error, options) { | ||
| options = { ...options }; | ||
| this.#defineRequestMetadataGroup(options.request); | ||
| const parsedError = await this.#createErrorParser({ offset: options.offset }).parse(error); | ||
| return this.templates.toHTML({ | ||
| title: options.title ?? "An error has occurred", | ||
| ide: options.ide ?? process.env.IDE ?? "vscode", | ||
| cspNonce: options.cspNonce, | ||
| error: parsedError, | ||
| metadata: this.metadata | ||
| }); | ||
| } | ||
| /** | ||
| * Render error to ANSI output | ||
| */ | ||
| async toANSI(error, options) { | ||
| options = { ...options }; | ||
| const parsedError = await this.#createErrorParser({ offset: options.offset }).parse(error); | ||
| return this.templates.toANSI({ | ||
| title: "", | ||
| error: parsedError, | ||
| metadata: this.metadata | ||
| }); | ||
| } | ||
| #sourceLoader; | ||
| #parsers = []; | ||
| #transformers = []; | ||
| templates = new Templates(false); | ||
| metadata = new Metadata(); | ||
| #createErrorParser(options) { | ||
| const errorParser = new ErrorParser(options); | ||
| if (this.#sourceLoader) errorParser.defineSourceLoader(this.#sourceLoader); | ||
| this.#parsers.forEach((parser) => errorParser.useParser(parser)); | ||
| this.#transformers.forEach((transformer) => errorParser.useTransformer(transformer)); | ||
| return errorParser; | ||
| } | ||
| #defineRequestMetadataGroup(request) { | ||
| if (!request || Object.keys(request).length === 0) return; | ||
| this.metadata.group("Request", { | ||
| ...request.url ? { url: { | ||
| key: "URL", | ||
| value: request.url | ||
| } } : {}, | ||
| ...request.method ? { method: { | ||
| key: "Method", | ||
| value: request.method | ||
| } } : {}, | ||
| ...request.headers ? { headers: Object.keys(request.headers).map((key) => { | ||
| const value = request.headers[key]; | ||
| return { | ||
| key, | ||
| value: key === "cookie" ? { ...parse(value) } : value | ||
| }; | ||
| }) } : {} | ||
| }); | ||
| } | ||
| defineSourceLoader(loader) { | ||
| this.#sourceLoader = loader; | ||
| return this; | ||
| } | ||
| useParser(parser) { | ||
| this.#parsers.push(parser); | ||
| return this; | ||
| } | ||
| useTransformer(transformer) { | ||
| this.#transformers.push(transformer); | ||
| return this; | ||
| } | ||
| async toJSON(error, options) { | ||
| options = { ...options }; | ||
| return this.#createErrorParser({ offset: options.offset }).parse(error); | ||
| } | ||
| async toHTML(error, options) { | ||
| options = { ...options }; | ||
| this.#defineRequestMetadataGroup(options.request); | ||
| const parsedError = await this.#createErrorParser({ offset: options.offset }).parse(error); | ||
| return this.templates.toHTML({ | ||
| title: options.title ?? "An error has occurred", | ||
| ide: options.ide ?? process.env.IDE ?? "vscode", | ||
| cspNonce: options.cspNonce, | ||
| error: parsedError, | ||
| metadata: this.metadata | ||
| }); | ||
| } | ||
| async toANSI(error, options) { | ||
| options = { ...options }; | ||
| const parsedError = await this.#createErrorParser({ offset: options.offset }).parse(error); | ||
| return this.templates.toANSI({ | ||
| title: "", | ||
| error: parsedError, | ||
| metadata: this.metadata | ||
| }); | ||
| } | ||
| }; | ||
| export { | ||
| BaseComponent, | ||
| Metadata, | ||
| Youch | ||
| }; | ||
| export { BaseComponent, Metadata, Youch }; |
| function copyErrorMessage(button) { | ||
| const errorText = button.dataset.errorText; | ||
| navigator.clipboard.writeText(errorText) | ||
@@ -5,0 +5,0 @@ .then(() => { |
| { | ||
| "name": "youch", | ||
| "description": "Pretty print JavaScript errors on the Web and the Terminal", | ||
| "version": "4.1.0-beta.13", | ||
| "version": "4.1.0", | ||
| "type": "module", | ||
@@ -26,3 +26,3 @@ "files": [ | ||
| "copy:assets": "copyfiles --up=1 src/public/**/* build", | ||
| "compile": "tsup-node && tsc --emitDeclarationOnly --declaration", | ||
| "compile": "tsdown && tsc --emitDeclarationOnly --declaration", | ||
| "build": "npm run compile && npm run copy:assets", | ||
@@ -35,35 +35,35 @@ "version": "npm run build", | ||
| "devDependencies": { | ||
| "@adonisjs/eslint-config": "^3.0.0-next.0", | ||
| "@adonisjs/eslint-config": "^3.0.0", | ||
| "@adonisjs/prettier-config": "^1.4.5", | ||
| "@adonisjs/tsconfig": "^2.0.0-next.0", | ||
| "@aws-sdk/client-s3": "^3.922.0", | ||
| "@aws-sdk/s3-request-presigner": "^3.922.0", | ||
| "@japa/assert": "^4.1.1", | ||
| "@japa/expect": "^3.0.6", | ||
| "@japa/expect-type": "^2.0.3", | ||
| "@japa/file-system": "^2.3.2", | ||
| "@japa/runner": "^4.4.0", | ||
| "@japa/snapshot": "^2.0.9", | ||
| "@poppinss/exception": "^1.2.2", | ||
| "@poppinss/ts-exec": "^1.4.1", | ||
| "@release-it/conventional-changelog": "^10.0.1", | ||
| "@types/jsdom": "^27.0.0", | ||
| "@types/node": "^24.10.0", | ||
| "@types/pg": "^8.15.6", | ||
| "axios": "^1.13.1", | ||
| "@adonisjs/tsconfig": "^2.0.0", | ||
| "@aws-sdk/client-s3": "^3.996.0", | ||
| "@aws-sdk/s3-request-presigner": "^3.996.0", | ||
| "@japa/assert": "^4.2.0", | ||
| "@japa/expect": "^3.0.7", | ||
| "@japa/expect-type": "^2.0.4", | ||
| "@japa/file-system": "^3.0.0", | ||
| "@japa/runner": "^5.3.0", | ||
| "@japa/snapshot": "^2.0.10", | ||
| "@poppinss/exception": "^1.2.3", | ||
| "@poppinss/ts-exec": "^1.4.4", | ||
| "@release-it/conventional-changelog": "^10.0.5", | ||
| "@types/jsdom": "^28.0.0", | ||
| "@types/node": "^25.3.0", | ||
| "@types/pg": "^8.16.0", | ||
| "axios": "^1.13.5", | ||
| "c8": "^10.1.3", | ||
| "copyfiles": "^2.4.1", | ||
| "eslint": "^9.39.0", | ||
| "flydrive": "^1.3.0", | ||
| "jsdom": "^27.1.0", | ||
| "pg": "^8.16.3", | ||
| "prettier": "^3.6.2", | ||
| "release-it": "^19.0.5", | ||
| "tsup": "^8.5.0", | ||
| "eslint": "^10.0.2", | ||
| "flydrive": "^2.0.0", | ||
| "jsdom": "^28.1.0", | ||
| "pg": "^8.18.0", | ||
| "prettier": "^3.8.1", | ||
| "release-it": "^19.2.4", | ||
| "tsdown": "^0.20.3", | ||
| "typescript": "^5.9.3" | ||
| }, | ||
| "dependencies": { | ||
| "@poppinss/colors": "^4.1.5", | ||
| "@poppinss/dumper": "^0.6.5", | ||
| "@speed-highlight/core": "^1.2.9", | ||
| "@poppinss/colors": "^4.1.6", | ||
| "@poppinss/dumper": "^0.7.0", | ||
| "@speed-highlight/core": "^1.2.14", | ||
| "cookie-es": "^2.0.0", | ||
@@ -87,3 +87,3 @@ "youch-core": "^0.3.3" | ||
| }, | ||
| "tsup": { | ||
| "tsdown": { | ||
| "entry": [ | ||
@@ -97,4 +97,7 @@ "index.ts", | ||
| "format": "esm", | ||
| "minify": "dce-only", | ||
| "fixedExtension": false, | ||
| "dts": false, | ||
| "sourcemap": false, | ||
| "treeshake": false, | ||
| "sourcemaps": false, | ||
| "target": "esnext" | ||
@@ -101,0 +104,0 @@ }, |
| import "#nitro/virtual/polyfills"; | ||
| import { Server } from "node:http"; | ||
| import { parentPort, threadId } from "node:worker_threads"; | ||
| import wsAdapter from "crossws/adapters/node"; | ||
| import { toNodeHandler } from "srvx/node"; | ||
| import { getSocketAddress, isSocketSupported } from "get-port-please"; | ||
| import { useNitroApp, useNitroHooks } from "nitro/app"; | ||
@@ -11,58 +6,16 @@ import { startScheduleRunner } from "#nitro/runtime/task"; | ||
| import { resolveWebsocketHooks } from "#nitro/runtime/app"; | ||
| // Listen for shutdown signal from runner | ||
| parentPort?.on("message", (msg) => { | ||
| if (msg && msg.event === "shutdown") { | ||
| shutdown(); | ||
| } | ||
| }); | ||
| const nitroApp = useNitroApp(); | ||
| const nitroHooks = useNitroHooks(); | ||
| trapUnhandledErrors(); | ||
| const server = new Server(toNodeHandler(nitroApp.fetch)); | ||
| let listener; | ||
| listen().catch((error) => { | ||
| console.error("Dev worker failed to listen:", error); | ||
| return shutdown(); | ||
| }); | ||
| // https://crossws.unjs.io/adapters/node | ||
| if (import.meta._websocket) { | ||
| const { handleUpgrade } = wsAdapter({ resolve: resolveWebsocketHooks }); | ||
| server.on("upgrade", handleUpgrade); | ||
| } | ||
| // Scheduled tasks | ||
| if (import.meta._tasks) { | ||
| startScheduleRunner(); | ||
| startScheduleRunner({}); | ||
| } | ||
| // --- utils --- | ||
| async function listen() { | ||
| const listenAddr = await isSocketSupported() ? getSocketAddress({ | ||
| name: `nitro-dev-${threadId}`, | ||
| pid: true, | ||
| random: true | ||
| }) : { | ||
| port: 0, | ||
| host: "localhost" | ||
| }; | ||
| return new Promise((resolve, reject) => { | ||
| try { | ||
| listener = server.listen(listenAddr, () => { | ||
| const address = server.address(); | ||
| parentPort?.postMessage({ | ||
| event: "listen", | ||
| address: typeof address === "string" ? { socketPath: address } : { | ||
| host: "localhost", | ||
| port: address?.port | ||
| } | ||
| }); | ||
| resolve(); | ||
| }); | ||
| } catch (error) { | ||
| reject(error); | ||
| } | ||
| }); | ||
| } | ||
| async function shutdown() { | ||
| server.closeAllConnections?.(); | ||
| await Promise.all([new Promise((resolve) => listener?.close(resolve)), nitroHooks.callHook("close")]).catch(console.error); | ||
| parentPort?.postMessage({ event: "exit" }); | ||
| } | ||
| const ws = import.meta._websocket ? await import("crossws/adapters/node").then((m) => (m.default || m)({ resolve: resolveWebsocketHooks })) : undefined; | ||
| export default { | ||
| fetch: nitroApp.fetch, | ||
| upgrade: ws ? (context) => { | ||
| ws.handleUpgrade(context.node.req, context.node.socket, context.node.head); | ||
| } : undefined, | ||
| ipc: { onClose: () => nitroHooks.callHook("close") } | ||
| }; |
| import "#nitro/virtual/polyfills"; | ||
| import consola from "consola"; | ||
| import { HTTPError } from "h3"; | ||
| import { useNitroApp, useNitroHooks } from "nitro/app"; | ||
@@ -4,0 +5,0 @@ const nitroApp = useNitroApp(); |
@@ -14,3 +14,2 @@ import "#nitro/virtual/polyfills"; | ||
| req.runtime ??= { name: "service-worker" }; | ||
| // @ts-expect-error (add to srvx types) | ||
| req.runtime.serviceWorker ??= { event }; | ||
@@ -17,0 +16,0 @@ req.waitUntil = event.waitUntil.bind(event); |
@@ -8,3 +8,2 @@ import "#nitro/virtual/polyfills"; | ||
| const response = await nitroApp.fetch(request); | ||
| response.headers.set("transfer-encoding", "chunked"); | ||
| const httpResponseMetadata = { | ||
@@ -14,2 +13,5 @@ statusCode: response.status, | ||
| }; | ||
| if (!httpResponseMetadata.headers["transfer-encoding"]) { | ||
| httpResponseMetadata.headers["transfer-encoding"] = "chunked"; | ||
| } | ||
| const body = response.body ?? new ReadableStream({ start(controller) { | ||
@@ -16,0 +18,0 @@ controller.enqueue(""); |
@@ -8,3 +8,4 @@ import "#nitro/virtual/polyfills"; | ||
| import { resolveWebsocketHooks } from "#nitro/runtime/app"; | ||
| const port = Number.parseInt(process.env.NITRO_PORT || process.env.PORT || "") || 3e3; | ||
| const _parsedPort = Number.parseInt(process.env.NITRO_PORT ?? process.env.PORT ?? ""); | ||
| const port = Number.isNaN(_parsedPort) ? 3e3 : _parsedPort; | ||
| const host = process.env.NITRO_HOST || process.env.HOST; | ||
@@ -25,3 +26,3 @@ const cert = process.env.NITRO_SSL_CERT; | ||
| } | ||
| serve({ | ||
| const server = serve({ | ||
| port, | ||
@@ -39,4 +40,4 @@ hostname: host, | ||
| if (import.meta._tasks) { | ||
| startScheduleRunner(); | ||
| startScheduleRunner({ waitUntil: server.waitUntil }); | ||
| } | ||
| export default {}; |
@@ -8,3 +8,4 @@ import "#nitro/virtual/polyfills"; | ||
| import { resolveWebsocketHooks } from "#nitro/runtime/app"; | ||
| const port = Number.parseInt(process.env.NITRO_PORT || process.env.PORT || "") || 3e3; | ||
| const _parsedPort = Number.parseInt(process.env.NITRO_PORT ?? process.env.PORT ?? ""); | ||
| const port = Number.isNaN(_parsedPort) ? 3e3 : _parsedPort; | ||
| const host = process.env.NITRO_HOST || process.env.HOST; | ||
@@ -25,3 +26,3 @@ const cert = process.env.NITRO_SSL_CERT; | ||
| } | ||
| serve({ | ||
| const server = serve({ | ||
| port, | ||
@@ -38,4 +39,4 @@ hostname: host, | ||
| if (import.meta._tasks) { | ||
| startScheduleRunner(); | ||
| startScheduleRunner({ waitUntil: server.waitUntil }); | ||
| } | ||
| export default {}; |
@@ -11,3 +11,2 @@ import "#nitro/virtual/polyfills"; | ||
| req.runtime ??= { name: "netlify-edge" }; | ||
| // @ts-expect-error (add to srvx types) | ||
| req.runtime.netlify ??= { context }; | ||
@@ -14,0 +13,0 @@ const url = new URL(req.url); |
@@ -7,3 +7,3 @@ import "#nitro/virtual/polyfills"; | ||
| req.runtime ??= { name: "netlify" }; | ||
| req.ip = req.headers.get("x-nf-client-connection-ip") || undefined; | ||
| req.ip ??= req.headers.get("x-nf-client-connection-ip") || undefined; | ||
| const response = await nitroApp.fetch(req); | ||
@@ -10,0 +10,0 @@ const isr = (req.context?.routeRules || {})?.isr?.options; |
@@ -9,3 +9,4 @@ import "#nitro/virtual/polyfills"; | ||
| import { resolveWebsocketHooks } from "#nitro/runtime/app"; | ||
| const port = Number.parseInt(process.env.NITRO_PORT || process.env.PORT || "") || 3e3; | ||
| const _parsedPort = Number.parseInt(process.env.NITRO_PORT ?? process.env.PORT ?? ""); | ||
| const port = Number.isNaN(_parsedPort) ? 3e3 : _parsedPort; | ||
| const host = process.env.NITRO_HOST || process.env.HOST; | ||
@@ -52,4 +53,4 @@ const cert = process.env.NITRO_SSL_CERT; | ||
| if (import.meta._tasks) { | ||
| startScheduleRunner(); | ||
| startScheduleRunner({ waitUntil: server.waitUntil }); | ||
| } | ||
| export default {}; |
@@ -8,3 +8,4 @@ import "#nitro/virtual/polyfills"; | ||
| import { resolveWebsocketHooks } from "#nitro/runtime/app"; | ||
| const port = Number.parseInt(process.env.NITRO_PORT || process.env.PORT || "") || 3e3; | ||
| const _parsedPort = Number.parseInt(process.env.NITRO_PORT ?? process.env.PORT ?? ""); | ||
| const port = Number.isNaN(_parsedPort) ? 3e3 : _parsedPort; | ||
| const host = process.env.NITRO_HOST || process.env.HOST; | ||
@@ -45,4 +46,4 @@ const cert = process.env.NITRO_SSL_CERT; | ||
| if (import.meta._tasks) { | ||
| startScheduleRunner(); | ||
| startScheduleRunner({ waitUntil: server.waitUntil }); | ||
| } | ||
| export default {}; |
@@ -13,3 +13,2 @@ import "#nitro/virtual/polyfills"; | ||
| req.runtime ??= { name: "stormkit" }; | ||
| // @ts-expect-error (add to srvx types) | ||
| req.runtime.stormkit ??= { | ||
@@ -16,0 +15,0 @@ event, |
@@ -14,6 +14,4 @@ import "#nitro/virtual/polyfills"; | ||
| } | ||
| req.runtime = { | ||
| name: "vercel", | ||
| vercel: { context } | ||
| }; | ||
| req.runtime ??= { name: "vercel" }; | ||
| req.runtime.vercel = { context }; | ||
| let ip; | ||
@@ -20,0 +18,0 @@ Object.defineProperty(req, "ip", { get() { |
@@ -7,6 +7,2 @@ import type { EventHandler } from "h3"; | ||
| >(fn: (...args: ArgsT) => T | Promise<T>, opts?: CacheOptions<T, ArgsT>): (...args: ArgsT) => Promise<T>; | ||
| export declare function cachedFunction< | ||
| T, | ||
| ArgsT extends unknown[] = any[] | ||
| >(fn: (...args: ArgsT) => T | Promise<T>, opts?: CacheOptions<T>): (...args: ArgsT) => Promise<T | undefined>; | ||
| export declare function defineCachedHandler(handler: EventHandler, opts?: CachedEventHandlerOptions): EventHandler; |
@@ -1,265 +0,41 @@ | ||
| import { defineHandler, handleCacheHeaders, isHTTPEvent, toResponse } from "h3"; | ||
| import { defineHandler, handleCacheHeaders, toResponse } from "h3"; | ||
| import { FastResponse } from "srvx"; | ||
| import { parseURL } from "ufo"; | ||
| import { hash } from "ohash"; | ||
| import { defineCachedFunction as _defineCachedFunction, defineCachedHandler as _defineCachedHandler, setStorage } from "ocache"; | ||
| import { useNitroApp } from "./app.mjs"; | ||
| import { useStorage } from "./storage.mjs"; | ||
| function defaultCacheOptions() { | ||
| return { | ||
| name: "_", | ||
| base: "/cache", | ||
| swr: true, | ||
| maxAge: 1 | ||
| }; | ||
| let _storageReady = false; | ||
| function ensureStorage() { | ||
| if (_storageReady) { | ||
| return; | ||
| } | ||
| _storageReady = true; | ||
| const storage = useStorage(); | ||
| setStorage({ | ||
| get: (key) => storage.getItem(key), | ||
| set: (key, value, opts) => storage.setItem(key, value, opts?.ttl ? { ttl: opts.ttl } : undefined) | ||
| }); | ||
| } | ||
| function defaultOnError(error) { | ||
| console.error("[cache]", error); | ||
| useNitroApp().captureError?.(error, { tags: ["cache"] }); | ||
| } | ||
| export function defineCachedFunction(fn, opts = {}) { | ||
| opts = { | ||
| ...defaultCacheOptions(), | ||
| ensureStorage(); | ||
| return _defineCachedFunction(fn, { | ||
| group: "nitro/functions", | ||
| onError: defaultOnError, | ||
| ...opts | ||
| }; | ||
| const pending = {}; | ||
| // Normalize cache params | ||
| const group = opts.group || "nitro/functions"; | ||
| const name = opts.name || fn.name || "_"; | ||
| const integrity = opts.integrity || hash([fn, opts]); | ||
| const validate = opts.validate || ((entry) => entry.value !== undefined); | ||
| async function get(key, resolver, shouldInvalidateCache, event) { | ||
| // Use extension for key to avoid conflicting with parent namespace (foo/bar and foo/bar/baz) | ||
| const cacheKey = [ | ||
| opts.base, | ||
| group, | ||
| name, | ||
| key + ".json" | ||
| ].filter(Boolean).join(":").replace(/:\/$/, ":index"); | ||
| let entry = await useStorage().getItem(cacheKey).catch((error) => { | ||
| console.error(`[cache] Cache read error.`, error); | ||
| useNitroApp().captureError?.(error, { | ||
| event, | ||
| tags: ["cache"] | ||
| }); | ||
| }) || {}; | ||
| // https://github.com/nitrojs/nitro/issues/2160 | ||
| if (typeof entry !== "object") { | ||
| entry = {}; | ||
| const error = new Error("Malformed data read from cache."); | ||
| console.error("[cache]", error); | ||
| useNitroApp().captureError?.(error, { | ||
| event, | ||
| tags: ["cache"] | ||
| }); | ||
| } | ||
| const ttl = (opts.maxAge ?? 0) * 1e3; | ||
| if (ttl) { | ||
| entry.expires = Date.now() + ttl; | ||
| } | ||
| const expired = shouldInvalidateCache || entry.integrity !== integrity || ttl && Date.now() - (entry.mtime || 0) > ttl || validate(entry) === false; | ||
| const _resolve = async () => { | ||
| const isPending = pending[key]; | ||
| if (!isPending) { | ||
| if (entry.value !== undefined && (opts.staleMaxAge || 0) >= 0 && opts.swr === false) { | ||
| // Remove cached entry to prevent using expired cache on concurrent requests | ||
| entry.value = undefined; | ||
| entry.integrity = undefined; | ||
| entry.mtime = undefined; | ||
| entry.expires = undefined; | ||
| } | ||
| pending[key] = Promise.resolve(resolver()); | ||
| } | ||
| try { | ||
| entry.value = await pending[key]; | ||
| } catch (error) { | ||
| // Make sure entries that reject get removed. | ||
| if (!isPending) { | ||
| delete pending[key]; | ||
| } | ||
| // Re-throw error to make sure the caller knows the task failed. | ||
| throw error; | ||
| } | ||
| if (!isPending) { | ||
| // Update mtime, integrity + validate and set the value in cache only the first time the request is made. | ||
| entry.mtime = Date.now(); | ||
| entry.integrity = integrity; | ||
| delete pending[key]; | ||
| if (validate(entry) !== false) { | ||
| let setOpts; | ||
| if (opts.maxAge && !opts.swr) { | ||
| setOpts = { ttl: opts.maxAge }; | ||
| } | ||
| const promise = useStorage().setItem(cacheKey, entry, setOpts).catch((error) => { | ||
| console.error(`[cache] Cache write error.`, error); | ||
| useNitroApp().captureError?.(error, { | ||
| event, | ||
| tags: ["cache"] | ||
| }); | ||
| }); | ||
| if (typeof event?.req?.waitUntil === "function") { | ||
| event.req.waitUntil(promise); | ||
| } | ||
| } | ||
| } | ||
| }; | ||
| const _resolvePromise = expired ? _resolve() : Promise.resolve(); | ||
| if (entry.value === undefined) { | ||
| await _resolvePromise; | ||
| } else if (expired && event && event.req.waitUntil) { | ||
| event.req.waitUntil(_resolvePromise); | ||
| } | ||
| if (opts.swr && validate(entry) !== false) { | ||
| _resolvePromise.catch((error) => { | ||
| console.error(`[cache] SWR handler error.`, error); | ||
| useNitroApp().captureError?.(error, { | ||
| event, | ||
| tags: ["cache"] | ||
| }); | ||
| }); | ||
| return entry; | ||
| } | ||
| return _resolvePromise.then(() => entry); | ||
| } | ||
| return async (...args) => { | ||
| const shouldBypassCache = await opts.shouldBypassCache?.(...args); | ||
| if (shouldBypassCache) { | ||
| return fn(...args); | ||
| } | ||
| const key = await (opts.getKey || getKey)(...args); | ||
| const shouldInvalidateCache = await opts.shouldInvalidateCache?.(...args); | ||
| const entry = await get(key, () => fn(...args), shouldInvalidateCache, args[0] && isHTTPEvent(args[0]) ? args[0] : undefined); | ||
| let value = entry.value; | ||
| if (opts.transform) { | ||
| value = await opts.transform(entry, ...args) || value; | ||
| } | ||
| return value; | ||
| }; | ||
| }); | ||
| } | ||
| export function cachedFunction(fn, opts = {}) { | ||
| return defineCachedFunction(fn, opts); | ||
| } | ||
| function getKey(...args) { | ||
| return args.length > 0 ? hash(args) : ""; | ||
| } | ||
| function escapeKey(key) { | ||
| return String(key).replace(/\W/g, ""); | ||
| } | ||
| export function defineCachedHandler(handler, opts = defaultCacheOptions()) { | ||
| const variableHeaderNames = (opts.varies || []).filter(Boolean).map((h) => h.toLowerCase()).sort(); | ||
| const _opts = { | ||
| ...opts, | ||
| shouldBypassCache: (event) => { | ||
| return event.req.method !== "GET" && event.req.method !== "HEAD"; | ||
| }, | ||
| getKey: async (event) => { | ||
| // Custom user-defined key | ||
| const customKey = await opts.getKey?.(event); | ||
| if (customKey) { | ||
| return escapeKey(customKey); | ||
| } | ||
| // Auto-generated key | ||
| const _path = event.url.pathname + event.url.search; | ||
| let _pathname; | ||
| try { | ||
| _pathname = escapeKey(decodeURI(parseURL(_path).pathname)).slice(0, 16) || "index"; | ||
| } catch { | ||
| _pathname = "-"; | ||
| } | ||
| const _hashedPath = `${_pathname}.${hash(_path)}`; | ||
| const _headers = variableHeaderNames.map((header) => [header, event.req.headers.get(header)]).map(([name, value]) => `${escapeKey(name)}.${hash(value)}`); | ||
| return [_hashedPath, ..._headers].join(":"); | ||
| }, | ||
| validate: (entry) => { | ||
| if (!entry.value) { | ||
| return false; | ||
| } | ||
| if (entry.value.status >= 400) { | ||
| return false; | ||
| } | ||
| if (entry.value.body === undefined) { | ||
| return false; | ||
| } | ||
| // https://github.com/nitrojs/nitro/pull/1857 | ||
| if (entry.value.headers.etag === "undefined" || entry.value.headers["last-modified"] === "undefined") { | ||
| return false; | ||
| } | ||
| return true; | ||
| }, | ||
| group: opts.group || "nitro/handlers", | ||
| integrity: opts.integrity || hash([handler, opts]) | ||
| }; | ||
| const _cachedHandler = cachedFunction(async (event) => { | ||
| // Filter non variable headers | ||
| const filteredHeaders = [...event.req.headers.entries()].filter(([key]) => !variableHeaderNames.includes(key.toLowerCase())); | ||
| try { | ||
| const originalReq = event.req; | ||
| // @ts-expect-error assigning to publicly readonly property | ||
| event.req = new Request(event.req.url, { | ||
| method: event.req.method, | ||
| headers: filteredHeaders | ||
| }); | ||
| // Inherit srvx context | ||
| event.req.runtime = originalReq.runtime; | ||
| event.req.waitUntil = originalReq.waitUntil; | ||
| } catch (error) { | ||
| console.error("[cache] Failed to filter headers:", error); | ||
| } | ||
| // Call handler | ||
| const rawValue = await handler(event); | ||
| const res = await toResponse(rawValue, event); | ||
| // Stringified body | ||
| // TODO: support binary responses | ||
| const body = await res.text(); | ||
| if (!res.headers.has("etag")) { | ||
| res.headers.set("etag", `W/"${hash(body)}"`); | ||
| } | ||
| if (!res.headers.has("last-modified")) { | ||
| res.headers.set("last-modified", new Date().toUTCString()); | ||
| } | ||
| const cacheControl = []; | ||
| if (opts.swr) { | ||
| if (opts.maxAge) { | ||
| cacheControl.push(`s-maxage=${opts.maxAge}`); | ||
| } | ||
| if (opts.staleMaxAge) { | ||
| cacheControl.push(`stale-while-revalidate=${opts.staleMaxAge}`); | ||
| } else { | ||
| cacheControl.push("stale-while-revalidate"); | ||
| } | ||
| } else if (opts.maxAge) { | ||
| cacheControl.push(`max-age=${opts.maxAge}`); | ||
| } | ||
| if (cacheControl.length > 0) { | ||
| res.headers.set("cache-control", cacheControl.join(", ")); | ||
| } | ||
| const cacheEntry = { | ||
| status: res.status, | ||
| statusText: res.statusText, | ||
| headers: Object.fromEntries(res.headers.entries()), | ||
| body | ||
| }; | ||
| return cacheEntry; | ||
| }, _opts); | ||
| return defineHandler(async (event) => { | ||
| // Headers-only mode | ||
| if (opts.headersOnly) { | ||
| // TODO: Send SWR too | ||
| if (handleCacheHeaders(event, { maxAge: opts.maxAge })) { | ||
| return; | ||
| } | ||
| return handler(event); | ||
| } | ||
| // Call with cache | ||
| const response = await _cachedHandler(event); | ||
| // Check for cache headers | ||
| if (handleCacheHeaders(event, { | ||
| modifiedTime: new Date(response.headers["last-modified"]), | ||
| etag: response.headers.etag, | ||
| maxAge: opts.maxAge | ||
| })) { | ||
| return; | ||
| } | ||
| // Send Response | ||
| return new FastResponse(response.body, { | ||
| status: response.status, | ||
| statusText: response.statusText, | ||
| headers: response.headers | ||
| }); | ||
| export function defineCachedHandler(handler, opts = {}) { | ||
| ensureStorage(); | ||
| const ocacheHandler = _defineCachedHandler(handler, { | ||
| group: "nitro/handlers", | ||
| onError: defaultOnError, | ||
| toResponse: (value, event) => toResponse(value, event), | ||
| createResponse: (body, init) => new FastResponse(body, init), | ||
| handleCacheHeaders: (event, conditions) => handleCacheHeaders(event, conditions), | ||
| ...opts | ||
| }); | ||
| return defineHandler((event) => ocacheHandler(event)); | ||
| } |
@@ -0,3 +1,5 @@ | ||
| import type { NitroAsyncContext } from "nitro/types"; | ||
| import type { ServerRequest } from "srvx"; | ||
| export declare const nitroAsyncContext: unknown; | ||
| import { type UseContext } from "unctx"; | ||
| export declare const nitroAsyncContext: UseContext<NitroAsyncContext>; | ||
| /** | ||
@@ -4,0 +6,0 @@ * |
@@ -1,5 +0,6 @@ | ||
| import type { HTTPError, HTTPEvent } from "h3"; | ||
| import { HTTPError, type HTTPEvent } from "h3"; | ||
| import type { InternalHandlerResponse } from "./utils.mjs"; | ||
| declare const _default; | ||
| export default _default; | ||
| import type { NitroErrorHandler } from "nitro/types"; | ||
| declare const errorHandler: NitroErrorHandler; | ||
| export default errorHandler; | ||
| export declare function defaultHandler(error: HTTPError, event: HTTPEvent, opts?: { | ||
@@ -9,2 +10,2 @@ silent?: boolean; | ||
| }): Promise<InternalHandlerResponse>; | ||
| export declare function loadStackTrace(error: any); | ||
| export declare function loadStackTrace(error: any): Promise<void>; |
@@ -0,1 +1,2 @@ | ||
| import { HTTPError } from "h3"; | ||
| import { getRequestURL } from "h3"; | ||
@@ -5,15 +6,12 @@ import { readFile } from "node:fs/promises"; | ||
| import consola from "consola"; | ||
| import { ErrorParser } from "youch-core"; | ||
| import { Youch } from "youch"; | ||
| import { SourceMapConsumer } from "source-map"; | ||
| import { defineNitroErrorHandler } from "./utils.mjs"; | ||
| import { FastResponse } from "srvx"; | ||
| export default defineNitroErrorHandler(async function defaultNitroErrorHandler(error, event) { | ||
| const errorHandler = defineNitroErrorHandler(async function defaultNitroErrorHandler(error, event) { | ||
| const res = await defaultHandler(error, event); | ||
| return new FastResponse(typeof res.body === "string" ? res.body : JSON.stringify(res.body, null, 2), res); | ||
| }); | ||
| export default errorHandler; | ||
| export async function defaultHandler(error, event, opts) { | ||
| const isSensitive = error.unhandled; | ||
| const status = error.status || 500; | ||
| // prettier-ignore | ||
| const unhandled = error.unhandled ?? !HTTPError.isError(error); | ||
| const { status = 500, statusText = "" } = unhandled ? {} : error; | ||
| const url = getRequestURL(event, { | ||
@@ -27,7 +25,6 @@ xForwardedHost: true, | ||
| if (/^\/[^/]/.test(baseURL) && !url.pathname.startsWith(baseURL)) { | ||
| const redirectTo = `${baseURL}${url.pathname.slice(1)}${url.search}`; | ||
| return { | ||
| status: 302, | ||
| statusText: "Found", | ||
| headers: { location: redirectTo }, | ||
| headers: new Headers({ location: `${baseURL}${url.pathname.slice(1)}${url.search}` }), | ||
| body: `Redirecting...` | ||
@@ -39,43 +36,42 @@ }; | ||
| await loadStackTrace(error).catch(consola.error); | ||
| const { Youch } = await import("youch"); | ||
| // https://github.com/poppinss/youch | ||
| const youch = new Youch(); | ||
| // Console output | ||
| if (isSensitive && !opts?.silent) { | ||
| // prettier-ignore | ||
| const tags = [error.unhandled && "[unhandled]"].filter(Boolean).join(" "); | ||
| const ansiError = await (await youch.toANSI(error)).replaceAll(process.cwd(), "."); | ||
| consola.error(`[request error] ${tags} [${event.req.method}] ${url}\n\n`, ansiError); | ||
| if (unhandled && !opts?.silent) { | ||
| const ansiError = (await youch.toANSI(error)).replaceAll(process.cwd(), "."); | ||
| consola.error(`[request error] [${event.req.method}] ${url}\n\n`, ansiError); | ||
| } | ||
| // Use HTML response only when user-agent expects it (browsers) | ||
| const useJSON = opts?.json ?? !event.req.headers.get("accept")?.includes("text/html"); | ||
| // Prepare headers | ||
| const headers = { | ||
| "content-type": useJSON ? "application/json" : "text/html", | ||
| "x-content-type-options": "nosniff", | ||
| "x-frame-options": "DENY", | ||
| "referrer-policy": "no-referrer", | ||
| "content-security-policy": "script-src 'self' 'unsafe-inline'; object-src 'none'; base-uri 'self';" | ||
| }; | ||
| if (status === 404 || !event.res.headers.has("cache-control")) { | ||
| headers["cache-control"] = "no-cache"; | ||
| const headers = new Headers(unhandled ? {} : error.headers); | ||
| if (useJSON) { | ||
| headers.set("Content-Type", "application/json; charset=utf-8"); | ||
| const jsonBody = typeof error.toJSON === "function" ? error.toJSON() : { | ||
| status, | ||
| statusText, | ||
| message: error.message | ||
| }; | ||
| return { | ||
| status, | ||
| statusText, | ||
| headers, | ||
| body: { | ||
| error: true, | ||
| stack: error.stack?.split("\n").map((line) => line.trim()), | ||
| ...jsonBody | ||
| } | ||
| }; | ||
| } | ||
| // Prepare body | ||
| const body = useJSON ? { | ||
| error: true, | ||
| url, | ||
| status, | ||
| statusText: error.statusText, | ||
| message: error.message, | ||
| data: error.data, | ||
| stack: error.stack?.split("\n").map((line) => line.trim()) | ||
| } : await youch.toHTML(error, { request: { | ||
| url: url.href, | ||
| method: event.req.method, | ||
| headers: Object.fromEntries(event.req.headers.entries()) | ||
| } }); | ||
| // HTML response | ||
| headers.set("Content-Type", "text/html; charset=utf-8"); | ||
| return { | ||
| status, | ||
| statusText: error.statusText, | ||
| statusText: unhandled ? "" : error.statusText, | ||
| headers, | ||
| body | ||
| body: await youch.toHTML(error, { request: { | ||
| url: url.href, | ||
| method: event.req.method, | ||
| headers: Object.fromEntries(event.req.headers.entries()) | ||
| } }) | ||
| }; | ||
@@ -88,2 +84,3 @@ } | ||
| } | ||
| const { ErrorParser } = await import("youch-core"); | ||
| const parsed = await new ErrorParser().defineSourceLoader(sourceLoader).parse(error); | ||
@@ -104,2 +101,3 @@ const stack = error.message + "\n" + parsed.frames.map((frame) => fmtFrame(frame)).join("\n"); | ||
| if (rawSourceMap) { | ||
| const { SourceMapConsumer } = await import("source-map"); | ||
| const consumer = await new SourceMapConsumer(rawSourceMap); | ||
@@ -106,0 +104,0 @@ // prettier-ignore |
@@ -1,1 +0,1 @@ | ||
| export declare function trapUnhandledErrors(); | ||
| export declare function trapUnhandledErrors(): void; |
@@ -1,2 +0,2 @@ | ||
| import type { HTTPError, HTTPEvent } from "h3"; | ||
| import { HTTPError, type HTTPEvent } from "h3"; | ||
| import type { InternalHandlerResponse } from "./utils.mjs"; | ||
@@ -6,5 +6,2 @@ import type { NitroErrorHandler } from "nitro/types"; | ||
| export default errorHandler; | ||
| export declare function defaultHandler(error: HTTPError, event: HTTPEvent, opts?: { | ||
| silent?: boolean; | ||
| json?: boolean; | ||
| }): InternalHandlerResponse; | ||
| export declare function defaultHandler(error: HTTPError, event: HTTPEvent): InternalHandlerResponse; |
@@ -0,1 +1,2 @@ | ||
| import { HTTPError } from "h3"; | ||
| import { FastResponse } from "srvx"; | ||
@@ -7,49 +8,34 @@ const errorHandler = (error, event) => { | ||
| export default errorHandler; | ||
| export function defaultHandler(error, event, opts) { | ||
| const isSensitive = error.unhandled; | ||
| const status = error.status || 500; | ||
| const url = event.url || new URL(event.req.url); | ||
| export function defaultHandler(error, event) { | ||
| const unhandled = error.unhandled ?? !HTTPError.isError(error); | ||
| const { status = 500, statusText = "" } = unhandled ? {} : error; | ||
| if (status === 404) { | ||
| const url = event.url || new URL(event.req.url); | ||
| const baseURL = import.meta.baseURL || "/"; | ||
| if (/^\/[^/]/.test(baseURL) && !url.pathname.startsWith(baseURL)) { | ||
| const redirectTo = `${baseURL}${url.pathname.slice(1)}${url.search}`; | ||
| return { | ||
| status: 302, | ||
| statusText: "Found", | ||
| headers: { location: redirectTo }, | ||
| body: `Redirecting...` | ||
| headers: new Headers({ location: `${baseURL}${url.pathname.slice(1)}${url.search}` }) | ||
| }; | ||
| } | ||
| } | ||
| // Console output | ||
| if (isSensitive && !opts?.silent) { | ||
| // prettier-ignore | ||
| const tags = [error.unhandled && "[unhandled]"].filter(Boolean).join(" "); | ||
| console.error(`[request error] ${tags} [${event.req.method}] ${url}\n`, error); | ||
| } | ||
| // Send response | ||
| const headers = { | ||
| "content-type": "application/json", | ||
| "x-content-type-options": "nosniff", | ||
| "x-frame-options": "DENY", | ||
| "referrer-policy": "no-referrer", | ||
| "content-security-policy": "script-src 'none'; frame-ancestors 'none';" | ||
| }; | ||
| if (status === 404 || !event.res.headers.has("cache-control")) { | ||
| headers["cache-control"] = "no-cache"; | ||
| } | ||
| const body = { | ||
| error: true, | ||
| url: url.href, | ||
| const headers = new Headers(unhandled ? {} : error.headers); | ||
| headers.set("content-type", "application/json; charset=utf-8"); | ||
| const jsonBody = unhandled ? { | ||
| status, | ||
| statusText: error.statusText, | ||
| message: isSensitive ? "Server Error" : error.message, | ||
| data: isSensitive ? undefined : error.data | ||
| unhandled: true | ||
| } : typeof error.toJSON === "function" ? error.toJSON() : { | ||
| status, | ||
| statusText, | ||
| message: error.message | ||
| }; | ||
| return { | ||
| status, | ||
| statusText: error.statusText, | ||
| statusText, | ||
| headers, | ||
| body | ||
| body: { | ||
| error: true, | ||
| ...jsonBody | ||
| } | ||
| }; | ||
| } |
| import type { NitroErrorHandler } from "nitro/types"; | ||
| export declare function defineNitroErrorHandler(handler: NitroErrorHandler): NitroErrorHandler; | ||
| export type InternalHandlerResponse = { | ||
| status: number; | ||
| statusText: string | undefined; | ||
| headers: Record<string, string>; | ||
| body: string | Record<string, any>; | ||
| status?: number; | ||
| statusText?: string | undefined; | ||
| headers?: HeadersInit; | ||
| body?: string | Record<string, any>; | ||
| }; |
| import type { NitroRouteMeta } from "nitro/types"; | ||
| export declare function defineRouteMeta(meta: NitroRouteMeta); | ||
| export declare function defineRouteMeta(meta: NitroRouteMeta): NitroRouteMeta; |
| import type { NitroAppPlugin } from "nitro/types"; | ||
| export declare function defineNitroPlugin(def: NitroAppPlugin); | ||
| export declare const nitroPlugin: unknown; | ||
| export declare function defineNitroPlugin(def: NitroAppPlugin): NitroAppPlugin; | ||
| export declare const nitroPlugin: (def: NitroAppPlugin) => NitroAppPlugin; |
@@ -1,4 +0,9 @@ | ||
| export declare const headers: unknown; | ||
| export declare const redirect: unknown; | ||
| export declare const proxy: unknown; | ||
| export declare const cache: unknown; | ||
| import type { Middleware } from "h3"; | ||
| import type { MatchedRouteRule, NitroRouteRules } from "nitro/types"; | ||
| type RouteRuleCtor<T extends keyof NitroRouteRules> = (m: MatchedRouteRule<T>) => Middleware; | ||
| export declare const headers: RouteRuleCtor<"headers">; | ||
| export declare const redirect: RouteRuleCtor<"redirect">; | ||
| export declare const proxy: RouteRuleCtor<"proxy">; | ||
| export declare const cache: RouteRuleCtor<"cache">; | ||
| export declare const basicAuth: RouteRuleCtor<"auth">; | ||
| export {}; |
@@ -1,2 +0,2 @@ | ||
| import { proxyRequest, redirect as sendRedirect } from "h3"; | ||
| import { proxyRequest, redirect as sendRedirect, requireBasicAuth } from "h3"; | ||
| import { joinURL, withQuery, withoutBase } from "ufo"; | ||
@@ -65,1 +65,9 @@ import { defineCachedHandler } from "./cache.mjs"; | ||
| }); | ||
| // basicAuth auth route rule | ||
| export const basicAuth = ((m) => async function authRouteRule(event, next) { | ||
| if (!m.options) { | ||
| return; | ||
| } | ||
| await requireBasicAuth(event, m.options); | ||
| return next(); | ||
| }); |
@@ -1,2 +0,3 @@ | ||
| declare const _default; | ||
| export default _default; | ||
| import { H3 } from "h3"; | ||
| declare const app: H3; | ||
| export default app; |
| import { H3 } from "h3"; | ||
| import { runTask } from "../task.mjs"; | ||
| import { scheduledTasks, tasks } from "#nitro/virtual/tasks"; | ||
| export default new H3().get("/_nitro/tasks", async () => { | ||
| const app = new H3().get("/_nitro/tasks", async () => { | ||
| const _tasks = await Promise.all(Object.entries(tasks).map(async ([name, task]) => { | ||
@@ -20,3 +20,7 @@ const _task = await task.resolve?.(); | ||
| }; | ||
| return await runTask(name, { payload }); | ||
| return await runTask(name, { | ||
| context: { waitUntil: event.req.waitUntil }, | ||
| payload | ||
| }); | ||
| }); | ||
| export default app; |
| import type { H3Event } from "h3"; | ||
| export default function renderIndexHTML(event: H3Event); | ||
| export default function renderIndexHTML(event: H3Event): any; |
| import type { H3Event } from "h3"; | ||
| export default function renderIndexHTML(event: H3Event); | ||
| import { HTTPResponse } from "h3"; | ||
| export default function renderIndexHTML(event: H3Event): Promise<HTTPResponse | Response>; |
@@ -8,3 +8,3 @@ import type { NitroRuntimeConfig } from "nitro/types"; | ||
| }; | ||
| export declare function applyEnv(obj: Record<string, any>, opts: EnvOptions, parentKey?: string); | ||
| export declare function applyEnv(obj: Record<string, any>, opts: EnvOptions, parentKey?: string): Record<string, any>; | ||
| export {}; |
@@ -7,3 +7,4 @@ import { HTTPError, defineHandler } from "h3"; | ||
| gzip: ".gz", | ||
| br: ".br" | ||
| br: ".br", | ||
| zstd: ".zst" | ||
| }; | ||
@@ -18,5 +19,2 @@ export default defineHandler((event) => { | ||
| const encodings = [...encodingHeader.split(",").map((e) => EncodingMap[e.trim()]).filter(Boolean).sort(), ""]; | ||
| if (encodings.length > 1) { | ||
| event.res.headers.append("Vary", "Accept-Encoding"); | ||
| } | ||
| for (const encoding of encodings) { | ||
@@ -39,2 +37,5 @@ for (const _id of [id + encoding, joinURL(id, "index.html" + encoding)]) { | ||
| } | ||
| if (encodings.length > 1) { | ||
| event.res.headers.append("Vary", "Accept-Encoding"); | ||
| } | ||
| const ifNotMatch = event.req.headers.get("if-none-match") === asset.etag; | ||
@@ -41,0 +42,0 @@ if (ifNotMatch) { |
@@ -10,3 +10,5 @@ import type { Task, TaskContext, TaskPayload, TaskResult } from "nitro/types"; | ||
| /** @experimental */ | ||
| export declare function startScheduleRunner(); | ||
| export declare function startScheduleRunner({ waitUntil }?: { | ||
| waitUntil?: ((promise: Promise<unknown>) => void) | undefined; | ||
| }): void; | ||
| /** @experimental */ | ||
@@ -13,0 +15,0 @@ export declare function getCronTasks(cron: string): string[]; |
@@ -46,3 +46,3 @@ import { Cron } from "croner"; | ||
| /** @experimental */ | ||
| export function startScheduleRunner() { | ||
| export function startScheduleRunner({ waitUntil } = {}) { | ||
| if (!scheduledTasks || scheduledTasks.length === 0 || process.env.TEST) { | ||
@@ -56,3 +56,3 @@ return; | ||
| payload, | ||
| context: {} | ||
| context: { waitUntil } | ||
| }).catch((error) => { | ||
@@ -59,0 +59,0 @@ console.error(`Error while running scheduled task "${name}"`, error); |
@@ -6,2 +6,3 @@ import "#nitro/virtual/polyfills"; | ||
| import { resolveWebsocketHooks } from "#nitro/runtime/app"; | ||
| import { startScheduleRunner } from "#nitro/runtime/task"; | ||
@@ -12,6 +13,8 @@ const nitroApp = useNitroApp(); | ||
| const ws = import.meta._websocket | ||
| ? wsAdapter({ resolve: resolveWebsocketHooks }) | ||
| : undefined; | ||
| const ws = import.meta._websocket ? wsAdapter({ resolve: resolveWebsocketHooks }) : undefined; | ||
| if (import.meta._tasks) { | ||
| startScheduleRunner({}); | ||
| } | ||
| export const handleUpgrade = ws?.handleUpgrade; |
@@ -18,2 +18,3 @@ import { fileURLToPath } from "node:url"; | ||
| "ofetch", | ||
| "ocache", | ||
| "ohash", | ||
@@ -20,0 +21,0 @@ "rendu", |
@@ -8,3 +8,5 @@ import type { NitroConfig } from "nitro/types"; | ||
| export { defineNitroErrorHandler as defineErrorHandler } from "./internal/error/utils.mjs"; | ||
| export { defineHandler, defineMiddleware, defineWebSocketHandler, html, HTTPError, HTTPResponse } from "h3"; | ||
| export type { H3Event } from "h3"; | ||
| export declare function serverFetch(resource: string | URL | Request, init?: RequestInit, context?: ServerRequestContext | H3EventContext): Promise<Response>; | ||
| export declare function fetch(resource: string | URL | Request, init?: RequestInit, context?: ServerRequestContext | H3EventContext): Promise<Response>; |
@@ -9,2 +9,4 @@ import { toRequest } from "h3"; | ||
| export { defineNitroErrorHandler as defineErrorHandler } from "./internal/error/utils.mjs"; | ||
| // H3 | ||
| export { defineHandler, defineMiddleware, defineWebSocketHandler, html, HTTPError, HTTPResponse } from "h3"; | ||
| // Runtime | ||
@@ -11,0 +13,0 @@ export function serverFetch(resource, init, context) { |
| import "./_runtime_warn.mjs"; | ||
| import { toResponse } from "h3"; | ||
| import { H3Event, toResponse } from "h3"; | ||
| const errorHandler = (error, event) => { | ||
@@ -4,0 +4,0 @@ if (error.status !== 404) { |
| import "./_runtime_warn.mjs"; | ||
| import { type Storage } from "unstorage"; | ||
| import type { AssetMeta } from "nitro/types"; | ||
| export declare const assets: unknown; | ||
| export declare const assets: Storage; | ||
| export declare function readAsset<T = any>(_id: string): Promise<T>; | ||
| export declare function statAsset(_id: string): Promise<AssetMeta>; | ||
| export declare function getKeys(): Promise<string[]>; |
@@ -7,3 +7,3 @@ type FetchableEnv = { | ||
| } | ||
| export declare function fetchViteEnv(viteEnvName: string, input: RequestInfo | URL, init?: RequestInit); | ||
| export declare function fetchViteEnv(viteEnvName: string, input: RequestInfo | URL, init?: RequestInit): Promise<Response>; | ||
| export {}; |
+1
-11
@@ -1,11 +0,1 @@ | ||
| import "nitro"; | ||
| import "nitro/app"; | ||
| import "nitro/cache"; | ||
| import "nitro/context"; | ||
| import "nitro/database"; | ||
| import "nitro/h3"; | ||
| import "nitro/runtime-config"; | ||
| import "nitro/storage"; | ||
| import "nitro/task"; | ||
| export { }; | ||
| export {}; |
+3
-3
| import "vite/client"; | ||
| import "nitro/vite/types"; | ||
| import "./_dev.mjs"; | ||
| import "unenv"; | ||
| import { RunnerManager } from "env-runner"; | ||
| import { Plugin } from "vite"; | ||
@@ -33,4 +32,5 @@ import { Nitro, NitroConfig, NitroModule } from "nitro/types"; | ||
| /** | ||
| * Reload the page when a server module is updated. | ||
| * | ||
| * Invalidate server-only modules and optionally reload the browser when a server-only module is updated. | ||
| * | ||
| * @default true | ||
@@ -37,0 +37,0 @@ */ |
+236
-233
@@ -1,12 +0,17 @@ | ||
| import "./_common.mjs"; | ||
| import { $ as resolveModulePath, B as T, D as prepare, I as prettyPath, O as copyPublicAssets, V as a, at as join$1, ct as resolve$1, d as libChunkName, f as baseBuildConfig, h as writeBuildInfo, it as isAbsolute$1, l as NODE_MODULES_RE, m as getBuildInfo, n as baseBuildPlugins, nt as dirname$1, st as relative$1, tt as basename$1, u as getChunkName } from "./_build/common.mjs"; | ||
| import { i as debounce } from "./_libs/rc9+c12+dotenv.mjs"; | ||
| import "./_libs/c12+rc9.mjs"; | ||
| import { D as copyPublicAssets, E as prepare, F as prettyPath, H as v, V as m, at as dirname$1, ct as join$1, d as libChunkName, dt as resolve$1, f as baseBuildConfig, h as writeBuildInfo, it as basename$1, l as NODE_MODULES_RE, n as baseBuildPlugins, nt as resolveModulePath, st as isAbsolute$1, u as getChunkName } from "./_build/common.mjs"; | ||
| import { t as formatCompatibilityDate } from "./_libs/compatx.mjs"; | ||
| import { i as createNitro } from "./_chunks/nitro.mjs"; | ||
| import "./_libs/klona.mjs"; | ||
| import { i as createNitro, r as prerender } from "./_chunks/nitro.mjs"; | ||
| import "./_libs/escape-string-regexp.mjs"; | ||
| import "./_libs/tsconfck.mjs"; | ||
| import { n as scanHandlers } from "./_chunks/nitro2.mjs"; | ||
| import { i as NodeEnvRunner, r as NitroDevApp } from "./_chunks/dev.mjs"; | ||
| import "./_libs/rou3.mjs"; | ||
| import { n as watch$1 } from "./_libs/readdirp+chokidar.mjs"; | ||
| import { t as debounce } from "./_libs/perfect-debounce.mjs"; | ||
| import "./_libs/httpxy.mjs"; | ||
| import { r as NitroDevApp } from "./_dev.mjs"; | ||
| import "./_libs/ultrahtml.mjs"; | ||
| import { t as startPreview } from "./_chunks/nitro3.mjs"; | ||
| import { n as assetsPlugin } from "./_libs/pluginutils.mjs"; | ||
| import consola$1 from "consola"; | ||
| import { existsSync, watch } from "node:fs"; | ||
@@ -18,17 +23,17 @@ import { mkdir, readFile, rm, writeFile } from "node:fs/promises"; | ||
| import { colors } from "consola/utils"; | ||
| import { IncomingMessage } from "node:http"; | ||
| import { RunnerManager, loadRunner } from "env-runner"; | ||
| import { NodeRequest, sendNodeResponse } from "srvx/node"; | ||
| import "node:http"; | ||
| import { DevEnvironment } from "vite"; | ||
| import { spawn } from "node:child_process"; | ||
| import { createViteHotChannel } from "env-runner/vite"; | ||
| //#region src/build/vite/bundler.ts | ||
| const getBundlerConfig = async (ctx) => { | ||
| const nitro$1 = ctx.nitro; | ||
| const base = baseBuildConfig(nitro$1); | ||
| const nitro = ctx.nitro; | ||
| const base = baseBuildConfig(nitro); | ||
| const commonConfig = { | ||
| input: nitro$1.options.entry, | ||
| input: nitro.options.entry, | ||
| external: [...base.env.external], | ||
| plugins: [...await baseBuildPlugins(nitro$1, base)].filter(Boolean), | ||
| plugins: [...await baseBuildPlugins(nitro, base)].filter(Boolean), | ||
| treeshake: { moduleSideEffects(id) { | ||
| return nitro$1.options.moduleSideEffects.some((p) => id.startsWith(p)); | ||
| return nitro.options.moduleSideEffects.some((p) => id.startsWith(p)); | ||
| } }, | ||
@@ -39,14 +44,12 @@ onwarn(warning, warn) { | ||
| output: { | ||
| dir: nitro$1.options.output.serverDir, | ||
| dir: nitro.options.output.serverDir, | ||
| format: "esm", | ||
| entryFileNames: "index.mjs", | ||
| chunkFileNames: (chunk) => getChunkName(chunk, nitro$1), | ||
| inlineDynamicImports: nitro$1.options.inlineDynamicImports, | ||
| chunkFileNames: (chunk) => getChunkName(chunk, nitro), | ||
| inlineDynamicImports: nitro.options.inlineDynamicImports, | ||
| sourcemapIgnoreList: (id) => id.includes("node_modules") | ||
| } | ||
| }; | ||
| if (ctx._isRolldown) return { | ||
| base, | ||
| rollupConfig: void 0, | ||
| rolldownConfig: defu({ | ||
| if (ctx._isRolldown) { | ||
| const rolldownConfig = defu({ | ||
| transform: { inject: base.env.inject }, | ||
@@ -57,24 +60,33 @@ output: { codeSplitting: { groups: [{ | ||
| }] } } | ||
| }, nitro$1.options.rolldownConfig, nitro$1.options.rollupConfig, commonConfig) | ||
| }; | ||
| else { | ||
| const inject = (await import("./_libs/plugin-inject.mjs").then((n) => n.t)).default; | ||
| }, nitro.options.rolldownConfig, nitro.options.rollupConfig, commonConfig); | ||
| const outputConfig = rolldownConfig.output; | ||
| if (outputConfig.inlineDynamicImports || outputConfig.format === "iife") { | ||
| delete outputConfig.inlineDynamicImports; | ||
| outputConfig.codeSplitting = false; | ||
| } | ||
| return { | ||
| base, | ||
| rolldownConfig | ||
| }; | ||
| } else { | ||
| const inject = (await import("./_libs/_3.mjs")).default; | ||
| const alias = (await import("./_libs/plugin-alias.mjs").then((n) => n.n)).default; | ||
| const rollupConfig = defu({ | ||
| plugins: [inject(base.env.inject), alias({ entries: base.aliases })], | ||
| output: { | ||
| sourcemapExcludeSources: true, | ||
| generatedCode: { constBindings: true }, | ||
| manualChunks(id) { | ||
| if (NODE_MODULES_RE.test(id)) return libChunkName(id); | ||
| } | ||
| } | ||
| }, nitro.options.rolldownConfig, nitro.options.rollupConfig, commonConfig); | ||
| const outputConfig = rollupConfig.output; | ||
| if (outputConfig.inlineDynamicImports || outputConfig.format === "iife") delete outputConfig.manualChunks; | ||
| return { | ||
| base, | ||
| rolldownConfig: void 0, | ||
| rollupConfig: defu({ | ||
| plugins: [inject(base.env.inject), alias({ entries: base.aliases })], | ||
| output: { | ||
| sourcemapExcludeSources: true, | ||
| generatedCode: { constBindings: true }, | ||
| manualChunks(id) { | ||
| if (NODE_MODULES_RE.test(id)) return libChunkName(id); | ||
| } | ||
| } | ||
| }, nitro$1.options.rolldownConfig, nitro$1.options.rollupConfig, commonConfig) | ||
| rollupConfig | ||
| }; | ||
| } | ||
| }; | ||
| //#endregion | ||
@@ -88,3 +100,3 @@ //#region src/build/vite/prod.ts | ||
| async function buildEnvironments(ctx, builder) { | ||
| const nitro$1 = ctx.nitro; | ||
| const nitro = ctx.nitro; | ||
| for (const [envName, env] of Object.entries(builder.environments)) { | ||
@@ -97,7 +109,7 @@ const fmtName = BuilderNames[envName] || (envName.length <= 3 ? envName.toUpperCase() : envName[0].toUpperCase() + envName.slice(1)); | ||
| "client" | ||
| ].includes(envName)) nitro$1.logger.info(env.isBuilt ? `Skipping ${fmtName} (already built)` : `Skipping ${fmtName} (no input defined)`); | ||
| ].includes(envName)) nitro.logger.info(env.isBuilt ? `Skipping ${fmtName} (already built)` : `Skipping ${fmtName} (no input defined)`); | ||
| continue; | ||
| } | ||
| if (!a && !T) console.log(); | ||
| nitro$1.logger.start(`Building [${fmtName}]`); | ||
| if (!v && !m) console.log(); | ||
| nitro.logger.start(`Building [${fmtName}]`); | ||
| await builder.build(env); | ||
@@ -119,9 +131,9 @@ } | ||
| await builder.writeAssetsManifest?.(); | ||
| if (!a && !T) console.log(); | ||
| const buildInfo = [["preset", nitro$1.options.preset], ["compatibility", formatCompatibilityDate(nitro$1.options.compatibilityDate)]].filter((e) => e[1]); | ||
| nitro$1.logger.start(`Building [${BuilderNames.nitro}] ${colors.dim(`(${buildInfo.map(([k, v]) => `${k}: \`${v}\``).join(", ")})`)}`); | ||
| await copyPublicAssets(nitro$1); | ||
| if (!v && !m) console.log(); | ||
| const buildInfo = [["preset", nitro.options.preset], ["compatibility", formatCompatibilityDate(nitro.options.compatibilityDate)]].filter((e) => e[1]); | ||
| nitro.logger.start(`Building [${BuilderNames.nitro}] ${colors.dim(`(${buildInfo.map(([k, v]) => `${k}: \`${v}\``).join(", ")})`)}`); | ||
| await copyPublicAssets(nitro); | ||
| const assetDirs = new Set(Object.values(builder.environments).filter((env) => env.config.consumer === "client").map((env) => env.config.build.assetsDir).filter(Boolean)); | ||
| for (const assetsDir of assetDirs) { | ||
| if (!existsSync(resolve$1(nitro$1.options.output.publicDir, assetsDir))) continue; | ||
| if (!existsSync(resolve$1(nitro.options.output.publicDir, assetsDir))) continue; | ||
| const rule = ctx.nitro.options.routeRules[`/${assetsDir}/**`] ??= {}; | ||
@@ -134,13 +146,10 @@ if (!rule.headers?.["cache-control"]) rule.headers = { | ||
| ctx.nitro.routing.sync(); | ||
| await builder.build(builder.environments.nitro); | ||
| await nitro$1.close(); | ||
| await nitro$1.hooks.callHook("compiled", nitro$1); | ||
| await writeBuildInfo(nitro$1); | ||
| const rOutput = relative$1(process.cwd(), nitro$1.options.output.dir); | ||
| const rewriteRelativePaths = (input) => { | ||
| return input.replace(/([\s:])\.\/(\S*)/g, `$1${rOutput}/$2`); | ||
| }; | ||
| if (!a && !T) console.log(); | ||
| if (nitro$1.options.commands.preview) nitro$1.logger.success(`You can preview this build using \`${rewriteRelativePaths(nitro$1.options.commands.preview)}\``); | ||
| if (nitro$1.options.commands.deploy) nitro$1.logger.success(`You can deploy this build using \`${rewriteRelativePaths(nitro$1.options.commands.deploy)}\``); | ||
| await prerender(nitro); | ||
| const output = await builder.build(builder.environments.nitro); | ||
| await nitro.close(); | ||
| await nitro.hooks.callHook("compiled", nitro); | ||
| await writeBuildInfo(nitro, output); | ||
| if (!v && !m) console.log(); | ||
| nitro.logger.success("You can preview this build using `npx vite preview`"); | ||
| if (nitro.options.commands.deploy) nitro.logger.success("You can deploy this build using `npx nitro deploy --prebuilt`"); | ||
| } | ||
@@ -171,16 +180,33 @@ function prodSetup(ctx) { | ||
| } | ||
| //#endregion | ||
| //#region src/build/vite/dev.ts | ||
| function createFetchableDevEnvironment(name, config, devServer, entry) { | ||
| function createFetchableDevEnvironment(name, config, devServer, entry, opts) { | ||
| return new FetchableDevEnvironment(name, config, { | ||
| hot: true, | ||
| transport: createTransport(name, devServer) | ||
| }, devServer, entry); | ||
| transport: createViteHotChannel(devServer, name) | ||
| }, devServer, entry, opts); | ||
| } | ||
| var FetchableDevEnvironment = class extends DevEnvironment { | ||
| devServer; | ||
| constructor(name, config, context, devServer, entry) { | ||
| #entry; | ||
| #preventExternalize; | ||
| constructor(name, config, context, devServer, entry, opts) { | ||
| super(name, config, context); | ||
| this.devServer = devServer; | ||
| this.#entry = entry; | ||
| this.#preventExternalize = opts?.preventExternalize ?? false; | ||
| } | ||
| async fetchModule(id, importer, options) { | ||
| if (this.#preventExternalize && !id.startsWith("file://") && importer && id[0] !== "." && id[0] !== "/") { | ||
| const resolved = await this.pluginContainer.resolveId(id, importer); | ||
| if (resolved && !resolved.external) return super.fetchModule(resolved.id, importer, options); | ||
| } | ||
| return super.fetchModule(id, importer, options); | ||
| } | ||
| async dispatchFetch(request) { | ||
| return this.devServer.fetch(request); | ||
| } | ||
| async init(...args) { | ||
| await this.devServer.init?.(); | ||
| await super.init(...args); | ||
| this.devServer.sendMessage({ | ||
@@ -190,61 +216,30 @@ type: "custom", | ||
| data: { | ||
| name, | ||
| entry | ||
| name: this.name, | ||
| entry: this.#entry | ||
| } | ||
| }); | ||
| } | ||
| async dispatchFetch(request) { | ||
| return this.devServer.fetch(request); | ||
| } | ||
| async init(...args) { | ||
| await this.devServer.init?.(); | ||
| return super.init(...args); | ||
| } | ||
| }; | ||
| function createTransport(name, hooks) { | ||
| const listeners = /* @__PURE__ */ new WeakMap(); | ||
| return { | ||
| send: (data) => hooks.sendMessage({ | ||
| ...data, | ||
| viteEnv: name | ||
| }), | ||
| on: (event, handler) => { | ||
| if (event === "connection") return; | ||
| const listener = (value) => { | ||
| if (value?.type === "custom" && value.event === event && value.viteEnv === name) handler(value.data, { send: (payload) => hooks.sendMessage({ | ||
| ...payload, | ||
| viteEnv: name | ||
| }) }); | ||
| }; | ||
| listeners.set(handler, listener); | ||
| hooks.onMessage(listener); | ||
| }, | ||
| off: (event, handler) => { | ||
| if (event === "connection") return; | ||
| const listener = listeners.get(handler); | ||
| if (listener) { | ||
| hooks.offMessage(listener); | ||
| listeners.delete(handler); | ||
| } | ||
| } | ||
| }; | ||
| } | ||
| async function configureViteDevServer(ctx, server) { | ||
| const nitro$1 = ctx.nitro; | ||
| const nitroEnv$1 = server.environments.nitro; | ||
| const nitroConfigFile = nitro$1.options._c12.configFile; | ||
| const nitro = ctx.nitro; | ||
| const nitroEnv = server.environments.nitro; | ||
| const nitroConfigFile = nitro.options._c12.configFile; | ||
| if (nitroConfigFile) server.config.configFileDependencies.push(nitroConfigFile); | ||
| if (nitro$1.options.features.websocket ?? nitro$1.options.experimental.websocket) server.httpServer.on("upgrade", (req, socket, head) => { | ||
| if (req.url?.startsWith("/?token")) return; | ||
| getEnvRunner(ctx).upgrade?.(req, socket, head); | ||
| if (nitro.options.features.websocket ?? nitro.options.experimental.websocket) server.httpServer.on("upgrade", (req, socket, head) => { | ||
| if (req.headers["sec-websocket-protocol"]?.startsWith("vite-")) return; | ||
| getEnvRunner(ctx).upgrade?.({ node: { | ||
| req, | ||
| socket, | ||
| head | ||
| } }); | ||
| }); | ||
| const reload = debounce(async () => { | ||
| await scanHandlers(nitro$1); | ||
| nitro$1.routing.sync(); | ||
| nitroEnv$1.moduleGraph.invalidateAll(); | ||
| nitroEnv$1.hot.send({ type: "full-reload" }); | ||
| await scanHandlers(nitro); | ||
| nitro.routing.sync(); | ||
| nitroEnv.moduleGraph.invalidateAll(); | ||
| nitroEnv.hot.send({ type: "full-reload" }); | ||
| }); | ||
| const scanDirs = nitro$1.options.scanDirs.flatMap((dir) => [ | ||
| join$1(dir, nitro$1.options.apiDir || "api"), | ||
| join$1(dir, nitro$1.options.routesDir || "routes"), | ||
| const scanDirs = nitro.options.scanDirs.flatMap((dir) => [ | ||
| join$1(dir, nitro.options.apiDir || "api"), | ||
| join$1(dir, nitro.options.routesDir || "routes"), | ||
| join$1(dir, "middleware"), | ||
@@ -260,26 +255,24 @@ join$1(dir, "plugins"), | ||
| ]); | ||
| const scanDirsWatcher = watch$1(scanDirs, { ignoreInitial: true }).on("all", (event, path$1, stat$2) => { | ||
| const scanDirsWatcher = watch$1(scanDirs, { ignoreInitial: true }).on("all", (event, path, stat) => { | ||
| if (watchReloadEvents.has(event)) reload(); | ||
| }); | ||
| const rootDirWatcher = watch(nitro$1.options.rootDir, { persistent: false }, (_event, filename) => { | ||
| const rootDirWatcher = watch(nitro.options.rootDir, { persistent: false }, (_event, filename) => { | ||
| if (filename && /^server\.[mc]?[jt]sx?$/.test(filename)) reload(); | ||
| }); | ||
| nitro$1.hooks.hook("close", () => { | ||
| nitro.hooks.hook("close", () => { | ||
| scanDirsWatcher.close(); | ||
| rootDirWatcher.close(); | ||
| }); | ||
| const hostIPC = { async transformHTML(html) { | ||
| return server.transformIndexHtml("/", html).then((r) => r.replace("<!--ssr-outlet-->", `{{{ globalThis.__nitro_vite_envs__?.["ssr"]?.fetch($REQUEST) || "" }}}`)); | ||
| } }; | ||
| nitroEnv$1.devServer.onMessage(async (payload) => { | ||
| if (payload.type === "custom" && payload.event === "nitro:vite-invoke") { | ||
| const res = await hostIPC[payload.data.name](payload.data.data).then((data) => ({ data })).catch((error) => ({ error })); | ||
| nitroEnv$1.devServer.sendMessage({ | ||
| type: "custom", | ||
| event: "nitro:vite-invoke-response", | ||
| data: { | ||
| id: payload.data.id, | ||
| data: res | ||
| } | ||
| nitroEnv.devServer.onMessage(async (message) => { | ||
| if (message?.__rpc === "transformHTML") try { | ||
| const html = (await server.transformIndexHtml("/", message.data)).replace("<!--ssr-outlet-->", `{{{ globalThis.__nitro_vite_envs__?.["ssr"]?.fetch($REQUEST) || "" }}}`); | ||
| nitroEnv.devServer.sendMessage({ | ||
| __rpc_id: message.__rpc_id, | ||
| data: html | ||
| }); | ||
| } catch (error) { | ||
| nitroEnv.devServer.sendMessage({ | ||
| __rpc_id: message.__rpc_id, | ||
| error: error instanceof Error ? error.message : String(error) | ||
| }); | ||
| } | ||
@@ -298,3 +291,3 @@ }); | ||
| if (devAppRes.status !== 404) return await sendNodeResponse(nodeRes, devAppRes); | ||
| const envRes = await nitroEnv$1.dispatchFetch(req); | ||
| const envRes = await nitroEnv.dispatchFetch(req); | ||
| if (nodeRes.writableEnded || nodeRes.headersSent) return; | ||
@@ -316,13 +309,6 @@ return await sendNodeResponse(nodeRes, envRes); | ||
| } | ||
| //#endregion | ||
| //#region src/build/vite/env.ts | ||
| function getEnvRunner(ctx) { | ||
| return ctx._envRunner ??= new NodeEnvRunner({ | ||
| name: "nitro-vite", | ||
| entry: resolve(runtimeDir, "internal/vite/node-runner.mjs"), | ||
| data: { server: true } | ||
| }); | ||
| } | ||
| function createNitroEnvironment(ctx) { | ||
| const isWorkerdRunner = _isWorkerdRunner(ctx); | ||
| return { | ||
@@ -336,18 +322,30 @@ consumer: "server", | ||
| sourcemap: ctx.nitro.options.sourcemap, | ||
| commonjsOptions: ctx.nitro.options.commonJS | ||
| commonjsOptions: ctx.nitro.options.commonJS, | ||
| copyPublicDir: false | ||
| }, | ||
| resolve: { | ||
| noExternal: ctx.nitro.options.dev ? [ | ||
| noExternal: ctx.nitro.options.dev ? isWorkerdRunner ? true : [ | ||
| /^nitro$/, | ||
| /* @__PURE__ */ new RegExp(`^(${runtimeDependencies.join("|")})$`), | ||
| new RegExp(`^(${runtimeDependencies.join("|")})$`), | ||
| ...ctx.bundlerConfig.base.noExternal | ||
| ] : true, | ||
| conditions: ctx.nitro.options.exportConditions, | ||
| conditions: isWorkerdRunner ? [ | ||
| "workerd", | ||
| "worker", | ||
| ...ctx.nitro.options.exportConditions.filter((c) => c !== "node") | ||
| ] : ctx.nitro.options.exportConditions, | ||
| externalConditions: ctx.nitro.options.exportConditions?.filter((c) => !/browser|wasm|module/.test(c)) | ||
| }, | ||
| define: { "process.env.NODE_ENV": JSON.stringify(ctx.nitro.options.dev ? "development" : "production") }, | ||
| dev: { createEnvironment: (envName, envConfig) => createFetchableDevEnvironment(envName, envConfig, getEnvRunner(ctx), resolve(runtimeDir, "internal/vite/dev-entry.mjs")) } | ||
| dev: { createEnvironment: (envName, envConfig) => { | ||
| const entry = resolve(runtimeDir, "internal/vite/dev-entry.mjs"); | ||
| const env = createFetchableDevEnvironment(envName, envConfig, getEnvRunner(ctx), entry, { preventExternalize: isWorkerdRunner }); | ||
| ctx._transformRequest = (id) => env.transformRequest(id); | ||
| (ctx._viteEnvs ??= /* @__PURE__ */ new Map()).set(envName, entry); | ||
| return env; | ||
| } } | ||
| }; | ||
| } | ||
| function createServiceEnvironment(ctx, name, serviceConfig) { | ||
| const isWorkerdRunner = _isWorkerdRunner(ctx); | ||
| return { | ||
@@ -360,9 +358,19 @@ consumer: "server", | ||
| outDir: join(ctx.nitro.options.buildDir, "vite/services", name), | ||
| emptyOutDir: true | ||
| emptyOutDir: true, | ||
| copyPublicDir: false | ||
| }, | ||
| resolve: { | ||
| conditions: ctx.nitro.options.exportConditions, | ||
| ...isWorkerdRunner ? { noExternal: true } : {}, | ||
| conditions: isWorkerdRunner ? [ | ||
| "workerd", | ||
| "worker", | ||
| ...ctx.nitro.options.exportConditions.filter((c) => c !== "node") | ||
| ] : ctx.nitro.options.exportConditions, | ||
| externalConditions: ctx.nitro.options.exportConditions?.filter((c) => !/browser|wasm|module/.test(c)) | ||
| }, | ||
| dev: { createEnvironment: (envName, envConfig) => createFetchableDevEnvironment(envName, envConfig, getEnvRunner(ctx), tryResolve(serviceConfig.entry)) } | ||
| dev: { createEnvironment: (envName, envConfig) => { | ||
| const entry = tryResolve(serviceConfig.entry); | ||
| (ctx._viteEnvs ??= /* @__PURE__ */ new Map()).set(envName, entry); | ||
| return createFetchableDevEnvironment(envName, envConfig, getEnvRunner(ctx), entry, { preventExternalize: isWorkerdRunner }); | ||
| } } | ||
| }; | ||
@@ -373,2 +381,53 @@ } | ||
| } | ||
| async function initEnvRunner(ctx) { | ||
| if (ctx._envRunner) return ctx._envRunner; | ||
| if (!ctx._initPromise) ctx._initPromise = (async () => { | ||
| const manager = new RunnerManager(); | ||
| let _retries = 0; | ||
| manager.onClose((_runner, cause) => { | ||
| if (_retries++ < 3) { | ||
| ctx.nitro.logger.info("Restarting env runner...", cause ? `Cause: ${cause}` : ""); | ||
| _loadRunner(ctx, manager); | ||
| } else ctx.nitro.logger.error("Env runner failed after 3 retries.", cause ? `Last cause: ${cause}` : ""); | ||
| }); | ||
| manager.onReady(() => { | ||
| _retries = 0; | ||
| if (ctx._viteEnvs) for (const [name, entry] of ctx._viteEnvs) manager.sendMessage({ | ||
| type: "custom", | ||
| event: "nitro:vite-env", | ||
| data: { | ||
| name, | ||
| entry | ||
| } | ||
| }); | ||
| }); | ||
| await _loadRunner(ctx, manager); | ||
| ctx._envRunner = manager; | ||
| return manager; | ||
| })(); | ||
| return await ctx._initPromise; | ||
| } | ||
| function getEnvRunner(ctx) { | ||
| if (!ctx._envRunner) throw new Error("Env runner not initialized. Call initEnvRunner() first."); | ||
| return ctx._envRunner; | ||
| } | ||
| async function _loadRunner(ctx, manager) { | ||
| const runnerName = ctx.nitro.options.devServer.runner || process.env.NITRO_DEV_RUNNER || "node-worker"; | ||
| const entry = resolve(runtimeDir, "internal/vite/dev-worker.mjs"); | ||
| let runner; | ||
| if (runnerName === "miniflare") { | ||
| const { MiniflareEnvRunner } = await import("env-runner/runners/miniflare"); | ||
| runner = new MiniflareEnvRunner({ | ||
| name: "nitro-vite", | ||
| data: { entry } | ||
| }); | ||
| } else runner = await loadRunner(runnerName, { | ||
| name: "nitro-vite", | ||
| data: { entry } | ||
| }); | ||
| await manager.reload(runner); | ||
| } | ||
| function _isWorkerdRunner(ctx) { | ||
| return (ctx.nitro.options.devServer.runner || process.env.NITRO_DEV_RUNNER || "node-worker") === "miniflare"; | ||
| } | ||
| function tryResolve(id) { | ||
@@ -390,3 +449,2 @@ if (/^[~#/\0]/.test(id) || isAbsolute$1(id)) return id; | ||
| } | ||
| //#endregion | ||
@@ -402,83 +460,23 @@ //#region src/build/vite/preview.ts | ||
| async configurePreviewServer(server) { | ||
| const { outputDir, buildInfo } = await getBuildInfo(server.config.root); | ||
| if (!buildInfo) throw this.error("Cannot load nitro build info. Make sure to build first."); | ||
| const info = [ | ||
| ["Build Directory:", prettyPath(outputDir)], | ||
| ["Date:", buildInfo.date && new Date(buildInfo.date).toLocaleString()], | ||
| ["Nitro Version:", buildInfo.versions.nitro], | ||
| ["Nitro Preset:", buildInfo.preset], | ||
| buildInfo.framework?.name !== "nitro" && ["Framework:", buildInfo.framework?.name + (buildInfo.framework?.version ? ` (v${buildInfo.framework.version})` : "")] | ||
| ].filter((i) => i && i[1]); | ||
| consola$1.box({ | ||
| title: " [Build Info] ", | ||
| message: info.map((i) => `- ${i[0]} ${i[1]}`).join("\n") | ||
| const preview = await startPreview({ | ||
| rootDir: server.config.root, | ||
| loader: { nodeServer: server.httpServer } | ||
| }); | ||
| if (!buildInfo.commands?.preview) { | ||
| consola$1.warn("No nitro build preview command found for this preset."); | ||
| return; | ||
| } | ||
| const dotEnvEntries = await loadPreviewDotEnv(server.config.root); | ||
| if (dotEnvEntries.length > 0) consola$1.box({ | ||
| title: " [Environment Variables] ", | ||
| message: [ | ||
| "Loaded variables from .env files (preview mode only).", | ||
| "Set platform environment variables for production:", | ||
| ...dotEnvEntries.map(([key, val]) => ` - ${key}`) | ||
| ].join("\n") | ||
| server.httpServer.once("close", async () => { | ||
| await preview.close(); | ||
| }); | ||
| const [command, ...args] = buildInfo.commands.preview.split(" "); | ||
| consola$1.info(`Spawning preview server...`); | ||
| consola$1.info(buildInfo.commands?.preview); | ||
| console.log(""); | ||
| const { getRandomPort, waitForPort } = await import("get-port-please"); | ||
| const randomPort = await getRandomPort(); | ||
| const child = spawn(command, args, { | ||
| stdio: "inherit", | ||
| cwd: outputDir, | ||
| env: { | ||
| ...process.env, | ||
| ...Object.fromEntries(dotEnvEntries), | ||
| PORT: String(randomPort) | ||
| } | ||
| const { NodeRequest, sendNodeResponse } = await import("srvx/node"); | ||
| server.middlewares.use(async (req, res, next) => { | ||
| const nodeReq = new NodeRequest({ | ||
| req, | ||
| res | ||
| }); | ||
| await sendNodeResponse(res, await preview.fetch(nodeReq)).catch(next); | ||
| }); | ||
| const killChild = (signal) => { | ||
| if (child && !child.killed) child.kill(signal); | ||
| }; | ||
| for (const sig of ["SIGINT", "SIGHUP"]) process.once(sig, () => { | ||
| consola$1.info(`Stopping preview server...`); | ||
| killChild(sig); | ||
| process.exit(); | ||
| if (preview.upgrade) server.httpServer.on("upgrade", (req, socket, head) => { | ||
| preview.upgrade(req, socket, head); | ||
| }); | ||
| server.httpServer.once("close", () => { | ||
| killChild("SIGTERM"); | ||
| }); | ||
| child.once("exit", (code) => { | ||
| if (code && code !== 0) consola$1.error(`[nitro] Preview server exited with code ${code}`); | ||
| }); | ||
| const { createProxyServer } = await import("./_libs/httpxy.mjs").then((n) => n.n); | ||
| const proxy = createProxyServer({ target: `http://localhost:${randomPort}` }); | ||
| server.middlewares.use((req, res, next) => { | ||
| if (child && !child.killed) proxy.web(req, res).catch(next); | ||
| else res.end(`Nitro preview server is not running.`); | ||
| }); | ||
| await waitForPort(randomPort, { | ||
| retries: 20, | ||
| delay: 500 | ||
| }); | ||
| } | ||
| }; | ||
| } | ||
| async function loadPreviewDotEnv(root) { | ||
| const { loadDotenv } = await import("./_libs/rc9+c12+dotenv.mjs").then((n) => n.t); | ||
| const env = await loadDotenv({ | ||
| cwd: root, | ||
| fileName: [ | ||
| ".env.preview", | ||
| ".env.production", | ||
| ".env" | ||
| ] | ||
| }); | ||
| return Object.entries(env).filter(([_key, val]) => val); | ||
| } | ||
| //#endregion | ||
@@ -496,2 +494,3 @@ //#region src/build/vite/plugin.ts | ||
| function nitro(pluginConfig = {}) { | ||
| if (globalThis.__nitro_build__) return []; | ||
| const ctx = createContext(pluginConfig); | ||
@@ -552,2 +551,3 @@ return [ | ||
| config.build.outDir = useNitro(ctx).options.output.publicDir; | ||
| config.build.copyPublicDir ??= false; | ||
| return; | ||
@@ -619,15 +619,17 @@ } | ||
| async hotUpdate({ server, modules, timestamp }) { | ||
| if (ctx.pluginConfig.experimental?.vite?.serverReload === false) return; | ||
| const env = this.environment; | ||
| if (ctx.pluginConfig.experimental?.vite.serverReload === false || env.config.consumer === "client") return; | ||
| const clientEnvs = Object.values(server.environments).filter((env$1) => env$1.config.consumer === "client"); | ||
| let hasServerOnlyModule = false; | ||
| if (env.config.consumer === "client") return; | ||
| const clientEnvs = Object.values(server.environments).filter((env) => env.config.consumer === "client"); | ||
| const serverOnlyModules = []; | ||
| const sharedModules = []; | ||
| const invalidated = /* @__PURE__ */ new Set(); | ||
| for (const mod of modules) if (mod.id && !clientEnvs.some((env$1) => env$1.moduleGraph.getModuleById(mod.id))) { | ||
| hasServerOnlyModule = true; | ||
| for (const mod of modules) if (mod.id && !clientEnvs.some((env) => env.moduleGraph.getModuleById(mod.id))) { | ||
| serverOnlyModules.push(mod); | ||
| env.moduleGraph.invalidateModule(mod, invalidated, timestamp, false); | ||
| } | ||
| if (hasServerOnlyModule) { | ||
| } else sharedModules.push(mod); | ||
| if (serverOnlyModules.length > 0) { | ||
| env.hot.send({ type: "full-reload" }); | ||
| server.ws.send({ type: "full-reload" }); | ||
| return []; | ||
| if (sharedModules.length === 0 && serverOnlyModules.some((m) => m.environment !== "ssr")) server.ws.send({ type: "full-reload" }); | ||
| return sharedModules; | ||
| } | ||
@@ -695,3 +697,5 @@ } | ||
| for (const plugin of flattenPlugins(userConfig.plugins || [])) if (plugin.nitro) nitroConfig.modules.push(plugin.nitro); | ||
| ctx.nitro = ctx.pluginConfig._nitro || await createNitro(nitroConfig); | ||
| const dotenvFileNames = [".env", ".env.local"]; | ||
| if (configEnv.mode) dotenvFileNames.push(`.env.${configEnv.mode}`, `.env.${configEnv.mode}.local`); | ||
| ctx.nitro = ctx.pluginConfig._nitro || await createNitro(nitroConfig, { dotenv: { fileName: dotenvFileNames } }); | ||
| if (!ctx.services?.ssr) if (userConfig.environments?.ssr === void 0) { | ||
@@ -741,3 +745,3 @@ const ssrEntry = resolveModulePath("./entry-server", { | ||
| await ctx.nitro.hooks.callHook("rollup:before", ctx.nitro, ctx.bundlerConfig.rollupConfig || ctx.bundlerConfig.rolldownConfig); | ||
| if (ctx.nitro.options.dev) getEnvRunner(ctx); | ||
| if (ctx.nitro.options.dev) await initEnvRunner(ctx); | ||
| ctx.nitro.fetch = (req) => getEnvRunner(ctx).fetch(req); | ||
@@ -757,4 +761,3 @@ if (ctx.nitro.options.dev && !ctx.devApp) ctx.devApp = new NitroDevApp(ctx.nitro); | ||
| } | ||
| //#endregion | ||
| export { nitro }; | ||
| export { nitro }; |
+6
-0
@@ -22,1 +22,7 @@ MIT License | ||
| SOFTWARE. | ||
| ## Third-Party Licenses | ||
| This software includes bundled third-party dependencies. The licenses and | ||
| copyright notices for these dependencies are available in | ||
| `dist/THIRD-PARTY-LICENSES.md` within the distributed package. |
+83
-68
| { | ||
| "name": "nitro", | ||
| "version": "3.0.1-alpha.2", | ||
| "version": "3.0.260311-beta", | ||
| "description": "Build and Deploy Universal JavaScript Servers", | ||
@@ -17,5 +17,14 @@ "keywords": [ | ||
| "homepage": "https://nitro.build", | ||
| "license": "MIT", | ||
| "repository": "nitrojs/nitro", | ||
| "license": "MIT", | ||
| "bin": { | ||
| "nitro": "./dist/cli/index.mjs" | ||
| }, | ||
| "files": [ | ||
| "dist", | ||
| "lib", | ||
| "skills" | ||
| ], | ||
| "type": "module", | ||
| "types": "./lib/index.d.mts", | ||
| "imports": { | ||
@@ -45,10 +54,2 @@ "#nitro/runtime/*": "./dist/runtime/internal/*.mjs", | ||
| }, | ||
| "types": "./lib/index.d.mts", | ||
| "bin": { | ||
| "nitro": "./dist/cli/index.mjs" | ||
| }, | ||
| "files": [ | ||
| "dist", | ||
| "lib" | ||
| ], | ||
| "scripts": { | ||
@@ -61,36 +62,32 @@ "build": "pnpm gen-presets && obuild", | ||
| "gen-presets": "obuild --stub && node ./scripts/gen-presets.ts", | ||
| "lint": "eslint --cache . && prettier -c .", | ||
| "lint:fix": "automd && eslint --cache --fix . && prettier -w .", | ||
| "lint": "oxlint . && oxfmt --check .", | ||
| "format": "automd && oxlint --fix . && oxfmt .", | ||
| "nitro": "node ./src/cli/index.ts", | ||
| "release": "pnpm test && pnpm build && changelogen --release --prerelease --push", | ||
| "release": "node ./scripts/release.js", | ||
| "stub": "obuild --stub", | ||
| "test": "pnpm lint && pnpm test:types && pnpm test:rollup && pnpm test:rolldown", | ||
| "test": "pnpm lint && pnpm typecheck && pnpm test:rollup && pnpm test:rolldown", | ||
| "test:rolldown": "NITRO_BUILDER=rolldown pnpm vitest", | ||
| "test:rollup": "NITRO_BUILDER=rollup pnpm vitest", | ||
| "test:types": "tsc --noEmit" | ||
| "typecheck": "tsgo --noEmit --skipLibCheck" | ||
| }, | ||
| "resolutions": { | ||
| "nitro": "link:.", | ||
| "undici": "^7.18.2" | ||
| }, | ||
| "dependencies": { | ||
| "consola": "^3.4.2", | ||
| "crossws": "^0.4.3", | ||
| "crossws": "^0.4.4", | ||
| "db0": "^0.3.4", | ||
| "h3": "^2.0.1-rc.11", | ||
| "jiti": "^2.6.1", | ||
| "nf3": "^0.3.5", | ||
| "env-runner": "^0.1.6", | ||
| "h3": "^2.0.1-rc.16", | ||
| "hookable": "^6.0.1", | ||
| "nf3": "^0.3.11", | ||
| "ocache": "^0.1.2", | ||
| "ofetch": "^2.0.0-alpha.3", | ||
| "ohash": "^2.0.11", | ||
| "oxc-minify": "^0.110.0", | ||
| "oxc-transform": "^0.110.0", | ||
| "srvx": "^0.10.1", | ||
| "undici": "^7.18.2", | ||
| "rolldown": "^1.0.0-rc.8", | ||
| "srvx": "^0.11.9", | ||
| "unenv": "^2.0.0-rc.24", | ||
| "unstorage": "^2.0.0-alpha.5" | ||
| "unstorage": "^2.0.0-alpha.6" | ||
| }, | ||
| "devDependencies": { | ||
| "@azure/functions": "^3.5.1", | ||
| "@azure/static-web-apps-cli": "^2.0.7", | ||
| "@cloudflare/workers-types": "^4.20260120.0", | ||
| "@azure/static-web-apps-cli": "^2.0.8", | ||
| "@cloudflare/workers-types": "^4.20260310.1", | ||
| "@deno/types": "^0.0.1", | ||
@@ -101,3 +98,3 @@ "@hiogawa/vite-plugin-fullstack": "^0.0.11", | ||
| "@rollup/plugin-alias": "^6.0.0", | ||
| "@rollup/plugin-commonjs": "^29.0.0", | ||
| "@rollup/plugin-commonjs": "^29.0.2", | ||
| "@rollup/plugin-inject": "^5.0.5", | ||
@@ -107,4 +104,4 @@ "@rollup/plugin-json": "^6.1.0", | ||
| "@rollup/plugin-replace": "^6.0.3", | ||
| "@scalar/api-reference": "^1.43.8", | ||
| "@types/aws-lambda": "^8.10.160", | ||
| "@scalar/api-reference": "^1.48.2", | ||
| "@types/aws-lambda": "^8.10.161", | ||
| "@types/estree": "^1.0.8", | ||
@@ -114,16 +111,17 @@ "@types/etag": "^1.8.4", | ||
| "@types/http-proxy": "^1.17.17", | ||
| "@types/node": "^25.0.9", | ||
| "@types/node": "^25.4.0", | ||
| "@types/node-fetch": "^2.6.13", | ||
| "@types/semver": "^7.7.1", | ||
| "@types/xml2js": "^0.4.14", | ||
| "@vitest/coverage-v8": "^4.0.17", | ||
| "automd": "^0.4.2", | ||
| "c12": "^3.3.3", | ||
| "@typescript/native-preview": "7.0.0-dev.20260310.1", | ||
| "@vitest/coverage-v8": "^4.0.18", | ||
| "automd": "^0.4.3", | ||
| "c12": "^4.0.0-beta.3", | ||
| "changelogen": "^0.6.2", | ||
| "chokidar": "^5.0.0", | ||
| "citty": "^0.2.0", | ||
| "citty": "^0.2.1", | ||
| "compatx": "^0.2.0", | ||
| "confbox": "^0.2.2", | ||
| "confbox": "^0.2.4", | ||
| "cookie-es": "^2.0.0", | ||
| "croner": "^9.1.0", | ||
| "croner": "^10.0.1", | ||
| "defu": "^6.1.4", | ||
@@ -134,4 +132,2 @@ "destr": "^2.0.5", | ||
| "escape-string-regexp": "^5.0.0", | ||
| "eslint": "^9.39.2", | ||
| "eslint-config-unjs": "^0.6.2", | ||
| "etag": "^1.8.1", | ||
@@ -141,30 +137,31 @@ "execa": "^9.6.1", | ||
| "exsolve": "^1.0.8", | ||
| "fs-extra": "^11.3.3", | ||
| "fs-extra": "^11.3.4", | ||
| "get-port-please": "^3.2.0", | ||
| "giget": "^3.1.2", | ||
| "gzip-size": "^7.0.0", | ||
| "hookable": "^6.0.1", | ||
| "httpxy": "^0.1.7", | ||
| "httpxy": "^0.3.1", | ||
| "klona": "^2.0.6", | ||
| "knitwork": "^1.3.0", | ||
| "magic-string": "^0.30.21", | ||
| "mdzilla": "^0.0.5", | ||
| "mime": "^4.1.0", | ||
| "miniflare": "^4.20260114.0", | ||
| "mlly": "^1.8.0", | ||
| "nypm": "^0.6.4", | ||
| "obuild": "^0.4.18", | ||
| "miniflare": "^4.20260305.0", | ||
| "mlly": "^1.8.1", | ||
| "nypm": "^0.6.5", | ||
| "obuild": "^0.4.32", | ||
| "oxfmt": "^0.37.0", | ||
| "oxlint": "^1.52.0", | ||
| "pathe": "^2.0.3", | ||
| "perfect-debounce": "^2.0.0", | ||
| "perfect-debounce": "^2.1.0", | ||
| "pkg-types": "^2.3.0", | ||
| "prettier": "^3.8.0", | ||
| "pretty-bytes": "^7.1.0", | ||
| "react": "^19.2.3", | ||
| "react": "^19.2.4", | ||
| "rendu": "^0.0.7", | ||
| "rolldown": "1.0.0-beta.60", | ||
| "rollup": "^4.55.2", | ||
| "rou3": "^0.7.12", | ||
| "rollup": "^4.59.0", | ||
| "rou3": "^0.8.1", | ||
| "scule": "^1.3.0", | ||
| "semver": "^7.7.3", | ||
| "semver": "^7.7.4", | ||
| "serve-placeholder": "^2.0.2", | ||
| "source-map": "^0.7.6", | ||
| "std-env": "^3.10.0", | ||
| "std-env": "^4.0.0", | ||
| "tinyglobby": "^0.2.15", | ||
@@ -177,21 +174,25 @@ "tsconfck": "^3.1.6", | ||
| "unctx": "^2.5.0", | ||
| "unimport": "^5.6.0", | ||
| "unimport": "^6.0.1", | ||
| "untyped": "^2.0.0", | ||
| "unwasm": "^0.5.3", | ||
| "vite": "8.0.0-beta.8", | ||
| "vite": "^8.0.0-beta.18", | ||
| "vite7": "npm:vite@^7.3.1", | ||
| "vitest": "^4.0.17", | ||
| "wrangler": "~4.59.2", | ||
| "vitest": "^4.0.18", | ||
| "wrangler": "^4.71.0", | ||
| "xml2js": "^0.6.2", | ||
| "youch": "4.1.0-beta.13", | ||
| "youch-core": "^0.3.3" | ||
| "youch": "^4.1.0", | ||
| "youch-core": "^0.3.3", | ||
| "zephyr-agent": "^0.1.15" | ||
| }, | ||
| "peerDependencies": { | ||
| "rolldown": ">=1.0.0-beta.0", | ||
| "rollup": "^4", | ||
| "dotenv": "*", | ||
| "giget": "*", | ||
| "jiti": "^2.6.1", | ||
| "rollup": "^4.59.0", | ||
| "vite": "^7 || ^8 || >=8.0.0-0", | ||
| "xml2js": "^0.6.2" | ||
| "xml2js": "^0.6.2", | ||
| "zephyr-agent": "^0.1.15" | ||
| }, | ||
| "peerDependenciesMeta": { | ||
| "rolldown": { | ||
| "dotenv": { | ||
| "optional": true | ||
@@ -207,8 +208,22 @@ }, | ||
| "optional": true | ||
| }, | ||
| "giget": { | ||
| "optional": true | ||
| }, | ||
| "jiti": { | ||
| "optional": true | ||
| }, | ||
| "zephyr-agent": { | ||
| "optional": true | ||
| } | ||
| }, | ||
| "packageManager": "pnpm@10.28.0", | ||
| "resolutions": { | ||
| "nitro": "link:.", | ||
| "rolldown": "^1.0.0-rc.8", | ||
| "vite": "^8.0.0-beta.18" | ||
| }, | ||
| "engines": { | ||
| "node": "^20.19.0 || >=22.12.0" | ||
| }, | ||
| "packageManager": "pnpm@10.32.0", | ||
| "compatiblePackages": { | ||
@@ -215,0 +230,0 @@ "schemaVersion": 1, |
+2
-2
@@ -6,3 +6,3 @@ [](https://deepwiki.com/nitrojs/nitro) | ||
| > [!NOTE] | ||
| > You’re viewing the **v3 Alpha** branch. | ||
| > You’re viewing the **v3** branch. | ||
| > For the current stable release, see [Nitro v2](https://github.com/nitrojs/nitro/tree/v2). | ||
@@ -13,3 +13,3 @@ | ||
| 📘 **Docs (v3 Alpha):** [https://v3.nitro.build](https://v3.nitro.build) | ||
| 📘 **Docs:** [https://nitro.build](https://nitro.build) | ||
@@ -16,0 +16,0 @@ ## Contributing |
| import { B as T, V as a, at as join$1, ct as resolve$1, g as writeDevBuildInfo, j as src_default, rt as extname$1 } from "../_build/common.mjs"; | ||
| import { i as debounce } from "../_libs/rc9+c12+dotenv.mjs"; | ||
| import { t as createProxyServer } from "../_libs/httpxy.mjs"; | ||
| import { n as watch$1 } from "../_libs/readdirp+chokidar.mjs"; | ||
| import consola$1 from "consola"; | ||
| import { createReadStream, existsSync } from "node:fs"; | ||
| import { readFile, rm, stat as stat$1 } from "node:fs/promises"; | ||
| import { dirname, resolve } from "node:path"; | ||
| import { joinURL } from "ufo"; | ||
| import { createBrotliCompress, createGzip } from "node:zlib"; | ||
| import { Worker } from "node:worker_threads"; | ||
| import { H3, HTTPError, defineHandler, fromNodeHandler, getRequestIP, getRequestURL, serveStatic, toEventHandler } from "h3"; | ||
| import { Agent } from "undici"; | ||
| import { serve } from "srvx/node"; | ||
| import { ErrorParser } from "youch-core"; | ||
| import { Youch } from "youch"; | ||
| import { SourceMapConsumer } from "source-map"; | ||
| import { FastResponse } from "srvx"; | ||
| //#region src/runner/proxy.ts | ||
| function createHTTPProxy(defaults = {}) { | ||
| const proxy = createProxyServer(defaults); | ||
| proxy.on("proxyReq", (proxyReq, req) => { | ||
| if (!proxyReq.hasHeader("x-forwarded-for")) { | ||
| const address = req.socket.remoteAddress; | ||
| if (address) proxyReq.appendHeader("x-forwarded-for", address); | ||
| } | ||
| if (!proxyReq.hasHeader("x-forwarded-port")) { | ||
| if (req?.socket?.localPort) proxyReq.setHeader("x-forwarded-port", req.socket.localPort); | ||
| } | ||
| if (!proxyReq.hasHeader("x-forwarded-Proto")) { | ||
| const encrypted = (req?.connection)?.encrypted; | ||
| proxyReq.setHeader("x-forwarded-proto", encrypted ? "https" : "http"); | ||
| } | ||
| }); | ||
| return { | ||
| proxy, | ||
| async handleEvent(event, opts) { | ||
| try { | ||
| return await fromNodeHandler((req, res) => proxy.web(req, res, opts))(event); | ||
| } catch (error) { | ||
| event.res.headers.set("refresh", "3"); | ||
| throw new HTTPError({ | ||
| status: 503, | ||
| message: "Dev server is unavailable.", | ||
| cause: error | ||
| }); | ||
| } | ||
| } | ||
| }; | ||
| } | ||
| async function fetchAddress(addr, input, inputInit) { | ||
| let url; | ||
| let init; | ||
| if (input instanceof Request) { | ||
| url = new URL(input.url); | ||
| init = { | ||
| method: input.method, | ||
| headers: input.headers, | ||
| body: input.body, | ||
| ...inputInit | ||
| }; | ||
| } else { | ||
| url = new URL(input); | ||
| init = inputInit; | ||
| } | ||
| init = { | ||
| duplex: "half", | ||
| redirect: "manual", | ||
| ...init | ||
| }; | ||
| let res; | ||
| if (addr.socketPath) { | ||
| url.protocol = "http:"; | ||
| res = await fetch(url, { | ||
| ...init, | ||
| ...fetchSocketOptions(addr.socketPath) | ||
| }); | ||
| } else { | ||
| const origin = `http://${addr.host}${addr.port ? `:${addr.port}` : ""}`; | ||
| const outURL = new URL(url.pathname + url.search, origin); | ||
| res = await fetch(outURL, init); | ||
| } | ||
| const headers = new Headers(res.headers); | ||
| headers.delete("transfer-encoding"); | ||
| return new Response(res.body, { | ||
| status: res.status, | ||
| statusText: res.statusText, | ||
| headers | ||
| }); | ||
| } | ||
| function fetchSocketOptions(socketPath) { | ||
| if ("Bun" in globalThis) return { unix: socketPath }; | ||
| if ("Deno" in globalThis) return { client: Deno.createHttpClient({ | ||
| transport: "unix", | ||
| path: socketPath | ||
| }) }; | ||
| return { dispatcher: new Agent({ connect: { socketPath } }) }; | ||
| } | ||
| //#endregion | ||
| //#region src/runner/node.ts | ||
| var NodeEnvRunner = class { | ||
| closed = false; | ||
| #name; | ||
| #entry; | ||
| #data; | ||
| #hooks; | ||
| #worker; | ||
| #address; | ||
| #proxy; | ||
| #messageListeners; | ||
| constructor(opts) { | ||
| this.#name = opts.name; | ||
| this.#entry = opts.entry; | ||
| this.#data = opts.data; | ||
| this.#hooks = opts.hooks || {}; | ||
| this.#proxy = createHTTPProxy(); | ||
| this.#messageListeners = /* @__PURE__ */ new Set(); | ||
| this.#initWorker(); | ||
| } | ||
| get ready() { | ||
| return Boolean(!this.closed && this.#address && this.#proxy && this.#worker); | ||
| } | ||
| async fetch(input, init) { | ||
| for (let i = 0; i < 5 && !(this.#address && this.#proxy); i++) await new Promise((r) => setTimeout(r, 100 * Math.pow(2, i))); | ||
| if (!(this.#address && this.#proxy)) return new Response("Node env runner worker is unavailable", { status: 503 }); | ||
| return fetchAddress(this.#address, input, init); | ||
| } | ||
| upgrade(req, socket, head) { | ||
| if (!this.ready) return; | ||
| return this.#proxy.proxy.ws(req, socket, { | ||
| target: this.#address, | ||
| xfwd: true | ||
| }, head).catch((error) => { | ||
| consola$1.error("WebSocket proxy error:", error); | ||
| }); | ||
| } | ||
| sendMessage(message) { | ||
| if (!this.#worker) throw new Error("Node env worker should be initialized before sending messages."); | ||
| this.#worker.postMessage(message); | ||
| } | ||
| onMessage(listener) { | ||
| this.#messageListeners.add(listener); | ||
| } | ||
| offMessage(listener) { | ||
| this.#messageListeners.delete(listener); | ||
| } | ||
| async close(cause) { | ||
| if (this.closed) return; | ||
| this.closed = true; | ||
| this.#hooks.onClose?.(this, cause); | ||
| this.#hooks = {}; | ||
| const onError = (error) => consola$1.error(error); | ||
| await this.#closeWorker().catch(onError); | ||
| await this.#closeProxy().catch(onError); | ||
| await this.#closeSocket().catch(onError); | ||
| } | ||
| [Symbol.for("nodejs.util.inspect.custom")]() { | ||
| const status = this.closed ? "closed" : this.ready ? "ready" : "pending"; | ||
| return `NodeEnvRunner#${this.#name}(${status})`; | ||
| } | ||
| #initWorker() { | ||
| if (!existsSync(this.#entry)) { | ||
| this.close(`worker entry not found in "${this.#entry}".`); | ||
| return; | ||
| } | ||
| const worker = new Worker(this.#entry, { | ||
| env: { ...process.env }, | ||
| workerData: { | ||
| name: this.#name, | ||
| ...this.#data | ||
| } | ||
| }); | ||
| worker.once("exit", (code) => { | ||
| worker._exitCode = code; | ||
| this.close(`worker exited with code ${code}`); | ||
| }); | ||
| worker.once("error", (error) => { | ||
| consola$1.error(`Worker error:`, error); | ||
| this.close(error); | ||
| }); | ||
| worker.on("message", (message) => { | ||
| if (message?.address) { | ||
| this.#address = message.address; | ||
| this.#hooks.onReady?.(this, this.#address); | ||
| } | ||
| for (const listener of this.#messageListeners) listener(message); | ||
| }); | ||
| this.#worker = worker; | ||
| } | ||
| async #closeProxy() { | ||
| this.#proxy?.proxy?.close(() => {}); | ||
| this.#proxy = void 0; | ||
| } | ||
| async #closeSocket() { | ||
| const socketPath = this.#address?.socketPath; | ||
| if (socketPath && socketPath[0] !== "\0" && !socketPath.startsWith(String.raw`\\.\pipe`)) await rm(socketPath).catch(() => {}); | ||
| this.#address = void 0; | ||
| } | ||
| async #closeWorker() { | ||
| if (!this.#worker) return; | ||
| this.#worker.postMessage({ event: "shutdown" }); | ||
| if (!this.#worker._exitCode && !a && !T) await new Promise((resolve$2) => { | ||
| const gracefulShutdownTimeoutMs = Number.parseInt(process.env.NITRO_SHUTDOWN_TIMEOUT || "", 10) || 5e3; | ||
| const timeout = setTimeout(() => { | ||
| consola$1.warn(`force closing node env runner worker...`); | ||
| resolve$2(); | ||
| }, gracefulShutdownTimeoutMs); | ||
| this.#worker?.on("message", (message) => { | ||
| if (message.event === "exit") { | ||
| clearTimeout(timeout); | ||
| resolve$2(); | ||
| } | ||
| }); | ||
| }); | ||
| this.#worker.removeAllListeners(); | ||
| await this.#worker.terminate().catch((error) => { | ||
| consola$1.error(error); | ||
| }); | ||
| this.#worker = void 0; | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region src/dev/vfs.ts | ||
| function createVFSHandler(nitro) { | ||
| return defineHandler(async (event) => { | ||
| const { socket } = event.runtime?.node?.req || {}; | ||
| const ip = getRequestIP(event, { xForwardedFor: !socket?.remoteAddress && !socket?.localAddress && Object.keys(socket?.address?.() || {}).length === 0 && socket?.readable && socket?.writable && !socket?.remotePort }); | ||
| if (!(ip && /^::1$|^127\.\d+\.\d+\.\d+$/.test(ip))) throw new HTTPError({ | ||
| statusText: `Forbidden IP: "${ip || "?"}"`, | ||
| status: 403 | ||
| }); | ||
| const url = event.context.params?._ || ""; | ||
| const isJson = url.endsWith(".json") || event.req.headers.get("accept")?.includes("application/json"); | ||
| const id = decodeURIComponent(url.replace(/^(\.json)?\/?/, "") || ""); | ||
| if (id && !nitro.vfs.has(id)) throw new HTTPError({ | ||
| message: "File not found", | ||
| status: 404 | ||
| }); | ||
| const content = id ? await nitro.vfs.get(id)?.render() : void 0; | ||
| if (isJson) return { | ||
| rootDir: nitro.options.rootDir, | ||
| entries: [...nitro.vfs.keys()].map((id$1) => ({ | ||
| id: id$1, | ||
| path: "/_vfs.json/" + encodeURIComponent(id$1) | ||
| })), | ||
| current: id ? { | ||
| id, | ||
| content | ||
| } : null | ||
| }; | ||
| const directories = { [nitro.options.rootDir]: {} }; | ||
| const fpaths = [...nitro.vfs.keys()]; | ||
| for (const item of fpaths) { | ||
| const segments = item.replace(nitro.options.rootDir, "").split("/").filter(Boolean); | ||
| let currentDir = item.startsWith(nitro.options.rootDir) ? directories[nitro.options.rootDir] : directories; | ||
| for (const segment of segments) { | ||
| if (!currentDir[segment]) currentDir[segment] = {}; | ||
| currentDir = currentDir[segment]; | ||
| } | ||
| } | ||
| const generateHTML = (directory, path$1 = []) => Object.entries(directory).map(([fname, value = {}]) => { | ||
| const subpath = [...path$1, fname]; | ||
| const key = subpath.join("/"); | ||
| const encodedUrl = encodeURIComponent(key); | ||
| const linkClass = url === `/${encodedUrl}` ? "bg-gray-700 text-white" : "hover:bg-gray-800 text-gray-200"; | ||
| return Object.keys(value).length === 0 ? ` | ||
| <li class="flex flex-nowrap"> | ||
| <a href="/_vfs/${encodedUrl}" class="w-full text-sm px-2 py-1 border-b border-gray-10 ${linkClass}"> | ||
| ${fname} | ||
| </a> | ||
| </li> | ||
| ` : ` | ||
| <li> | ||
| <details ${url.startsWith(`/${encodedUrl}`) ? "open" : ""}> | ||
| <summary class="w-full text-sm px-2 py-1 border-b border-gray-10 hover:bg-gray-800 text-gray-200"> | ||
| ${fname} | ||
| </summary> | ||
| <ul class="ml-4"> | ||
| ${generateHTML(value, subpath)} | ||
| </ul> | ||
| </details> | ||
| </li> | ||
| `; | ||
| }).join(""); | ||
| const rootDirectory = directories[nitro.options.rootDir]; | ||
| delete directories[nitro.options.rootDir]; | ||
| const files = ` | ||
| <div class="h-full overflow-auto border-r border-gray:10"> | ||
| <p class="text-white text-bold text-center py-1 opacity-50">Virtual Files</p> | ||
| <ul class="flex flex-col">${generateHTML(rootDirectory, [nitro.options.rootDir]) + generateHTML(directories)}</ul> | ||
| </div> | ||
| `; | ||
| const file = id ? editorTemplate({ | ||
| readOnly: true, | ||
| language: id.endsWith("html") ? "html" : "javascript", | ||
| theme: "vs-dark", | ||
| value: content, | ||
| wordWrap: "wordWrapColumn", | ||
| wordWrapColumn: 80 | ||
| }) : ` | ||
| <div class="w-full h-full flex opacity-50"> | ||
| <h1 class="text-white m-auto">Select a virtual file to inspect</h1> | ||
| </div> | ||
| `; | ||
| event.res.headers.set("Content-Type", "text/html; charset=utf-8"); | ||
| return ` | ||
| <!doctype html> | ||
| <html> | ||
| <head> | ||
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@unocss/reset/tailwind.min.css" /> | ||
| <link rel="stylesheet" data-name="vs/editor/editor.main" href="${vsUrl}/editor/editor.main.min.css"> | ||
| <script src="https://cdn.jsdelivr.net/npm/@unocss/runtime"><\/script> | ||
| <style> | ||
| html { | ||
| background: #1E1E1E; | ||
| color: white; | ||
| } | ||
| [un-cloak] { | ||
| display: none; | ||
| } | ||
| </style> | ||
| </head> | ||
| <body class="bg-[#1E1E1E]"> | ||
| <div un-cloak class="h-screen grid grid-cols-[300px_1fr]"> | ||
| ${files} | ||
| ${file} | ||
| </div> | ||
| </body> | ||
| </html>`; | ||
| }); | ||
| } | ||
| const monacoUrl = `https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.30.0/min`; | ||
| const vsUrl = `${monacoUrl}/vs`; | ||
| const editorTemplate = (options) => ` | ||
| <div id="editor" class="min-h-screen w-full h-full"></div> | ||
| <script src="${vsUrl}/loader.min.js"><\/script> | ||
| <script> | ||
| require.config({ paths: { vs: '${vsUrl}' } }) | ||
| const proxy = URL.createObjectURL(new Blob([\` | ||
| self.MonacoEnvironment = { baseUrl: '${monacoUrl}' } | ||
| importScripts('${vsUrl}/base/worker/workerMain.min.js') | ||
| \`], { type: 'text/javascript' })) | ||
| window.MonacoEnvironment = { getWorkerUrl: () => proxy } | ||
| setTimeout(() => { | ||
| require(['vs/editor/editor.main'], function () { | ||
| monaco.editor.create(document.getElementById('editor'), ${JSON.stringify(options)}) | ||
| }) | ||
| }, 0); | ||
| <\/script> | ||
| `; | ||
| //#endregion | ||
| //#region src/runtime/internal/error/utils.ts | ||
| function defineNitroErrorHandler(handler) { | ||
| return handler; | ||
| } | ||
| //#endregion | ||
| //#region src/runtime/internal/error/dev.ts | ||
| var dev_default = defineNitroErrorHandler(async function defaultNitroErrorHandler(error, event) { | ||
| const res = await defaultHandler(error, event); | ||
| return new FastResponse(typeof res.body === "string" ? res.body : JSON.stringify(res.body, null, 2), res); | ||
| }); | ||
| async function defaultHandler(error, event, opts) { | ||
| const isSensitive = error.unhandled; | ||
| const status = error.status || 500; | ||
| const url = getRequestURL(event, { | ||
| xForwardedHost: true, | ||
| xForwardedProto: true | ||
| }); | ||
| if (status === 404) { | ||
| const baseURL = import.meta.baseURL || "/"; | ||
| if (/^\/[^/]/.test(baseURL) && !url.pathname.startsWith(baseURL)) return { | ||
| status: 302, | ||
| statusText: "Found", | ||
| headers: { location: `${baseURL}${url.pathname.slice(1)}${url.search}` }, | ||
| body: `Redirecting...` | ||
| }; | ||
| } | ||
| await loadStackTrace(error).catch(consola$1.error); | ||
| const youch = new Youch(); | ||
| if (isSensitive && !opts?.silent) { | ||
| const tags = [error.unhandled && "[unhandled]"].filter(Boolean).join(" "); | ||
| const ansiError = await (await youch.toANSI(error)).replaceAll(process.cwd(), "."); | ||
| consola$1.error(`[request error] ${tags} [${event.req.method}] ${url}\n\n`, ansiError); | ||
| } | ||
| const useJSON = opts?.json ?? !event.req.headers.get("accept")?.includes("text/html"); | ||
| const headers = { | ||
| "content-type": useJSON ? "application/json" : "text/html", | ||
| "x-content-type-options": "nosniff", | ||
| "x-frame-options": "DENY", | ||
| "referrer-policy": "no-referrer", | ||
| "content-security-policy": "script-src 'self' 'unsafe-inline'; object-src 'none'; base-uri 'self';" | ||
| }; | ||
| if (status === 404 || !event.res.headers.has("cache-control")) headers["cache-control"] = "no-cache"; | ||
| const body = useJSON ? { | ||
| error: true, | ||
| url, | ||
| status, | ||
| statusText: error.statusText, | ||
| message: error.message, | ||
| data: error.data, | ||
| stack: error.stack?.split("\n").map((line) => line.trim()) | ||
| } : await youch.toHTML(error, { request: { | ||
| url: url.href, | ||
| method: event.req.method, | ||
| headers: Object.fromEntries(event.req.headers.entries()) | ||
| } }); | ||
| return { | ||
| status, | ||
| statusText: error.statusText, | ||
| headers, | ||
| body | ||
| }; | ||
| } | ||
| async function loadStackTrace(error) { | ||
| if (!(error instanceof Error)) return; | ||
| const parsed = await new ErrorParser().defineSourceLoader(sourceLoader).parse(error); | ||
| const stack = error.message + "\n" + parsed.frames.map((frame) => fmtFrame(frame)).join("\n"); | ||
| Object.defineProperty(error, "stack", { value: stack }); | ||
| if (error.cause) await loadStackTrace(error.cause).catch(consola$1.error); | ||
| } | ||
| async function sourceLoader(frame) { | ||
| if (!frame.fileName || frame.fileType !== "fs" || frame.type === "native") return; | ||
| if (frame.type === "app") { | ||
| const rawSourceMap = await readFile(`${frame.fileName}.map`, "utf8").catch(() => {}); | ||
| if (rawSourceMap) { | ||
| const originalPosition = (await new SourceMapConsumer(rawSourceMap)).originalPositionFor({ | ||
| line: frame.lineNumber, | ||
| column: frame.columnNumber | ||
| }); | ||
| if (originalPosition.source && originalPosition.line) { | ||
| frame.fileName = resolve(dirname(frame.fileName), originalPosition.source); | ||
| frame.lineNumber = originalPosition.line; | ||
| frame.columnNumber = originalPosition.column || 0; | ||
| } | ||
| } | ||
| } | ||
| const contents = await readFile(frame.fileName, "utf8").catch(() => {}); | ||
| return contents ? { contents } : void 0; | ||
| } | ||
| function fmtFrame(frame) { | ||
| if (frame.type === "native") return frame.raw; | ||
| const src = `${frame.fileName || ""}:${frame.lineNumber}:${frame.columnNumber})`; | ||
| return frame.functionName ? `at ${frame.functionName} (${src}` : `at ${src}`; | ||
| } | ||
| //#endregion | ||
| //#region src/dev/app.ts | ||
| var NitroDevApp = class { | ||
| nitro; | ||
| fetch; | ||
| constructor(nitro, catchAllHandler) { | ||
| this.nitro = nitro; | ||
| const app = this.#createApp(catchAllHandler); | ||
| this.fetch = app.fetch.bind(app); | ||
| } | ||
| #createApp(catchAllHandler) { | ||
| const app = new H3({ | ||
| debug: true, | ||
| onError: async (error, event) => { | ||
| const errorHandler = this.nitro.options.devErrorHandler || dev_default; | ||
| await loadStackTrace(error).catch(() => {}); | ||
| return errorHandler(error, event, { defaultHandler }); | ||
| } | ||
| }); | ||
| for (const h of this.nitro.options.devHandlers) { | ||
| const handler = toEventHandler(h.handler); | ||
| if (!handler) { | ||
| this.nitro.logger.warn("Invalid dev handler:", h); | ||
| continue; | ||
| } | ||
| if (h.middleware || !h.route) if (h.route) app.use(h.route, handler, { method: h.method }); | ||
| else app.use(handler, { method: h.method }); | ||
| else app.on(h.method || "", h.route, handler, { meta: h.meta }); | ||
| } | ||
| app.get("/_vfs/**", createVFSHandler(this.nitro)); | ||
| for (const asset of this.nitro.options.publicAssets) { | ||
| const assetBase = joinURL(this.nitro.options.baseURL, asset.baseURL || "/"); | ||
| app.use(joinURL(assetBase, "**"), (event) => serveStaticDir(event, { | ||
| dir: asset.dir, | ||
| base: assetBase, | ||
| fallthrough: asset.fallthrough | ||
| })); | ||
| } | ||
| const routes = Object.keys(this.nitro.options.devProxy).sort().reverse(); | ||
| for (const route of routes) { | ||
| let opts = this.nitro.options.devProxy[route]; | ||
| if (typeof opts === "string") opts = { target: opts }; | ||
| const proxy = createHTTPProxy(opts); | ||
| app.all(route, proxy.handleEvent); | ||
| } | ||
| if (catchAllHandler) app.all("/**", catchAllHandler); | ||
| return app; | ||
| } | ||
| }; | ||
| function serveStaticDir(event, opts) { | ||
| const dir = resolve$1(opts.dir) + "/"; | ||
| const r = (id) => { | ||
| if (!id.startsWith(opts.base) || !extname$1(id)) return; | ||
| const resolved = join$1(dir, id.slice(opts.base.length)); | ||
| if (resolved.startsWith(dir)) return resolved; | ||
| }; | ||
| return serveStatic(event, { | ||
| fallthrough: opts.fallthrough, | ||
| getMeta: async (id) => { | ||
| const path$1 = r(id); | ||
| if (!path$1) return; | ||
| const s = await stat$1(path$1).catch(() => null); | ||
| if (!s?.isFile()) return; | ||
| const ext = extname$1(path$1); | ||
| return { | ||
| size: s.size, | ||
| mtime: s.mtime, | ||
| type: src_default.getType(ext) || "application/octet-stream" | ||
| }; | ||
| }, | ||
| getContents(id) { | ||
| const path$1 = r(id); | ||
| if (!path$1) return; | ||
| const stream = createReadStream(path$1); | ||
| const acceptEncoding = event.req.headers.get("accept-encoding") || ""; | ||
| if (acceptEncoding.includes("br")) { | ||
| event.res.headers.set("Content-Encoding", "br"); | ||
| event.res.headers.delete("Content-Length"); | ||
| event.res.headers.set("Vary", "Accept-Encoding"); | ||
| return stream.pipe(createBrotliCompress()); | ||
| } else if (acceptEncoding.includes("gzip")) { | ||
| event.res.headers.set("Content-Encoding", "gzip"); | ||
| event.res.headers.delete("Content-Length"); | ||
| event.res.headers.set("Vary", "Accept-Encoding"); | ||
| return stream.pipe(createGzip()); | ||
| } | ||
| return stream; | ||
| } | ||
| }); | ||
| } | ||
| //#endregion | ||
| //#region src/dev/server.ts | ||
| function createDevServer(nitro) { | ||
| return new NitroDevServer(nitro); | ||
| } | ||
| var NitroDevServer = class NitroDevServer extends NitroDevApp { | ||
| #entry; | ||
| #workerData = {}; | ||
| #listeners = []; | ||
| #watcher; | ||
| #workers = []; | ||
| #workerIdCtr = 0; | ||
| #workerError; | ||
| #building = true; | ||
| #buildError; | ||
| #messageListeners = /* @__PURE__ */ new Set(); | ||
| constructor(nitro) { | ||
| super(nitro, async (event) => { | ||
| const worker = await this.#getWorker(); | ||
| if (!worker) return this.#generateError(); | ||
| return worker.fetch(event.req); | ||
| }); | ||
| for (const key of Object.getOwnPropertyNames(NitroDevServer.prototype)) { | ||
| const value = this[key]; | ||
| if (typeof value === "function" && key !== "constructor") this[key] = value.bind(this); | ||
| } | ||
| nitro.fetch = this.fetch.bind(this); | ||
| this.#entry = resolve$1(nitro.options.output.dir, nitro.options.output.serverDir, "index.mjs"); | ||
| nitro.hooks.hook("close", () => this.close()); | ||
| nitro.hooks.hook("dev:start", () => { | ||
| this.#building = true; | ||
| this.#buildError = void 0; | ||
| }); | ||
| nitro.hooks.hook("dev:reload", (payload) => { | ||
| this.#buildError = void 0; | ||
| this.#building = false; | ||
| if (payload?.entry) this.#entry = payload.entry; | ||
| if (payload?.workerData) this.#workerData = payload.workerData; | ||
| this.reload(); | ||
| }); | ||
| nitro.hooks.hook("dev:error", (cause) => { | ||
| this.#buildError = cause; | ||
| this.#building = false; | ||
| for (const worker of this.#workers) worker.close(); | ||
| }); | ||
| const devWatch = nitro.options.devServer.watch; | ||
| if (devWatch && devWatch.length > 0) { | ||
| const debouncedReload = debounce(() => this.reload()); | ||
| this.#watcher = watch$1(devWatch, nitro.options.watchOptions); | ||
| this.#watcher.on("add", debouncedReload).on("change", debouncedReload); | ||
| } | ||
| } | ||
| async upgrade(req, socket, head) { | ||
| const worker = await this.#getWorker(); | ||
| if (!worker) throw new HTTPError({ | ||
| status: 503, | ||
| statusText: "No worker available." | ||
| }); | ||
| if (!worker.upgrade) throw new HTTPError({ | ||
| status: 501, | ||
| statusText: "Worker does not support upgrades." | ||
| }); | ||
| return worker.upgrade(req, socket, head); | ||
| } | ||
| listen(opts) { | ||
| const server = serve({ | ||
| ...opts, | ||
| fetch: this.fetch, | ||
| gracefulShutdown: false | ||
| }); | ||
| this.#listeners.push(server); | ||
| if (server.node?.server) server.node.server.on("upgrade", (req, sock, head) => this.upgrade(req, sock, head)); | ||
| return server; | ||
| } | ||
| async close() { | ||
| await Promise.all([ | ||
| Promise.all(this.#listeners.map((l) => l.close())).then(() => { | ||
| this.#listeners = []; | ||
| }), | ||
| Promise.all(this.#workers.map((w) => w.close())).then(() => { | ||
| this.#workers = []; | ||
| }), | ||
| Promise.resolve(this.#watcher?.close()).then(() => { | ||
| this.#watcher = void 0; | ||
| }) | ||
| ].map((p) => p.catch((error) => { | ||
| consola$1.error(error); | ||
| }))); | ||
| } | ||
| reload() { | ||
| for (const worker$1 of this.#workers) worker$1.close(); | ||
| const worker = new NodeEnvRunner({ | ||
| name: `Nitro_${this.#workerIdCtr++}`, | ||
| entry: this.#entry, | ||
| data: this.#workerData, | ||
| hooks: { | ||
| onClose: (worker$1, cause) => { | ||
| this.#workerError = cause; | ||
| const index = this.#workers.indexOf(worker$1); | ||
| if (index !== -1) this.#workers.splice(index, 1); | ||
| }, | ||
| onReady: async (_worker, addr) => { | ||
| writeDevBuildInfo(this.nitro, addr).catch(() => {}); | ||
| } | ||
| } | ||
| }); | ||
| if (!worker.closed) { | ||
| for (const listener of this.#messageListeners) worker.onMessage(listener); | ||
| this.#workers.unshift(worker); | ||
| } | ||
| } | ||
| sendMessage(message) { | ||
| for (const worker of this.#workers) if (!worker.closed) worker.sendMessage(message); | ||
| } | ||
| onMessage(listener) { | ||
| this.#messageListeners.add(listener); | ||
| for (const worker of this.#workers) worker.onMessage(listener); | ||
| } | ||
| offMessage(listener) { | ||
| this.#messageListeners.delete(listener); | ||
| for (const worker of this.#workers) worker.offMessage(listener); | ||
| } | ||
| async #getWorker() { | ||
| let retry = 0; | ||
| const maxRetries = a || T ? 100 : 10; | ||
| while (this.#building || ++retry < maxRetries) { | ||
| if ((this.#workers.length === 0 || this.#buildError) && !this.#building) return; | ||
| const activeWorker = this.#workers.find((w) => w.ready); | ||
| if (activeWorker) return activeWorker; | ||
| await new Promise((resolve$2) => setTimeout(resolve$2, 600)); | ||
| } | ||
| } | ||
| #generateError() { | ||
| const error = this.#buildError || this.#workerError; | ||
| if (error) { | ||
| try { | ||
| error.unhandled = false; | ||
| let id = error.id || error.path; | ||
| if (id) { | ||
| const cause = error.errors?.[0]; | ||
| const loc = error.location || error.loc || cause?.location || cause?.loc; | ||
| if (loc) id += `:${loc.line}:${loc.column}`; | ||
| error.stack = (error.stack || "").replace(/(^\s*at\s+.+)/m, ` at ${id}\n$1`); | ||
| } | ||
| } catch {} | ||
| return new HTTPError(error); | ||
| } | ||
| return new Response(JSON.stringify({ | ||
| error: "Dev server is unavailable.", | ||
| hint: "Please reload the page and check the console for errors if the issue persists." | ||
| }, null, 2), { | ||
| status: 503, | ||
| statusText: "Dev server is unavailable", | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| "Cache-Control": "no-store", | ||
| Refresh: "3" | ||
| } | ||
| }); | ||
| } | ||
| }; | ||
| //#endregion | ||
| export { NodeEnvRunner as i, createDevServer as n, NitroDevApp as r, NitroDevServer as t }; |
| import { Stats } from "node:fs"; | ||
| import { Jiti, JitiOptions } from "jiti"; | ||
| import { EventEmitter } from "node:events"; | ||
| import { Readable } from "node:stream"; | ||
| import { diff } from "ohash/utils"; | ||
| //#region node_modules/.pnpm/chokidar@5.0.0/node_modules/chokidar/index.d.ts | ||
| type AWF = { | ||
| stabilityThreshold: number; | ||
| pollInterval: number; | ||
| }; | ||
| type BasicOpts = { | ||
| persistent: boolean; | ||
| ignoreInitial: boolean; | ||
| followSymlinks: boolean; | ||
| cwd?: string; | ||
| usePolling: boolean; | ||
| interval: number; | ||
| binaryInterval: number; | ||
| alwaysStat?: boolean; | ||
| depth?: number; | ||
| ignorePermissionErrors: boolean; | ||
| atomic: boolean | number; | ||
| }; | ||
| type ChokidarOptions = Partial<BasicOpts & { | ||
| ignored: Matcher | Matcher[]; | ||
| awaitWriteFinish: boolean | Partial<AWF>; | ||
| }>; | ||
| type MatchFunction = (val: string, stats?: Stats) => boolean; | ||
| interface MatcherObject { | ||
| path: string; | ||
| recursive?: boolean; | ||
| } | ||
| type Matcher = string | RegExp | MatchFunction | MatcherObject; | ||
| //#endregion | ||
| //#region node_modules/.pnpm/giget@2.0.0/node_modules/giget/dist/index.d.mts | ||
| interface TemplateInfo { | ||
| name: string; | ||
| tar: string; | ||
| version?: string; | ||
| subdir?: string; | ||
| url?: string; | ||
| defaultDir?: string; | ||
| headers?: Record<string, string | undefined>; | ||
| source?: never; | ||
| dir?: never; | ||
| [key: string]: any; | ||
| } | ||
| type TemplateProvider = (input: string, options: { | ||
| auth?: string; | ||
| }) => TemplateInfo | Promise<TemplateInfo> | null; | ||
| interface DownloadTemplateOptions { | ||
| provider?: string; | ||
| force?: boolean; | ||
| forceClean?: boolean; | ||
| offline?: boolean; | ||
| preferOffline?: boolean; | ||
| providers?: Record<string, TemplateProvider>; | ||
| dir?: string; | ||
| registry?: false | string; | ||
| cwd?: string; | ||
| auth?: string; | ||
| install?: boolean; | ||
| silent?: boolean; | ||
| } | ||
| //#endregion | ||
| //#region node_modules/.pnpm/c12@3.3.3_magicast@0.5.1/node_modules/c12/dist/index.d.mts | ||
| //#region src/dotenv.d.ts | ||
| interface DotenvOptions { | ||
| /** | ||
| * The project root directory (either absolute or relative to the current working directory). | ||
| * | ||
| * Defaults to `options.cwd` in `loadConfig` context, or `process.cwd()` when used as standalone. | ||
| */ | ||
| cwd?: string; | ||
| /** | ||
| * What file or files to look in for environment variables (either absolute or relative | ||
| * to the current working directory). For example, `.env`. | ||
| * With the array type, the order enforce the env loading priority (last one overrides). | ||
| */ | ||
| fileName?: string | string[]; | ||
| /** | ||
| * Whether to interpolate variables within .env. | ||
| * | ||
| * @example | ||
| * ```env | ||
| * BASE_DIR="/test" | ||
| * # resolves to "/test/further" | ||
| * ANOTHER_DIR="${BASE_DIR}/further" | ||
| * ``` | ||
| */ | ||
| interpolate?: boolean; | ||
| /** | ||
| * An object describing environment variables (key, value pairs). | ||
| */ | ||
| env?: NodeJS.ProcessEnv; | ||
| } | ||
| declare global { | ||
| var __c12_dotenv_vars__: Map<Record<string, any>, Set<string>>; | ||
| } //#endregion | ||
| //#region src/types.d.ts | ||
| interface ConfigLayerMeta { | ||
| name?: string; | ||
| [key: string]: any; | ||
| } | ||
| type UserInputConfig = Record<string, any>; | ||
| interface C12InputConfig<T extends UserInputConfig = UserInputConfig, MT extends ConfigLayerMeta = ConfigLayerMeta> { | ||
| $test?: T; | ||
| $development?: T; | ||
| $production?: T; | ||
| $env?: Record<string, T>; | ||
| $meta?: MT; | ||
| } | ||
| interface SourceOptions<T extends UserInputConfig = UserInputConfig, MT extends ConfigLayerMeta = ConfigLayerMeta> { | ||
| /** Custom meta for layer */ | ||
| meta?: MT; | ||
| /** Layer config overrides */ | ||
| overrides?: T; | ||
| [key: string]: any; | ||
| /** | ||
| * Options for cloning remote sources | ||
| * | ||
| * @see https://giget.unjs.io | ||
| */ | ||
| giget?: DownloadTemplateOptions; | ||
| /** | ||
| * Install dependencies after cloning | ||
| * | ||
| * @see https://nypm.unjs.io | ||
| */ | ||
| install?: boolean; | ||
| /** | ||
| * Token for cloning private sources | ||
| * | ||
| * @see https://giget.unjs.io#providing-token-for-private-repositories | ||
| */ | ||
| auth?: string; | ||
| } | ||
| interface ConfigLayer<T extends UserInputConfig = UserInputConfig, MT extends ConfigLayerMeta = ConfigLayerMeta> { | ||
| config: T | null; | ||
| source?: string; | ||
| sourceOptions?: SourceOptions<T, MT>; | ||
| meta?: MT; | ||
| cwd?: string; | ||
| configFile?: string; | ||
| } | ||
| interface ResolvedConfig<T extends UserInputConfig = UserInputConfig, MT extends ConfigLayerMeta = ConfigLayerMeta> extends ConfigLayer<T, MT> { | ||
| config: T; | ||
| layers?: ConfigLayer<T, MT>[]; | ||
| cwd?: string; | ||
| _configFile?: string; | ||
| } | ||
| type ConfigSource = "overrides" | "main" | "rc" | "packageJson" | "defaultConfig"; | ||
| interface ConfigFunctionContext { | ||
| [key: string]: any; | ||
| } | ||
| interface ResolvableConfigContext<T extends UserInputConfig = UserInputConfig> { | ||
| configs: Record<ConfigSource, T | null | undefined>; | ||
| rawConfigs: Record<ConfigSource, ResolvableConfig<T> | null | undefined>; | ||
| } | ||
| type MaybePromise<T> = T | Promise<T>; | ||
| type ResolvableConfig<T extends UserInputConfig = UserInputConfig> = MaybePromise<T | null | undefined> | ((ctx: ResolvableConfigContext<T>) => MaybePromise<T | null | undefined>); | ||
| interface LoadConfigOptions<T extends UserInputConfig = UserInputConfig, MT extends ConfigLayerMeta = ConfigLayerMeta> { | ||
| name?: string; | ||
| cwd?: string; | ||
| configFile?: string; | ||
| rcFile?: false | string; | ||
| globalRc?: boolean; | ||
| dotenv?: boolean | DotenvOptions; | ||
| envName?: string | false; | ||
| packageJson?: boolean | string | string[]; | ||
| defaults?: T; | ||
| defaultConfig?: ResolvableConfig<T>; | ||
| overrides?: ResolvableConfig<T>; | ||
| omit$Keys?: boolean; | ||
| /** Context passed to config functions */ | ||
| context?: ConfigFunctionContext; | ||
| resolve?: (id: string, options: LoadConfigOptions<T, MT>) => null | undefined | ResolvedConfig<T, MT> | Promise<ResolvedConfig<T, MT> | undefined | null>; | ||
| jiti?: Jiti; | ||
| jitiOptions?: JitiOptions; | ||
| giget?: false | DownloadTemplateOptions; | ||
| merger?: (...sources: Array<T | null | undefined>) => T; | ||
| extend?: false | { | ||
| extendKey?: string | string[]; | ||
| }; | ||
| configFileRequired?: boolean; | ||
| } | ||
| //#endregion | ||
| //#region src/watch.d.ts | ||
| type DiffEntries = ReturnType<typeof diff>; | ||
| type ConfigWatcher<T extends UserInputConfig = UserInputConfig, MT extends ConfigLayerMeta = ConfigLayerMeta> = ResolvedConfig<T, MT> & { | ||
| watchingFiles: string[]; | ||
| unwatch: () => Promise<void>; | ||
| }; | ||
| interface WatchConfigOptions<T extends UserInputConfig = UserInputConfig, MT extends ConfigLayerMeta = ConfigLayerMeta> extends LoadConfigOptions<T, MT> { | ||
| chokidarOptions?: ChokidarOptions; | ||
| debounce?: false | number; | ||
| onWatch?: (event: { | ||
| type: "created" | "updated" | "removed"; | ||
| path: string; | ||
| }) => void | Promise<void>; | ||
| acceptHMR?: (context: { | ||
| getDiff: () => DiffEntries; | ||
| newConfig: ResolvedConfig<T, MT>; | ||
| oldConfig: ResolvedConfig<T, MT>; | ||
| }) => void | boolean | Promise<void | boolean>; | ||
| onUpdate?: (context: { | ||
| getDiff: () => ReturnType<typeof diff>; | ||
| newConfig: ResolvedConfig<T, MT>; | ||
| oldConfig: ResolvedConfig<T, MT>; | ||
| }) => void | Promise<void>; | ||
| } | ||
| //#endregion | ||
| export { WatchConfigOptions as a, ResolvedConfig as i, ConfigWatcher as n, ChokidarOptions as o, DotenvOptions as r, C12InputConfig as t }; |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
| import { Plugin } from "rollup"; | ||
| //#region node_modules/.pnpm/@rollup+pluginutils@5.3.0_rollup@4.55.3/node_modules/@rollup/pluginutils/types/index.d.ts | ||
| /** | ||
| * A valid `picomatch` glob pattern, or array of patterns. | ||
| */ | ||
| type FilterPattern = ReadonlyArray<string | RegExp> | string | RegExp | null; | ||
| //#endregion | ||
| //#region node_modules/.pnpm/@rollup+plugin-commonjs@29.0.0_rollup@4.55.3/node_modules/@rollup/plugin-commonjs/types/index.d.ts | ||
| type RequireReturnsDefaultOption = boolean | 'auto' | 'preferred' | 'namespace'; | ||
| type DefaultIsModuleExportsOption = boolean | 'auto'; | ||
| interface RollupCommonJSOptions { | ||
| /** | ||
| * A picomatch pattern, or array of patterns, which specifies the files in | ||
| * the build the plugin should operate on. By default, all files with | ||
| * extension `".cjs"` or those in `extensions` are included, but you can | ||
| * narrow this list by only including specific files. These files will be | ||
| * analyzed and transpiled if either the analysis does not find ES module | ||
| * specific statements or `transformMixedEsModules` is `true`. | ||
| * @default undefined | ||
| */ | ||
| include?: FilterPattern; | ||
| /** | ||
| * A picomatch pattern, or array of patterns, which specifies the files in | ||
| * the build the plugin should _ignore_. By default, all files with | ||
| * extensions other than those in `extensions` or `".cjs"` are ignored, but you | ||
| * can exclude additional files. See also the `include` option. | ||
| * @default undefined | ||
| */ | ||
| exclude?: FilterPattern; | ||
| /** | ||
| * For extensionless imports, search for extensions other than .js in the | ||
| * order specified. Note that you need to make sure that non-JavaScript files | ||
| * are transpiled by another plugin first. | ||
| * @default [ '.js' ] | ||
| */ | ||
| extensions?: ReadonlyArray<string>; | ||
| /** | ||
| * If true then uses of `global` won't be dealt with by this plugin | ||
| * @default false | ||
| */ | ||
| ignoreGlobal?: boolean; | ||
| /** | ||
| * If false, skips source map generation for CommonJS modules. This will | ||
| * improve performance. | ||
| * @default true | ||
| */ | ||
| sourceMap?: boolean; | ||
| /** | ||
| * Some `require` calls cannot be resolved statically to be translated to | ||
| * imports. | ||
| * When this option is set to `false`, the generated code will either | ||
| * directly throw an error when such a call is encountered or, when | ||
| * `dynamicRequireTargets` is used, when such a call cannot be resolved with a | ||
| * configured dynamic require target. | ||
| * Setting this option to `true` will instead leave the `require` call in the | ||
| * code or use it as a fallback for `dynamicRequireTargets`. | ||
| * @default false | ||
| */ | ||
| ignoreDynamicRequires?: boolean; | ||
| /** | ||
| * Instructs the plugin whether to enable mixed module transformations. This | ||
| * is useful in scenarios with modules that contain a mix of ES `import` | ||
| * statements and CommonJS `require` expressions. Set to `true` if `require` | ||
| * calls should be transformed to imports in mixed modules, or `false` if the | ||
| * `require` expressions should survive the transformation. The latter can be | ||
| * important if the code contains environment detection, or you are coding | ||
| * for an environment with special treatment for `require` calls such as | ||
| * ElectronJS. See also the `ignore` option. | ||
| * @default false | ||
| */ | ||
| transformMixedEsModules?: boolean; | ||
| /** | ||
| * By default, this plugin will try to hoist `require` statements as imports | ||
| * to the top of each file. While this works well for many code bases and | ||
| * allows for very efficient ESM output, it does not perfectly capture | ||
| * CommonJS semantics as the order of side effects like log statements may | ||
| * change. But it is especially problematic when there are circular `require` | ||
| * calls between CommonJS modules as those often rely on the lazy execution of | ||
| * nested `require` calls. | ||
| * | ||
| * Setting this option to `true` will wrap all CommonJS files in functions | ||
| * which are executed when they are required for the first time, preserving | ||
| * NodeJS semantics. Note that this can have an impact on the size and | ||
| * performance of the generated code. | ||
| * | ||
| * The default value of `"auto"` will only wrap CommonJS files when they are | ||
| * part of a CommonJS dependency cycle, e.g. an index file that is required by | ||
| * many of its dependencies. All other CommonJS files are hoisted. This is the | ||
| * recommended setting for most code bases. | ||
| * | ||
| * `false` will entirely prevent wrapping and hoist all files. This may still | ||
| * work depending on the nature of cyclic dependencies but will often cause | ||
| * problems. | ||
| * | ||
| * You can also provide a picomatch pattern, or array of patterns, to only | ||
| * specify a subset of files which should be wrapped in functions for proper | ||
| * `require` semantics. | ||
| * | ||
| * `"debug"` works like `"auto"` but after bundling, it will display a warning | ||
| * containing a list of ids that have been wrapped which can be used as | ||
| * picomatch pattern for fine-tuning. | ||
| * @default "auto" | ||
| */ | ||
| strictRequires?: boolean | FilterPattern; | ||
| /** | ||
| * Sometimes you have to leave require statements unconverted. Pass an array | ||
| * containing the IDs or a `id => boolean` function. | ||
| * @default [] | ||
| */ | ||
| ignore?: ReadonlyArray<string> | ((id: string) => boolean); | ||
| /** | ||
| * In most cases, where `require` calls are inside a `try-catch` clause, | ||
| * they should be left unconverted as it requires an optional dependency | ||
| * that may or may not be installed beside the rolled up package. | ||
| * Due to the conversion of `require` to a static `import` - the call is | ||
| * hoisted to the top of the file, outside the `try-catch` clause. | ||
| * | ||
| * - `true`: Default. All `require` calls inside a `try` will be left unconverted. | ||
| * - `false`: All `require` calls inside a `try` will be converted as if the | ||
| * `try-catch` clause is not there. | ||
| * - `remove`: Remove all `require` calls from inside any `try` block. | ||
| * - `string[]`: Pass an array containing the IDs to left unconverted. | ||
| * - `((id: string) => boolean|'remove')`: Pass a function that controls | ||
| * individual IDs. | ||
| * | ||
| * @default true | ||
| */ | ||
| ignoreTryCatch?: boolean | 'remove' | ReadonlyArray<string> | ((id: string) => boolean | 'remove'); | ||
| /** | ||
| * Controls how to render imports from external dependencies. By default, | ||
| * this plugin assumes that all external dependencies are CommonJS. This | ||
| * means they are rendered as default imports to be compatible with e.g. | ||
| * NodeJS where ES modules can only import a default export from a CommonJS | ||
| * dependency. | ||
| * | ||
| * If you set `esmExternals` to `true`, this plugin assumes that all | ||
| * external dependencies are ES modules and respect the | ||
| * `requireReturnsDefault` option. If that option is not set, they will be | ||
| * rendered as namespace imports. | ||
| * | ||
| * You can also supply an array of ids to be treated as ES modules, or a | ||
| * function that will be passed each external id to determine whether it is | ||
| * an ES module. | ||
| * @default false | ||
| */ | ||
| esmExternals?: boolean | ReadonlyArray<string> | ((id: string) => boolean); | ||
| /** | ||
| * Controls what is returned when requiring an ES module from a CommonJS file. | ||
| * When using the `esmExternals` option, this will also apply to external | ||
| * modules. By default, this plugin will render those imports as namespace | ||
| * imports i.e. | ||
| * | ||
| * ```js | ||
| * // input | ||
| * const foo = require('foo'); | ||
| * | ||
| * // output | ||
| * import * as foo from 'foo'; | ||
| * ``` | ||
| * | ||
| * However, there are some situations where this may not be desired. | ||
| * For these situations, you can change Rollup's behaviour either globally or | ||
| * per module. To change it globally, set the `requireReturnsDefault` option | ||
| * to one of the following values: | ||
| * | ||
| * - `false`: This is the default, requiring an ES module returns its | ||
| * namespace. This is the only option that will also add a marker | ||
| * `__esModule: true` to the namespace to support interop patterns in | ||
| * CommonJS modules that are transpiled ES modules. | ||
| * - `"namespace"`: Like `false`, requiring an ES module returns its | ||
| * namespace, but the plugin does not add the `__esModule` marker and thus | ||
| * creates more efficient code. For external dependencies when using | ||
| * `esmExternals: true`, no additional interop code is generated. | ||
| * - `"auto"`: This is complementary to how `output.exports: "auto"` works in | ||
| * Rollup: If a module has a default export and no named exports, requiring | ||
| * that module returns the default export. In all other cases, the namespace | ||
| * is returned. For external dependencies when using `esmExternals: true`, a | ||
| * corresponding interop helper is added. | ||
| * - `"preferred"`: If a module has a default export, requiring that module | ||
| * always returns the default export, no matter whether additional named | ||
| * exports exist. This is similar to how previous versions of this plugin | ||
| * worked. Again for external dependencies when using `esmExternals: true`, | ||
| * an interop helper is added. | ||
| * - `true`: This will always try to return the default export on require | ||
| * without checking if it actually exists. This can throw at build time if | ||
| * there is no default export. This is how external dependencies are handled | ||
| * when `esmExternals` is not used. The advantage over the other options is | ||
| * that, like `false`, this does not add an interop helper for external | ||
| * dependencies, keeping the code lean. | ||
| * | ||
| * To change this for individual modules, you can supply a function for | ||
| * `requireReturnsDefault` instead. This function will then be called once for | ||
| * each required ES module or external dependency with the corresponding id | ||
| * and allows you to return different values for different modules. | ||
| * @default false | ||
| */ | ||
| requireReturnsDefault?: RequireReturnsDefaultOption | ((id: string) => RequireReturnsDefaultOption); | ||
| /** | ||
| * @default "auto" | ||
| */ | ||
| defaultIsModuleExports?: DefaultIsModuleExportsOption | ((id: string) => DefaultIsModuleExportsOption); | ||
| /** | ||
| * Some modules contain dynamic `require` calls, or require modules that | ||
| * contain circular dependencies, which are not handled well by static | ||
| * imports. Including those modules as `dynamicRequireTargets` will simulate a | ||
| * CommonJS (NodeJS-like) environment for them with support for dynamic | ||
| * dependencies. It also enables `strictRequires` for those modules. | ||
| * | ||
| * Note: In extreme cases, this feature may result in some paths being | ||
| * rendered as absolute in the final bundle. The plugin tries to avoid | ||
| * exposing paths from the local machine, but if you are `dynamicRequirePaths` | ||
| * with paths that are far away from your project's folder, that may require | ||
| * replacing strings like `"/Users/John/Desktop/foo-project/"` -> `"/"`. | ||
| */ | ||
| dynamicRequireTargets?: string | ReadonlyArray<string>; | ||
| /** | ||
| * To avoid long paths when using the `dynamicRequireTargets` option, you can use this option to specify a directory | ||
| * that is a common parent for all files that use dynamic require statements. Using a directory higher up such as `/` | ||
| * may lead to unnecessarily long paths in the generated code and may expose directory names on your machine like your | ||
| * home directory name. By default, it uses the current working directory. | ||
| */ | ||
| dynamicRequireRoot?: string; | ||
| /** | ||
| * When enabled, external Node built-ins (e.g., `node:fs`) required from wrapped CommonJS modules | ||
| * will use `createRequire(import.meta.url)` instead of being hoisted as ESM imports. This prevents | ||
| * eager loading of Node built-ins at module initialization time. | ||
| * | ||
| * Note: This option adds a dependency on `node:module` in the output, which may not be available | ||
| * in some environments like edge runtimes (Cloudflare Workers, Vercel Edge Runtime). | ||
| * | ||
| * @default false | ||
| */ | ||
| requireNodeBuiltins?: boolean; | ||
| } | ||
| /** | ||
| * Convert CommonJS modules to ES6, so they can be included in a Rollup bundle | ||
| */ | ||
| declare function commonjs(options?: RollupCommonJSOptions): Plugin; | ||
| //#endregion | ||
| export { commonjs as t }; |
| import { i as __toESM, n as __exportAll, r as __require, t as __commonJSMin } from "../_common.mjs"; | ||
| import { $ as resolveModulePath, Y as readPackageJSON, at as join$1, ct as resolve$1, nt as dirname$1, ot as normalize$1, q as findWorkspaceDir, rt as extname$1, tt as basename$1 } from "../_build/common.mjs"; | ||
| import { existsSync, promises, readFileSync, statSync } from "node:fs"; | ||
| import { readFile, rm } from "node:fs/promises"; | ||
| import { pathToFileURL } from "node:url"; | ||
| import { homedir } from "node:os"; | ||
| import { resolve } from "node:path"; | ||
| import { createJiti } from "jiti"; | ||
| import destr from "destr"; | ||
| import { defu } from "defu"; | ||
| //#region node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv/package.json | ||
| var require_package = /* @__PURE__ */ __commonJSMin(((exports, module) => { | ||
| module.exports = { | ||
| "name": "dotenv", | ||
| "version": "17.2.3", | ||
| "description": "Loads environment variables from .env file", | ||
| "main": "lib/main.js", | ||
| "types": "lib/main.d.ts", | ||
| "exports": { | ||
| ".": { | ||
| "types": "./lib/main.d.ts", | ||
| "require": "./lib/main.js", | ||
| "default": "./lib/main.js" | ||
| }, | ||
| "./config": "./config.js", | ||
| "./config.js": "./config.js", | ||
| "./lib/env-options": "./lib/env-options.js", | ||
| "./lib/env-options.js": "./lib/env-options.js", | ||
| "./lib/cli-options": "./lib/cli-options.js", | ||
| "./lib/cli-options.js": "./lib/cli-options.js", | ||
| "./package.json": "./package.json" | ||
| }, | ||
| "scripts": { | ||
| "dts-check": "tsc --project tests/types/tsconfig.json", | ||
| "lint": "standard", | ||
| "pretest": "npm run lint && npm run dts-check", | ||
| "test": "tap run tests/**/*.js --allow-empty-coverage --disable-coverage --timeout=60000", | ||
| "test:coverage": "tap run tests/**/*.js --show-full-coverage --timeout=60000 --coverage-report=text --coverage-report=lcov", | ||
| "prerelease": "npm test", | ||
| "release": "standard-version" | ||
| }, | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "git://github.com/motdotla/dotenv.git" | ||
| }, | ||
| "homepage": "https://github.com/motdotla/dotenv#readme", | ||
| "funding": "https://dotenvx.com", | ||
| "keywords": [ | ||
| "dotenv", | ||
| "env", | ||
| ".env", | ||
| "environment", | ||
| "variables", | ||
| "config", | ||
| "settings" | ||
| ], | ||
| "readmeFilename": "README.md", | ||
| "license": "BSD-2-Clause", | ||
| "devDependencies": { | ||
| "@types/node": "^18.11.3", | ||
| "decache": "^4.6.2", | ||
| "sinon": "^14.0.1", | ||
| "standard": "^17.0.0", | ||
| "standard-version": "^9.5.0", | ||
| "tap": "^19.2.0", | ||
| "typescript": "^4.8.4" | ||
| }, | ||
| "engines": { "node": ">=12" }, | ||
| "browser": { "fs": false } | ||
| }; | ||
| })); | ||
| //#endregion | ||
| //#region node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv/lib/main.js | ||
| var require_main = /* @__PURE__ */ __commonJSMin(((exports, module) => { | ||
| const fs$1 = __require("fs"); | ||
| const path$1 = __require("path"); | ||
| const os$1 = __require("os"); | ||
| const crypto = __require("crypto"); | ||
| const version = require_package().version; | ||
| const TIPS = [ | ||
| "🔐 encrypt with Dotenvx: https://dotenvx.com", | ||
| "🔐 prevent committing .env to code: https://dotenvx.com/precommit", | ||
| "🔐 prevent building .env in docker: https://dotenvx.com/prebuild", | ||
| "📡 add observability to secrets: https://dotenvx.com/ops", | ||
| "👥 sync secrets across teammates & machines: https://dotenvx.com/ops", | ||
| "🗂️ backup and recover secrets: https://dotenvx.com/ops", | ||
| "✅ audit secrets and track compliance: https://dotenvx.com/ops", | ||
| "🔄 add secrets lifecycle management: https://dotenvx.com/ops", | ||
| "🔑 add access controls to secrets: https://dotenvx.com/ops", | ||
| "🛠️ run anywhere with `dotenvx run -- yourcommand`", | ||
| "⚙️ specify custom .env file path with { path: '/custom/path/.env' }", | ||
| "⚙️ enable debug logging with { debug: true }", | ||
| "⚙️ override existing env vars with { override: true }", | ||
| "⚙️ suppress all logs with { quiet: true }", | ||
| "⚙️ write to custom object with { processEnv: myObject }", | ||
| "⚙️ load multiple .env files with { path: ['.env.local', '.env'] }" | ||
| ]; | ||
| function _getRandomTip() { | ||
| return TIPS[Math.floor(Math.random() * TIPS.length)]; | ||
| } | ||
| function parseBoolean(value) { | ||
| if (typeof value === "string") return ![ | ||
| "false", | ||
| "0", | ||
| "no", | ||
| "off", | ||
| "" | ||
| ].includes(value.toLowerCase()); | ||
| return Boolean(value); | ||
| } | ||
| function supportsAnsi() { | ||
| return process.stdout.isTTY; | ||
| } | ||
| function dim(text) { | ||
| return supportsAnsi() ? `\x1b[2m${text}\x1b[0m` : text; | ||
| } | ||
| const LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/gm; | ||
| function parse(src) { | ||
| const obj = {}; | ||
| let lines = src.toString(); | ||
| lines = lines.replace(/\r\n?/gm, "\n"); | ||
| let match; | ||
| while ((match = LINE.exec(lines)) != null) { | ||
| const key = match[1]; | ||
| let value = match[2] || ""; | ||
| value = value.trim(); | ||
| const maybeQuote = value[0]; | ||
| value = value.replace(/^(['"`])([\s\S]*)\1$/gm, "$2"); | ||
| if (maybeQuote === "\"") { | ||
| value = value.replace(/\\n/g, "\n"); | ||
| value = value.replace(/\\r/g, "\r"); | ||
| } | ||
| obj[key] = value; | ||
| } | ||
| return obj; | ||
| } | ||
| function _parseVault(options) { | ||
| options = options || {}; | ||
| const vaultPath = _vaultPath(options); | ||
| options.path = vaultPath; | ||
| const result = DotenvModule.configDotenv(options); | ||
| if (!result.parsed) { | ||
| const err = /* @__PURE__ */ new Error(`MISSING_DATA: Cannot parse ${vaultPath} for an unknown reason`); | ||
| err.code = "MISSING_DATA"; | ||
| throw err; | ||
| } | ||
| const keys = _dotenvKey(options).split(","); | ||
| const length = keys.length; | ||
| let decrypted; | ||
| for (let i = 0; i < length; i++) try { | ||
| const attrs = _instructions(result, keys[i].trim()); | ||
| decrypted = DotenvModule.decrypt(attrs.ciphertext, attrs.key); | ||
| break; | ||
| } catch (error) { | ||
| if (i + 1 >= length) throw error; | ||
| } | ||
| return DotenvModule.parse(decrypted); | ||
| } | ||
| function _warn(message) { | ||
| console.error(`[dotenv@${version}][WARN] ${message}`); | ||
| } | ||
| function _debug(message) { | ||
| console.log(`[dotenv@${version}][DEBUG] ${message}`); | ||
| } | ||
| function _log(message) { | ||
| console.log(`[dotenv@${version}] ${message}`); | ||
| } | ||
| function _dotenvKey(options) { | ||
| if (options && options.DOTENV_KEY && options.DOTENV_KEY.length > 0) return options.DOTENV_KEY; | ||
| if (process.env.DOTENV_KEY && process.env.DOTENV_KEY.length > 0) return process.env.DOTENV_KEY; | ||
| return ""; | ||
| } | ||
| function _instructions(result, dotenvKey) { | ||
| let uri; | ||
| try { | ||
| uri = new URL(dotenvKey); | ||
| } catch (error) { | ||
| if (error.code === "ERR_INVALID_URL") { | ||
| const err = /* @__PURE__ */ new Error("INVALID_DOTENV_KEY: Wrong format. Must be in valid uri format like dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=development"); | ||
| err.code = "INVALID_DOTENV_KEY"; | ||
| throw err; | ||
| } | ||
| throw error; | ||
| } | ||
| const key = uri.password; | ||
| if (!key) { | ||
| const err = /* @__PURE__ */ new Error("INVALID_DOTENV_KEY: Missing key part"); | ||
| err.code = "INVALID_DOTENV_KEY"; | ||
| throw err; | ||
| } | ||
| const environment = uri.searchParams.get("environment"); | ||
| if (!environment) { | ||
| const err = /* @__PURE__ */ new Error("INVALID_DOTENV_KEY: Missing environment part"); | ||
| err.code = "INVALID_DOTENV_KEY"; | ||
| throw err; | ||
| } | ||
| const environmentKey = `DOTENV_VAULT_${environment.toUpperCase()}`; | ||
| const ciphertext = result.parsed[environmentKey]; | ||
| if (!ciphertext) { | ||
| const err = /* @__PURE__ */ new Error(`NOT_FOUND_DOTENV_ENVIRONMENT: Cannot locate environment ${environmentKey} in your .env.vault file.`); | ||
| err.code = "NOT_FOUND_DOTENV_ENVIRONMENT"; | ||
| throw err; | ||
| } | ||
| return { | ||
| ciphertext, | ||
| key | ||
| }; | ||
| } | ||
| function _vaultPath(options) { | ||
| let possibleVaultPath = null; | ||
| if (options && options.path && options.path.length > 0) if (Array.isArray(options.path)) { | ||
| for (const filepath of options.path) if (fs$1.existsSync(filepath)) possibleVaultPath = filepath.endsWith(".vault") ? filepath : `${filepath}.vault`; | ||
| } else possibleVaultPath = options.path.endsWith(".vault") ? options.path : `${options.path}.vault`; | ||
| else possibleVaultPath = path$1.resolve(process.cwd(), ".env.vault"); | ||
| if (fs$1.existsSync(possibleVaultPath)) return possibleVaultPath; | ||
| return null; | ||
| } | ||
| function _resolveHome(envPath) { | ||
| return envPath[0] === "~" ? path$1.join(os$1.homedir(), envPath.slice(1)) : envPath; | ||
| } | ||
| function _configVault(options) { | ||
| const debug = parseBoolean(process.env.DOTENV_CONFIG_DEBUG || options && options.debug); | ||
| const quiet = parseBoolean(process.env.DOTENV_CONFIG_QUIET || options && options.quiet); | ||
| if (debug || !quiet) _log("Loading env from encrypted .env.vault"); | ||
| const parsed = DotenvModule._parseVault(options); | ||
| let processEnv = process.env; | ||
| if (options && options.processEnv != null) processEnv = options.processEnv; | ||
| DotenvModule.populate(processEnv, parsed, options); | ||
| return { parsed }; | ||
| } | ||
| function configDotenv(options) { | ||
| const dotenvPath = path$1.resolve(process.cwd(), ".env"); | ||
| let encoding = "utf8"; | ||
| let processEnv = process.env; | ||
| if (options && options.processEnv != null) processEnv = options.processEnv; | ||
| let debug = parseBoolean(processEnv.DOTENV_CONFIG_DEBUG || options && options.debug); | ||
| let quiet = parseBoolean(processEnv.DOTENV_CONFIG_QUIET || options && options.quiet); | ||
| if (options && options.encoding) encoding = options.encoding; | ||
| else if (debug) _debug("No encoding is specified. UTF-8 is used by default"); | ||
| let optionPaths = [dotenvPath]; | ||
| if (options && options.path) if (!Array.isArray(options.path)) optionPaths = [_resolveHome(options.path)]; | ||
| else { | ||
| optionPaths = []; | ||
| for (const filepath of options.path) optionPaths.push(_resolveHome(filepath)); | ||
| } | ||
| let lastError; | ||
| const parsedAll = {}; | ||
| for (const path$2 of optionPaths) try { | ||
| const parsed = DotenvModule.parse(fs$1.readFileSync(path$2, { encoding })); | ||
| DotenvModule.populate(parsedAll, parsed, options); | ||
| } catch (e) { | ||
| if (debug) _debug(`Failed to load ${path$2} ${e.message}`); | ||
| lastError = e; | ||
| } | ||
| const populated = DotenvModule.populate(processEnv, parsedAll, options); | ||
| debug = parseBoolean(processEnv.DOTENV_CONFIG_DEBUG || debug); | ||
| quiet = parseBoolean(processEnv.DOTENV_CONFIG_QUIET || quiet); | ||
| if (debug || !quiet) { | ||
| const keysCount = Object.keys(populated).length; | ||
| const shortPaths = []; | ||
| for (const filePath of optionPaths) try { | ||
| const relative$1 = path$1.relative(process.cwd(), filePath); | ||
| shortPaths.push(relative$1); | ||
| } catch (e) { | ||
| if (debug) _debug(`Failed to load ${filePath} ${e.message}`); | ||
| lastError = e; | ||
| } | ||
| _log(`injecting env (${keysCount}) from ${shortPaths.join(",")} ${dim(`-- tip: ${_getRandomTip()}`)}`); | ||
| } | ||
| if (lastError) return { | ||
| parsed: parsedAll, | ||
| error: lastError | ||
| }; | ||
| else return { parsed: parsedAll }; | ||
| } | ||
| function config(options) { | ||
| if (_dotenvKey(options).length === 0) return DotenvModule.configDotenv(options); | ||
| const vaultPath = _vaultPath(options); | ||
| if (!vaultPath) { | ||
| _warn(`You set DOTENV_KEY but you are missing a .env.vault file at ${vaultPath}. Did you forget to build it?`); | ||
| return DotenvModule.configDotenv(options); | ||
| } | ||
| return DotenvModule._configVault(options); | ||
| } | ||
| function decrypt(encrypted, keyStr) { | ||
| const key = Buffer.from(keyStr.slice(-64), "hex"); | ||
| let ciphertext = Buffer.from(encrypted, "base64"); | ||
| const nonce = ciphertext.subarray(0, 12); | ||
| const authTag = ciphertext.subarray(-16); | ||
| ciphertext = ciphertext.subarray(12, -16); | ||
| try { | ||
| const aesgcm = crypto.createDecipheriv("aes-256-gcm", key, nonce); | ||
| aesgcm.setAuthTag(authTag); | ||
| return `${aesgcm.update(ciphertext)}${aesgcm.final()}`; | ||
| } catch (error) { | ||
| const isRange = error instanceof RangeError; | ||
| const invalidKeyLength = error.message === "Invalid key length"; | ||
| const decryptionFailed = error.message === "Unsupported state or unable to authenticate data"; | ||
| if (isRange || invalidKeyLength) { | ||
| const err = /* @__PURE__ */ new Error("INVALID_DOTENV_KEY: It must be 64 characters long (or more)"); | ||
| err.code = "INVALID_DOTENV_KEY"; | ||
| throw err; | ||
| } else if (decryptionFailed) { | ||
| const err = /* @__PURE__ */ new Error("DECRYPTION_FAILED: Please check your DOTENV_KEY"); | ||
| err.code = "DECRYPTION_FAILED"; | ||
| throw err; | ||
| } else throw error; | ||
| } | ||
| } | ||
| function populate(processEnv, parsed, options = {}) { | ||
| const debug = Boolean(options && options.debug); | ||
| const override = Boolean(options && options.override); | ||
| const populated = {}; | ||
| if (typeof parsed !== "object") { | ||
| const err = /* @__PURE__ */ new Error("OBJECT_REQUIRED: Please check the processEnv argument being passed to populate"); | ||
| err.code = "OBJECT_REQUIRED"; | ||
| throw err; | ||
| } | ||
| for (const key of Object.keys(parsed)) if (Object.prototype.hasOwnProperty.call(processEnv, key)) { | ||
| if (override === true) { | ||
| processEnv[key] = parsed[key]; | ||
| populated[key] = parsed[key]; | ||
| } | ||
| if (debug) if (override === true) _debug(`"${key}" is already defined and WAS overwritten`); | ||
| else _debug(`"${key}" is already defined and was NOT overwritten`); | ||
| } else { | ||
| processEnv[key] = parsed[key]; | ||
| populated[key] = parsed[key]; | ||
| } | ||
| return populated; | ||
| } | ||
| const DotenvModule = { | ||
| configDotenv, | ||
| _configVault, | ||
| _parseVault, | ||
| config, | ||
| decrypt, | ||
| parse, | ||
| populate | ||
| }; | ||
| module.exports.configDotenv = DotenvModule.configDotenv; | ||
| module.exports._configVault = DotenvModule._configVault; | ||
| module.exports._parseVault = DotenvModule._parseVault; | ||
| module.exports.config = DotenvModule.config; | ||
| module.exports.decrypt = DotenvModule.decrypt; | ||
| module.exports.parse = DotenvModule.parse; | ||
| module.exports.populate = DotenvModule.populate; | ||
| module.exports = DotenvModule; | ||
| })); | ||
| //#endregion | ||
| //#region node_modules/.pnpm/rc9@2.1.2/node_modules/rc9/dist/index.mjs | ||
| function isBuffer(obj) { | ||
| return obj && obj.constructor && typeof obj.constructor.isBuffer === "function" && obj.constructor.isBuffer(obj); | ||
| } | ||
| function keyIdentity(key) { | ||
| return key; | ||
| } | ||
| function flatten(target, opts) { | ||
| opts = opts || {}; | ||
| const delimiter$1 = opts.delimiter || "."; | ||
| const maxDepth = opts.maxDepth; | ||
| const transformKey = opts.transformKey || keyIdentity; | ||
| const output = {}; | ||
| function step(object, prev, currentDepth) { | ||
| currentDepth = currentDepth || 1; | ||
| Object.keys(object).forEach(function(key) { | ||
| const value = object[key]; | ||
| const isarray = opts.safe && Array.isArray(value); | ||
| const type$1 = Object.prototype.toString.call(value); | ||
| const isbuffer = isBuffer(value); | ||
| const isobject = type$1 === "[object Object]" || type$1 === "[object Array]"; | ||
| const newKey = prev ? prev + delimiter$1 + transformKey(key) : transformKey(key); | ||
| if (!isarray && !isbuffer && isobject && Object.keys(value).length && (!opts.maxDepth || currentDepth < maxDepth)) return step(value, newKey, currentDepth + 1); | ||
| output[newKey] = value; | ||
| }); | ||
| } | ||
| step(target); | ||
| return output; | ||
| } | ||
| function unflatten(target, opts) { | ||
| opts = opts || {}; | ||
| const delimiter$1 = opts.delimiter || "."; | ||
| const overwrite = opts.overwrite || false; | ||
| const transformKey = opts.transformKey || keyIdentity; | ||
| const result = {}; | ||
| if (isBuffer(target) || Object.prototype.toString.call(target) !== "[object Object]") return target; | ||
| function getkey(key) { | ||
| const parsedKey = Number(key); | ||
| return isNaN(parsedKey) || key.indexOf(".") !== -1 || opts.object ? key : parsedKey; | ||
| } | ||
| function addKeys(keyPrefix, recipient, target$1) { | ||
| return Object.keys(target$1).reduce(function(result$1, key) { | ||
| result$1[keyPrefix + delimiter$1 + key] = target$1[key]; | ||
| return result$1; | ||
| }, recipient); | ||
| } | ||
| function isEmpty(val) { | ||
| const type$1 = Object.prototype.toString.call(val); | ||
| const isArray = type$1 === "[object Array]"; | ||
| const isObject = type$1 === "[object Object]"; | ||
| if (!val) return true; | ||
| else if (isArray) return !val.length; | ||
| else if (isObject) return !Object.keys(val).length; | ||
| } | ||
| target = Object.keys(target).reduce(function(result$1, key) { | ||
| const type$1 = Object.prototype.toString.call(target[key]); | ||
| if (!(type$1 === "[object Object]" || type$1 === "[object Array]") || isEmpty(target[key])) { | ||
| result$1[key] = target[key]; | ||
| return result$1; | ||
| } else return addKeys(key, result$1, flatten(target[key], opts)); | ||
| }, {}); | ||
| Object.keys(target).forEach(function(key) { | ||
| const split = key.split(delimiter$1).map(transformKey); | ||
| let key1 = getkey(split.shift()); | ||
| let key2 = getkey(split[0]); | ||
| let recipient = result; | ||
| while (key2 !== void 0) { | ||
| if (key1 === "__proto__") return; | ||
| const type$1 = Object.prototype.toString.call(recipient[key1]); | ||
| const isobject = type$1 === "[object Object]" || type$1 === "[object Array]"; | ||
| if (!overwrite && !isobject && typeof recipient[key1] !== "undefined") return; | ||
| if (overwrite && !isobject || !overwrite && recipient[key1] == null) recipient[key1] = typeof key2 === "number" && !opts.object ? [] : {}; | ||
| recipient = recipient[key1]; | ||
| if (split.length > 0) { | ||
| key1 = getkey(split.shift()); | ||
| key2 = getkey(split[0]); | ||
| } | ||
| } | ||
| recipient[key1] = unflatten(target[key], opts); | ||
| }); | ||
| return result; | ||
| } | ||
| const RE_KEY_VAL = /^\s*([^\s=]+)\s*=\s*(.*)?\s*$/; | ||
| const RE_LINES = /\n|\r|\r\n/; | ||
| const defaults = { | ||
| name: ".conf", | ||
| dir: process.cwd(), | ||
| flat: false | ||
| }; | ||
| function withDefaults(options) { | ||
| if (typeof options === "string") options = { name: options }; | ||
| return { | ||
| ...defaults, | ||
| ...options | ||
| }; | ||
| } | ||
| function parse(contents, options = {}) { | ||
| const config = {}; | ||
| const lines = contents.split(RE_LINES); | ||
| for (const line of lines) { | ||
| const match = line.match(RE_KEY_VAL); | ||
| if (!match) continue; | ||
| const key = match[1]; | ||
| if (!key || key === "__proto__" || key === "constructor") continue; | ||
| const value = destr((match[2] || "").trim()); | ||
| if (key.endsWith("[]")) { | ||
| const nkey = key.slice(0, Math.max(0, key.length - 2)); | ||
| config[nkey] = (config[nkey] || []).concat(value); | ||
| continue; | ||
| } | ||
| config[key] = value; | ||
| } | ||
| return options.flat ? config : unflatten(config, { overwrite: true }); | ||
| } | ||
| function parseFile(path$2, options) { | ||
| if (!existsSync(path$2)) return {}; | ||
| return parse(readFileSync(path$2, "utf8"), options); | ||
| } | ||
| function read(options) { | ||
| options = withDefaults(options); | ||
| return parseFile(resolve(options.dir, options.name), options); | ||
| } | ||
| function readUser(options) { | ||
| options = withDefaults(options); | ||
| options.dir = process.env.XDG_CONFIG_HOME || homedir(); | ||
| return read(options); | ||
| } | ||
| //#endregion | ||
| //#region node_modules/.pnpm/perfect-debounce@2.0.0/node_modules/perfect-debounce/dist/index.mjs | ||
| const DEBOUNCE_DEFAULTS = { trailing: true }; | ||
| /** | ||
| Debounce functions | ||
| @param fn - Promise-returning/async function to debounce. | ||
| @param wait - Milliseconds to wait before calling `fn`. Default value is 25ms | ||
| @returns A function that delays calling `fn` until after `wait` milliseconds have elapsed since the last time it was called. | ||
| @example | ||
| ``` | ||
| import { debounce } from 'perfect-debounce'; | ||
| const expensiveCall = async input => input; | ||
| const debouncedFn = debounce(expensiveCall, 200); | ||
| for (const number of [1, 2, 3]) { | ||
| console.log(await debouncedFn(number)); | ||
| } | ||
| //=> 1 | ||
| //=> 2 | ||
| //=> 3 | ||
| ``` | ||
| */ | ||
| function debounce(fn, wait = 25, options = {}) { | ||
| options = { | ||
| ...DEBOUNCE_DEFAULTS, | ||
| ...options | ||
| }; | ||
| if (!Number.isFinite(wait)) throw new TypeError("Expected `wait` to be a finite number"); | ||
| let leadingValue; | ||
| let timeout; | ||
| let resolveList = []; | ||
| let currentPromise; | ||
| let trailingArgs; | ||
| const applyFn = (_this, args) => { | ||
| currentPromise = _applyPromised(fn, _this, args); | ||
| currentPromise.finally(() => { | ||
| currentPromise = null; | ||
| if (options.trailing && trailingArgs && !timeout) { | ||
| const promise = applyFn(_this, trailingArgs); | ||
| trailingArgs = null; | ||
| return promise; | ||
| } | ||
| }); | ||
| return currentPromise; | ||
| }; | ||
| const debounced = function(...args) { | ||
| if (options.trailing) trailingArgs = args; | ||
| if (currentPromise) return currentPromise; | ||
| return new Promise((resolve$2) => { | ||
| const shouldCallNow = !timeout && options.leading; | ||
| clearTimeout(timeout); | ||
| timeout = setTimeout(() => { | ||
| timeout = null; | ||
| const promise = options.leading ? leadingValue : applyFn(this, args); | ||
| trailingArgs = null; | ||
| for (const _resolve of resolveList) _resolve(promise); | ||
| resolveList = []; | ||
| }, wait); | ||
| if (shouldCallNow) { | ||
| leadingValue = applyFn(this, args); | ||
| resolve$2(leadingValue); | ||
| } else resolveList.push(resolve$2); | ||
| }); | ||
| }; | ||
| const _clearTimeout = (timer) => { | ||
| if (timer) { | ||
| clearTimeout(timer); | ||
| timeout = null; | ||
| } | ||
| }; | ||
| debounced.isPending = () => !!timeout; | ||
| debounced.cancel = () => { | ||
| _clearTimeout(timeout); | ||
| resolveList = []; | ||
| trailingArgs = null; | ||
| }; | ||
| debounced.flush = () => { | ||
| _clearTimeout(timeout); | ||
| if (!trailingArgs || currentPromise) return; | ||
| const args = trailingArgs; | ||
| trailingArgs = null; | ||
| return applyFn(this, args); | ||
| }; | ||
| return debounced; | ||
| } | ||
| async function _applyPromised(fn, _this, args) { | ||
| return await fn.apply(_this, args); | ||
| } | ||
| //#endregion | ||
| //#region node_modules/.pnpm/c12@3.3.3_magicast@0.5.1/node_modules/c12/dist/index.mjs | ||
| var dist_exports = /* @__PURE__ */ __exportAll({ | ||
| SUPPORTED_EXTENSIONS: () => SUPPORTED_EXTENSIONS, | ||
| loadConfig: () => loadConfig, | ||
| loadDotenv: () => loadDotenv, | ||
| setupDotenv: () => setupDotenv, | ||
| watchConfig: () => watchConfig | ||
| }); | ||
| var import_main = /* @__PURE__ */ __toESM(require_main(), 1); | ||
| /** | ||
| * Load and interpolate environment variables into `process.env`. | ||
| * If you need more control (or access to the values), consider using `loadDotenv` instead | ||
| * | ||
| */ | ||
| async function setupDotenv(options) { | ||
| const targetEnvironment = options.env ?? process.env; | ||
| const environment = await loadDotenv({ | ||
| cwd: options.cwd, | ||
| fileName: options.fileName ?? ".env", | ||
| env: targetEnvironment, | ||
| interpolate: options.interpolate ?? true | ||
| }); | ||
| const dotenvVars = getDotEnvVars(targetEnvironment); | ||
| for (const key in environment) { | ||
| if (key.startsWith("_")) continue; | ||
| if (targetEnvironment[key] === void 0 || dotenvVars.has(key)) targetEnvironment[key] = environment[key]; | ||
| } | ||
| return environment; | ||
| } | ||
| /** Load environment variables into an object. */ | ||
| async function loadDotenv(options) { | ||
| const environment = Object.create(null); | ||
| const cwd = resolve$1(options.cwd || "."); | ||
| const _fileName = options.fileName || ".env"; | ||
| const dotenvFiles = typeof _fileName === "string" ? [_fileName] : _fileName; | ||
| const dotenvVars = getDotEnvVars(options.env || {}); | ||
| Object.assign(environment, options.env); | ||
| for (const file of dotenvFiles) { | ||
| const dotenvFile = resolve$1(cwd, file); | ||
| if (!statSync(dotenvFile, { throwIfNoEntry: false })?.isFile()) continue; | ||
| const parsed = import_main.parse(await promises.readFile(dotenvFile, "utf8")); | ||
| for (const key in parsed) { | ||
| if (key in environment && !dotenvVars.has(key)) continue; | ||
| environment[key] = parsed[key]; | ||
| dotenvVars.add(key); | ||
| } | ||
| } | ||
| if (options.interpolate) interpolate(environment); | ||
| return environment; | ||
| } | ||
| function interpolate(target, source = {}, parse$1 = (v) => v) { | ||
| function getValue(key) { | ||
| return source[key] === void 0 ? target[key] : source[key]; | ||
| } | ||
| function interpolate$1(value, parents = []) { | ||
| if (typeof value !== "string") return value; | ||
| return parse$1((value.match(/(.?\${?(?:[\w:]+)?}?)/g) || []).reduce((newValue, match) => { | ||
| const parts = /(.?)\${?([\w:]+)?}?/g.exec(match) || []; | ||
| const prefix = parts[1]; | ||
| let value$1, replacePart; | ||
| if (prefix === "\\") { | ||
| replacePart = parts[0] || ""; | ||
| value$1 = replacePart.replace(String.raw`\$`, "$"); | ||
| } else { | ||
| const key = parts[2]; | ||
| replacePart = (parts[0] || "").slice(prefix.length); | ||
| if (parents.includes(key)) { | ||
| console.warn(`Please avoid recursive environment variables ( loop: ${parents.join(" > ")} > ${key} )`); | ||
| return ""; | ||
| } | ||
| value$1 = getValue(key); | ||
| value$1 = interpolate$1(value$1, [...parents, key]); | ||
| } | ||
| return value$1 === void 0 ? newValue : newValue.replace(replacePart, value$1); | ||
| }, value)); | ||
| } | ||
| for (const key in target) target[key] = interpolate$1(getValue(key)); | ||
| } | ||
| function getDotEnvVars(targetEnvironment) { | ||
| const globalRegistry = globalThis.__c12_dotenv_vars__ ||= /* @__PURE__ */ new Map(); | ||
| if (!globalRegistry.has(targetEnvironment)) globalRegistry.set(targetEnvironment, /* @__PURE__ */ new Set()); | ||
| return globalRegistry.get(targetEnvironment); | ||
| } | ||
| const _normalize = (p) => p?.replace(/\\/g, "/"); | ||
| const ASYNC_LOADERS = { | ||
| ".yaml": () => import("./confbox.mjs").then((n) => n.a).then((r) => r.parseYAML), | ||
| ".yml": () => import("./confbox.mjs").then((n) => n.a).then((r) => r.parseYAML), | ||
| ".jsonc": () => import("./confbox.mjs").then((n) => n.t).then((r) => r.parseJSONC), | ||
| ".json5": () => import("./confbox.mjs").then((n) => n.o).then((r) => r.parseJSON5), | ||
| ".toml": () => import("./confbox.mjs").then((n) => n.r).then((r) => r.parseTOML) | ||
| }; | ||
| const SUPPORTED_EXTENSIONS = Object.freeze([ | ||
| ".js", | ||
| ".ts", | ||
| ".mjs", | ||
| ".cjs", | ||
| ".mts", | ||
| ".cts", | ||
| ".json", | ||
| ".jsonc", | ||
| ".json5", | ||
| ".yaml", | ||
| ".yml", | ||
| ".toml" | ||
| ]); | ||
| async function loadConfig(options) { | ||
| options.cwd = resolve$1(process.cwd(), options.cwd || "."); | ||
| options.name = options.name || "config"; | ||
| options.envName = options.envName ?? process.env.NODE_ENV; | ||
| options.configFile = options.configFile ?? (options.name === "config" ? "config" : `${options.name}.config`); | ||
| options.rcFile = options.rcFile ?? `.${options.name}rc`; | ||
| if (options.extend !== false) options.extend = { | ||
| extendKey: "extends", | ||
| ...options.extend | ||
| }; | ||
| const _merger = options.merger || defu; | ||
| options.jiti = options.jiti || createJiti(join$1(options.cwd, options.configFile), { | ||
| interopDefault: true, | ||
| moduleCache: false, | ||
| extensions: [...SUPPORTED_EXTENSIONS], | ||
| ...options.jitiOptions | ||
| }); | ||
| const r = { | ||
| config: {}, | ||
| cwd: options.cwd, | ||
| configFile: resolve$1(options.cwd, options.configFile), | ||
| layers: [], | ||
| _configFile: void 0 | ||
| }; | ||
| const rawConfigs = { | ||
| overrides: options.overrides, | ||
| main: void 0, | ||
| rc: void 0, | ||
| packageJson: void 0, | ||
| defaultConfig: options.defaultConfig | ||
| }; | ||
| if (options.dotenv) await setupDotenv({ | ||
| cwd: options.cwd, | ||
| ...options.dotenv === true ? {} : options.dotenv | ||
| }); | ||
| const _mainConfig = await resolveConfig(".", options); | ||
| if (_mainConfig.configFile) { | ||
| rawConfigs.main = _mainConfig.config; | ||
| r.configFile = _mainConfig.configFile; | ||
| r._configFile = _mainConfig._configFile; | ||
| } | ||
| if (_mainConfig.meta) r.meta = _mainConfig.meta; | ||
| if (options.rcFile) { | ||
| const rcSources = []; | ||
| rcSources.push(read({ | ||
| name: options.rcFile, | ||
| dir: options.cwd | ||
| })); | ||
| if (options.globalRc) { | ||
| const workspaceDir = await findWorkspaceDir(options.cwd).catch(() => {}); | ||
| if (workspaceDir) rcSources.push(read({ | ||
| name: options.rcFile, | ||
| dir: workspaceDir | ||
| })); | ||
| rcSources.push(readUser({ | ||
| name: options.rcFile, | ||
| dir: options.cwd | ||
| })); | ||
| } | ||
| rawConfigs.rc = _merger({}, ...rcSources); | ||
| } | ||
| if (options.packageJson) { | ||
| const keys = (Array.isArray(options.packageJson) ? options.packageJson : [typeof options.packageJson === "string" ? options.packageJson : options.name]).filter((t) => t && typeof t === "string"); | ||
| const pkgJsonFile = await readPackageJSON(options.cwd).catch(() => {}); | ||
| rawConfigs.packageJson = _merger({}, ...keys.map((key) => pkgJsonFile?.[key])); | ||
| } | ||
| const configs = {}; | ||
| for (const key in rawConfigs) { | ||
| const value = rawConfigs[key]; | ||
| configs[key] = await (typeof value === "function" ? value({ | ||
| configs, | ||
| rawConfigs | ||
| }) : value); | ||
| } | ||
| if (Array.isArray(configs.main)) r.config = configs.main; | ||
| else { | ||
| r.config = _merger(configs.overrides, configs.main, configs.rc, configs.packageJson, configs.defaultConfig); | ||
| if (options.extend) { | ||
| await extendConfig(r.config, options); | ||
| r.layers = r.config._layers; | ||
| delete r.config._layers; | ||
| r.config = _merger(r.config, ...r.layers.map((e) => e.config)); | ||
| } | ||
| } | ||
| r.layers = [...[ | ||
| configs.overrides && { | ||
| config: configs.overrides, | ||
| configFile: void 0, | ||
| cwd: void 0 | ||
| }, | ||
| { | ||
| config: configs.main, | ||
| configFile: options.configFile, | ||
| cwd: options.cwd | ||
| }, | ||
| configs.rc && { | ||
| config: configs.rc, | ||
| configFile: options.rcFile | ||
| }, | ||
| configs.packageJson && { | ||
| config: configs.packageJson, | ||
| configFile: "package.json" | ||
| } | ||
| ].filter((l) => l && l.config), ...r.layers]; | ||
| if (options.defaults) r.config = _merger(r.config, options.defaults); | ||
| if (options.omit$Keys) { | ||
| for (const key in r.config) if (key.startsWith("$")) delete r.config[key]; | ||
| } | ||
| if (options.configFileRequired && !r._configFile) throw new Error(`Required config (${r.configFile}) cannot be resolved.`); | ||
| return r; | ||
| } | ||
| async function extendConfig(config, options) { | ||
| config._layers = config._layers || []; | ||
| if (!options.extend) return; | ||
| let keys = options.extend.extendKey; | ||
| if (typeof keys === "string") keys = [keys]; | ||
| const extendSources = []; | ||
| for (const key of keys) { | ||
| extendSources.push(...(Array.isArray(config[key]) ? config[key] : [config[key]]).filter(Boolean)); | ||
| delete config[key]; | ||
| } | ||
| for (let extendSource of extendSources) { | ||
| const originalExtendSource = extendSource; | ||
| let sourceOptions = {}; | ||
| if (extendSource.source) { | ||
| sourceOptions = extendSource.options || {}; | ||
| extendSource = extendSource.source; | ||
| } | ||
| if (Array.isArray(extendSource)) { | ||
| sourceOptions = extendSource[1] || {}; | ||
| extendSource = extendSource[0]; | ||
| } | ||
| if (typeof extendSource !== "string") { | ||
| console.warn(`Cannot extend config from \`${JSON.stringify(originalExtendSource)}\` in ${options.cwd}`); | ||
| continue; | ||
| } | ||
| const _config = await resolveConfig(extendSource, options, sourceOptions); | ||
| if (!_config.config) { | ||
| console.warn(`Cannot extend config from \`${extendSource}\` in ${options.cwd}`); | ||
| continue; | ||
| } | ||
| await extendConfig(_config.config, { | ||
| ...options, | ||
| cwd: _config.cwd | ||
| }); | ||
| config._layers.push(_config); | ||
| if (_config.config._layers) { | ||
| config._layers.push(..._config.config._layers); | ||
| delete _config.config._layers; | ||
| } | ||
| } | ||
| } | ||
| const GIGET_PREFIXES = [ | ||
| "gh:", | ||
| "github:", | ||
| "gitlab:", | ||
| "bitbucket:", | ||
| "https://", | ||
| "http://" | ||
| ]; | ||
| const NPM_PACKAGE_RE = /^(@[\da-z~-][\d._a-z~-]*\/)?[\da-z~-][\d._a-z~-]*($|\/.*)/; | ||
| async function resolveConfig(source, options, sourceOptions = {}) { | ||
| if (options.resolve) { | ||
| const res$1 = await options.resolve(source, options); | ||
| if (res$1) return res$1; | ||
| } | ||
| const _merger = options.merger || defu; | ||
| const customProviderKeys = Object.keys(sourceOptions.giget?.providers || {}).map((key) => `${key}:`); | ||
| const gigetPrefixes = customProviderKeys.length > 0 ? [...new Set([...customProviderKeys, ...GIGET_PREFIXES])] : GIGET_PREFIXES; | ||
| if (options.giget !== false && gigetPrefixes.some((prefix) => source.startsWith(prefix))) { | ||
| const { downloadTemplate } = await import("./nypm+giget+tinyexec.mjs").then((n) => n.t); | ||
| const { digest } = await import("ohash"); | ||
| const cloneName = source.replace(/\W+/g, "_").split("_").splice(0, 3).join("_") + "_" + digest(source).slice(0, 10).replace(/[-_]/g, ""); | ||
| let cloneDir; | ||
| const localNodeModules = resolve$1(options.cwd, "node_modules"); | ||
| const parentDir = dirname$1(options.cwd); | ||
| if (basename$1(parentDir) === ".c12") cloneDir = join$1(parentDir, cloneName); | ||
| else if (existsSync(localNodeModules)) cloneDir = join$1(localNodeModules, ".c12", cloneName); | ||
| else cloneDir = process.env.XDG_CACHE_HOME ? resolve$1(process.env.XDG_CACHE_HOME, "c12", cloneName) : resolve$1(homedir(), ".cache/c12", cloneName); | ||
| if (existsSync(cloneDir) && !sourceOptions.install) await rm(cloneDir, { recursive: true }); | ||
| source = (await downloadTemplate(source, { | ||
| dir: cloneDir, | ||
| install: sourceOptions.install, | ||
| force: sourceOptions.install, | ||
| auth: sourceOptions.auth, | ||
| ...options.giget, | ||
| ...sourceOptions.giget | ||
| })).dir; | ||
| } | ||
| if (NPM_PACKAGE_RE.test(source)) source = tryResolve(source, options) || source; | ||
| const ext = extname$1(source); | ||
| const isDir = !ext || ext === basename$1(source); | ||
| const cwd = resolve$1(options.cwd, isDir ? source : dirname$1(source)); | ||
| if (isDir) source = options.configFile; | ||
| const res = { | ||
| config: void 0, | ||
| configFile: void 0, | ||
| cwd, | ||
| source, | ||
| sourceOptions | ||
| }; | ||
| res.configFile = tryResolve(resolve$1(cwd, source), options) || tryResolve(resolve$1(cwd, ".config", source.replace(/\.config$/, "")), options) || tryResolve(resolve$1(cwd, ".config", source), options) || source; | ||
| if (!existsSync(res.configFile)) return res; | ||
| res._configFile = res.configFile; | ||
| const configFileExt = extname$1(res.configFile) || ""; | ||
| if (configFileExt in ASYNC_LOADERS) res.config = (await ASYNC_LOADERS[configFileExt]())(await readFile(res.configFile, "utf8")); | ||
| else res.config = await options.jiti.import(res.configFile, { default: true }); | ||
| if (typeof res.config === "function") res.config = await res.config(options.context); | ||
| if (options.envName) { | ||
| const envConfig = { | ||
| ...res.config["$" + options.envName], | ||
| ...res.config.$env?.[options.envName] | ||
| }; | ||
| if (Object.keys(envConfig).length > 0) res.config = _merger(envConfig, res.config); | ||
| } | ||
| res.meta = defu(res.sourceOptions.meta, res.config.$meta); | ||
| delete res.config.$meta; | ||
| if (res.sourceOptions.overrides) res.config = _merger(res.sourceOptions.overrides, res.config); | ||
| res.configFile = _normalize(res.configFile); | ||
| res.source = _normalize(res.source); | ||
| return res; | ||
| } | ||
| function tryResolve(id, options) { | ||
| const res = resolveModulePath(id, { | ||
| try: true, | ||
| from: pathToFileURL(join$1(options.cwd || ".", options.configFile || "/")), | ||
| suffixes: ["", "/index"], | ||
| extensions: SUPPORTED_EXTENSIONS, | ||
| cache: false | ||
| }); | ||
| return res ? normalize$1(res) : void 0; | ||
| } | ||
| const eventMap = { | ||
| add: "created", | ||
| change: "updated", | ||
| unlink: "removed" | ||
| }; | ||
| async function watchConfig(options) { | ||
| let config = await loadConfig(options); | ||
| const configName = options.name || "config"; | ||
| const configFileName = options.configFile ?? (options.name === "config" ? "config" : `${options.name}.config`); | ||
| const watchingFiles = [...new Set((config.layers || []).filter((l) => l.cwd).flatMap((l) => [ | ||
| ...SUPPORTED_EXTENSIONS.flatMap((ext) => [ | ||
| resolve$1(l.cwd, configFileName + ext), | ||
| resolve$1(l.cwd, ".config", configFileName + ext), | ||
| resolve$1(l.cwd, ".config", configFileName.replace(/\.config$/, "") + ext) | ||
| ]), | ||
| l.source && resolve$1(l.cwd, l.source), | ||
| options.rcFile && resolve$1(l.cwd, typeof options.rcFile === "string" ? options.rcFile : `.${configName}rc`), | ||
| options.packageJson && resolve$1(l.cwd, "package.json") | ||
| ]).filter(Boolean))]; | ||
| const watch$1 = await import("./readdirp+chokidar.mjs").then((n) => n.t).then((r) => r.watch || r.default || r); | ||
| const { diff } = await import("ohash/utils"); | ||
| const _fswatcher = watch$1(watchingFiles, { | ||
| ignoreInitial: true, | ||
| ...options.chokidarOptions | ||
| }); | ||
| const onChange = async (event, path$2) => { | ||
| const type$1 = eventMap[event]; | ||
| if (!type$1) return; | ||
| if (options.onWatch) await options.onWatch({ | ||
| type: type$1, | ||
| path: path$2 | ||
| }); | ||
| const oldConfig = config; | ||
| try { | ||
| config = await loadConfig(options); | ||
| } catch (error) { | ||
| console.warn(`Failed to load config ${path$2}\n${error}`); | ||
| return; | ||
| } | ||
| const changeCtx = { | ||
| newConfig: config, | ||
| oldConfig, | ||
| getDiff: () => diff(oldConfig.config, config.config) | ||
| }; | ||
| if (options.acceptHMR) { | ||
| if (await options.acceptHMR(changeCtx)) return; | ||
| } | ||
| if (options.onUpdate) await options.onUpdate(changeCtx); | ||
| }; | ||
| if (options.debounce === false) _fswatcher.on("all", onChange); | ||
| else _fswatcher.on("all", debounce(onChange, options.debounce ?? 100)); | ||
| const utils = { | ||
| watchingFiles, | ||
| unwatch: async () => { | ||
| await _fswatcher.close(); | ||
| } | ||
| }; | ||
| return new Proxy(utils, { get(_, prop) { | ||
| if (prop in utils) return utils[prop]; | ||
| return config[prop]; | ||
| } }); | ||
| } | ||
| //#endregion | ||
| export { debounce as i, loadConfig as n, watchConfig as r, dist_exports as t }; |
| //#region src/utils.ts | ||
| function flatHooks(configHooks, hooks = {}, parentName) { | ||
| for (const key in configHooks) { | ||
| const subHook = configHooks[key]; | ||
| const name = parentName ? `${parentName}:${key}` : key; | ||
| if (typeof subHook === "object" && subHook !== null) flatHooks(subHook, hooks, name); | ||
| else if (typeof subHook === "function") hooks[name] = subHook; | ||
| } | ||
| return hooks; | ||
| } | ||
| function mergeHooks(...hooks) { | ||
| const finalHooks = {}; | ||
| for (const hook of hooks) { | ||
| const flatenHook = flatHooks(hook); | ||
| for (const key in flatenHook) if (finalHooks[key]) finalHooks[key].push(flatenHook[key]); | ||
| else finalHooks[key] = [flatenHook[key]]; | ||
| } | ||
| for (const key in finalHooks) if (finalHooks[key].length > 1) { | ||
| const array = finalHooks[key]; | ||
| finalHooks[key] = (...arguments_) => serial(array, (function_) => function_(...arguments_)); | ||
| } else finalHooks[key] = finalHooks[key][0]; | ||
| return finalHooks; | ||
| } | ||
| function serial(tasks, function_) { | ||
| return tasks.reduce((promise, task) => promise.then(() => function_(task)), Promise.resolve()); | ||
| } | ||
| const createTask = /* @__PURE__ */ (() => { | ||
| if (console.createTask) return console.createTask; | ||
| const defaultTask = { run: (fn) => fn() }; | ||
| return () => defaultTask; | ||
| })(); | ||
| function callHooks(hooks, args, startIndex, task) { | ||
| for (let i = startIndex; i < hooks.length; i += 1) try { | ||
| const result = task ? task.run(() => hooks[i](...args)) : hooks[i](...args); | ||
| if (result instanceof Promise) return result.then(() => callHooks(hooks, args, i + 1, task)); | ||
| } catch (error) { | ||
| return Promise.reject(error); | ||
| } | ||
| } | ||
| function serialTaskCaller(hooks, args, name) { | ||
| if (hooks.length > 0) return callHooks(hooks, args, 0, createTask(name)); | ||
| } | ||
| function parallelTaskCaller(hooks, args, name) { | ||
| if (hooks.length > 0) { | ||
| const task = createTask(name); | ||
| return Promise.all(hooks.map((hook) => task.run(() => hook(...args)))); | ||
| } | ||
| } | ||
| /** @deprecated */ | ||
| function serialCaller(hooks, arguments_) { | ||
| return hooks.reduce((promise, hookFunction) => promise.then(() => hookFunction(...arguments_ || [])), Promise.resolve()); | ||
| } | ||
| /** @deprecated */ | ||
| function parallelCaller(hooks, args) { | ||
| return Promise.all(hooks.map((hook) => hook(...args || []))); | ||
| } | ||
| function callEachWith(callbacks, arg0) { | ||
| for (const callback of [...callbacks]) callback(arg0); | ||
| } | ||
| //#endregion | ||
| //#region src/hookable.ts | ||
| var Hookable = class { | ||
| _hooks; | ||
| _before; | ||
| _after; | ||
| _deprecatedHooks; | ||
| _deprecatedMessages; | ||
| constructor() { | ||
| this._hooks = {}; | ||
| this._before = void 0; | ||
| this._after = void 0; | ||
| this._deprecatedMessages = void 0; | ||
| this._deprecatedHooks = {}; | ||
| this.hook = this.hook.bind(this); | ||
| this.callHook = this.callHook.bind(this); | ||
| this.callHookWith = this.callHookWith.bind(this); | ||
| } | ||
| hook(name, function_, options = {}) { | ||
| if (!name || typeof function_ !== "function") return () => {}; | ||
| const originalName = name; | ||
| let dep; | ||
| while (this._deprecatedHooks[name]) { | ||
| dep = this._deprecatedHooks[name]; | ||
| name = dep.to; | ||
| } | ||
| if (dep && !options.allowDeprecated) { | ||
| let message = dep.message; | ||
| if (!message) message = `${originalName} hook has been deprecated` + (dep.to ? `, please use ${dep.to}` : ""); | ||
| if (!this._deprecatedMessages) this._deprecatedMessages = /* @__PURE__ */ new Set(); | ||
| if (!this._deprecatedMessages.has(message)) { | ||
| console.warn(message); | ||
| this._deprecatedMessages.add(message); | ||
| } | ||
| } | ||
| if (!function_.name) try { | ||
| Object.defineProperty(function_, "name", { | ||
| get: () => "_" + name.replace(/\W+/g, "_") + "_hook_cb", | ||
| configurable: true | ||
| }); | ||
| } catch {} | ||
| this._hooks[name] = this._hooks[name] || []; | ||
| this._hooks[name].push(function_); | ||
| return () => { | ||
| if (function_) { | ||
| this.removeHook(name, function_); | ||
| function_ = void 0; | ||
| } | ||
| }; | ||
| } | ||
| hookOnce(name, function_) { | ||
| let _unreg; | ||
| let _function = (...arguments_) => { | ||
| if (typeof _unreg === "function") _unreg(); | ||
| _unreg = void 0; | ||
| _function = void 0; | ||
| return function_(...arguments_); | ||
| }; | ||
| _unreg = this.hook(name, _function); | ||
| return _unreg; | ||
| } | ||
| removeHook(name, function_) { | ||
| const hooks = this._hooks[name]; | ||
| if (hooks) { | ||
| const index = hooks.indexOf(function_); | ||
| if (index !== -1) hooks.splice(index, 1); | ||
| if (hooks.length === 0) this._hooks[name] = void 0; | ||
| } | ||
| } | ||
| deprecateHook(name, deprecated) { | ||
| this._deprecatedHooks[name] = typeof deprecated === "string" ? { to: deprecated } : deprecated; | ||
| const _hooks = this._hooks[name] || []; | ||
| this._hooks[name] = void 0; | ||
| for (const hook of _hooks) this.hook(name, hook); | ||
| } | ||
| deprecateHooks(deprecatedHooks) { | ||
| for (const name in deprecatedHooks) this.deprecateHook(name, deprecatedHooks[name]); | ||
| } | ||
| addHooks(configHooks) { | ||
| const hooks = flatHooks(configHooks); | ||
| const removeFns = Object.keys(hooks).map((key) => this.hook(key, hooks[key])); | ||
| return () => { | ||
| for (const unreg of removeFns) unreg(); | ||
| removeFns.length = 0; | ||
| }; | ||
| } | ||
| removeHooks(configHooks) { | ||
| const hooks = flatHooks(configHooks); | ||
| for (const key in hooks) this.removeHook(key, hooks[key]); | ||
| } | ||
| removeAllHooks() { | ||
| this._hooks = {}; | ||
| } | ||
| callHook(name, ...args) { | ||
| return this.callHookWith(serialTaskCaller, name, args); | ||
| } | ||
| callHookParallel(name, ...args) { | ||
| return this.callHookWith(parallelTaskCaller, name, args); | ||
| } | ||
| callHookWith(caller, name, args) { | ||
| const event = this._before || this._after ? { | ||
| name, | ||
| args, | ||
| context: {} | ||
| } : void 0; | ||
| if (this._before) callEachWith(this._before, event); | ||
| const result = caller(this._hooks[name] ? [...this._hooks[name]] : [], args, name); | ||
| if (result instanceof Promise) return result.finally(() => { | ||
| if (this._after && event) callEachWith(this._after, event); | ||
| }); | ||
| if (this._after && event) callEachWith(this._after, event); | ||
| return result; | ||
| } | ||
| beforeEach(function_) { | ||
| this._before = this._before || []; | ||
| this._before.push(function_); | ||
| return () => { | ||
| if (this._before !== void 0) { | ||
| const index = this._before.indexOf(function_); | ||
| if (index !== -1) this._before.splice(index, 1); | ||
| } | ||
| }; | ||
| } | ||
| afterEach(function_) { | ||
| this._after = this._after || []; | ||
| this._after.push(function_); | ||
| return () => { | ||
| if (this._after !== void 0) { | ||
| const index = this._after.indexOf(function_); | ||
| if (index !== -1) this._after.splice(index, 1); | ||
| } | ||
| }; | ||
| } | ||
| }; | ||
| function createHooks() { | ||
| return new Hookable(); | ||
| } | ||
| var HookableCore = class { | ||
| _hooks; | ||
| constructor() { | ||
| this._hooks = {}; | ||
| } | ||
| hook(name, fn) { | ||
| if (!name || typeof fn !== "function") return () => {}; | ||
| this._hooks[name] = this._hooks[name] || []; | ||
| this._hooks[name].push(fn); | ||
| return () => { | ||
| if (fn) { | ||
| this.removeHook(name, fn); | ||
| fn = void 0; | ||
| } | ||
| }; | ||
| } | ||
| removeHook(name, function_) { | ||
| const hooks = this._hooks[name]; | ||
| if (hooks) { | ||
| const index = hooks.indexOf(function_); | ||
| if (index !== -1) hooks.splice(index, 1); | ||
| if (hooks.length === 0) this._hooks[name] = void 0; | ||
| } | ||
| } | ||
| callHook(name, ...args) { | ||
| const hooks = this._hooks[name]; | ||
| if (!hooks || hooks.length === 0) return; | ||
| return callHooks(hooks, args, 0); | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region src/debugger.ts | ||
| const isBrowser = typeof window !== "undefined"; | ||
| /** Start debugging hook names and timing in console */ | ||
| function createDebugger(hooks, _options = {}) { | ||
| const options = { | ||
| inspect: isBrowser, | ||
| group: isBrowser, | ||
| filter: () => true, | ||
| ..._options | ||
| }; | ||
| const _filter = options.filter; | ||
| const filter = typeof _filter === "string" ? (name) => name.startsWith(_filter) : _filter; | ||
| const _tag = options.tag ? `[${options.tag}] ` : ""; | ||
| const logPrefix = (event) => _tag + event.name + "".padEnd(event._id, "\0"); | ||
| const _idCtr = {}; | ||
| const unsubscribeBefore = hooks.beforeEach((event) => { | ||
| if (filter !== void 0 && !filter(event.name)) return; | ||
| _idCtr[event.name] = _idCtr[event.name] || 0; | ||
| event._id = _idCtr[event.name]++; | ||
| console.time(logPrefix(event)); | ||
| }); | ||
| const unsubscribeAfter = hooks.afterEach((event) => { | ||
| if (filter !== void 0 && !filter(event.name)) return; | ||
| if (options.group) console.groupCollapsed(event.name); | ||
| if (options.inspect) console.timeLog(logPrefix(event), event.args); | ||
| else console.timeEnd(logPrefix(event)); | ||
| if (options.group) console.groupEnd(); | ||
| _idCtr[event.name]--; | ||
| }); | ||
| return { close: () => { | ||
| unsubscribeBefore(); | ||
| unsubscribeAfter(); | ||
| } }; | ||
| } | ||
| //#endregion | ||
| export { Hookable, HookableCore, createDebugger, createHooks, flatHooks, mergeHooks, parallelCaller, serial, serialCaller }; |
| { | ||
| "name": "hookable", | ||
| "version": "6.0.1", | ||
| "description": "Awaitable hook system", | ||
| "keywords": [ | ||
| "hook", | ||
| "hookable", | ||
| "plugin", | ||
| "tapable", | ||
| "tappable" | ||
| ], | ||
| "repository": "unjs/hookable", | ||
| "license": "MIT", | ||
| "sideEffects": false, | ||
| "type": "module", | ||
| "exports": { | ||
| ".": "./dist/index.mjs" | ||
| }, | ||
| "main": "./dist/index.mjs", | ||
| "types": "./dist/index.d.mts", | ||
| "files": [ | ||
| "dist" | ||
| ], | ||
| "scripts": { | ||
| "bench": "node --expose-gc --allow-natives-syntax test/bench.ts", | ||
| "build": "obuild src/index.ts", | ||
| "dev": "vitest", | ||
| "lint": "eslint --cache . && prettier -c src test", | ||
| "lint:fix": "eslint --cache . --fix && prettier -c src test -w", | ||
| "prepublish": "pnpm build", | ||
| "release": "pnpm test && pnpm build && changelogen --release --publish --push", | ||
| "test": "pnpm lint && vitest run --coverage", | ||
| "test:types": "tsc --noEmit" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/node": "^25.0.3", | ||
| "@vitest/coverage-v8": "^4.0.16", | ||
| "changelogen": "^0.6.2", | ||
| "esbuild": "^0.27.2", | ||
| "eslint": "^9.39.2", | ||
| "eslint-config-unjs": "^0.5.0", | ||
| "expect-type": "^1.3.0", | ||
| "hookable-prev": "npm:hookable@^5.5.3", | ||
| "mitata": "^1.0.34", | ||
| "obuild": "^0.4.9", | ||
| "prettier": "^3.7.4", | ||
| "typescript": "^5.9.3", | ||
| "vite": "^7.3.0", | ||
| "vitest": "^4.0.16" | ||
| }, | ||
| "packageManager": "pnpm@10.26.0" | ||
| } |
| // src/helpers.ts | ||
| import useColors from "@poppinss/colors"; | ||
| var ANSI_REGEX = new RegExp( | ||
| [ | ||
| `[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?(?:\\u0007|\\u001B\\u005C|\\u009C))`, | ||
| "(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))" | ||
| ].join("|"), | ||
| "g" | ||
| ); | ||
| function htmlEscape(value) { | ||
| return value.replace(/&/g, "&").replace(/\\"/g, "\"").replace(/</g, "<").replace(/>/g, ">"); | ||
| } | ||
| function wordWrap(value, options) { | ||
| const width = options.width; | ||
| const indent = options.indent; | ||
| const newLine = `${options.newLine}${indent}`; | ||
| if (!width) { | ||
| return options.escape ? options.escape(value) : htmlEscape(value); | ||
| } | ||
| let regexString = ".{1," + width + "}"; | ||
| regexString += "([\\s\u200B]+|$)|[^\\s\u200B]+?([\\s\u200B]+|$)"; | ||
| const re = new RegExp(regexString, "g"); | ||
| const lines = value.match(re) || []; | ||
| const result = lines.map(function(line) { | ||
| if (line.slice(-1) === "\n") { | ||
| line = line.slice(0, line.length - 1); | ||
| } | ||
| return options.escape ? options.escape(line) : htmlEscape(line); | ||
| }).join(newLine); | ||
| return result; | ||
| } | ||
| function stripAnsi(value) { | ||
| return value.replace(ANSI_REGEX, ""); | ||
| } | ||
| var colors = useColors.ansi(); | ||
| export { | ||
| htmlEscape, | ||
| wordWrap, | ||
| stripAnsi, | ||
| colors | ||
| }; |
| import { | ||
| colors, | ||
| stripAnsi | ||
| } from "./chunk-4L7RY2JA.js"; | ||
| import { | ||
| BaseComponent, | ||
| publicDirURL | ||
| } from "./chunk-PE3GG3TN.js"; | ||
| // src/templates/error_stack_source/main.ts | ||
| import { extname } from "path"; | ||
| import { highlightText } from "@speed-highlight/core"; | ||
| import { highlightText as cliHighlightText } from "@speed-highlight/core/terminal"; | ||
| var GUTTER = "\u2503"; | ||
| var POINTER = "\u276F"; | ||
| var LANGS_MAP = { | ||
| ".tsx": "ts", | ||
| ".jsx": "js", | ||
| ".js": "js", | ||
| ".ts": "ts", | ||
| ".css": "css", | ||
| ".json": "json", | ||
| ".html": "html", | ||
| ".astro": "ts", | ||
| ".vue": "ts" | ||
| }; | ||
| var ErrorStackSource = class extends BaseComponent { | ||
| cssFile = new URL("./error_stack_source/style.css", publicDirURL); | ||
| /** | ||
| * The toHTML method is used to output the HTML for the | ||
| * web view | ||
| */ | ||
| async toHTML(props) { | ||
| const frame = props.frame; | ||
| if (frame.type === "native" || !frame.source || !frame.fileName) { | ||
| return ""; | ||
| } | ||
| const language = LANGS_MAP[extname(frame.fileName)] ?? "plain"; | ||
| const highlightMarginTop = `${frame.source.findIndex((chunk) => { | ||
| return chunk.lineNumber === frame.lineNumber; | ||
| }) * 24}px`; | ||
| const highlight = `<div class="line-highlight" style="margin-top: ${highlightMarginTop}"></div>`; | ||
| let code = await highlightText( | ||
| frame.source.map((chunk) => chunk.chunk).join("\n"), | ||
| language, | ||
| true | ||
| ); | ||
| code = code.replace( | ||
| '<div class="shj-numbers">', | ||
| `<div class="shj-numbers" style="counter-set: line ${frame.source[0].lineNumber - 1}">` | ||
| ); | ||
| return `<pre><code class="shj-lang-js">${highlight}${code}</code></pre>`; | ||
| } | ||
| /** | ||
| * The toANSI method is used to output the text for the console | ||
| */ | ||
| async toANSI(props) { | ||
| const frame = props.frame; | ||
| if (frame.type === "native" || !frame.source || !frame.fileName) { | ||
| return ""; | ||
| } | ||
| const language = LANGS_MAP[extname(frame.fileName)] ?? "plain"; | ||
| const largestLineNumber = Math.max(...frame.source.map(({ lineNumber }) => lineNumber)); | ||
| const lineNumberCols = String(largestLineNumber).length; | ||
| const code = frame.source.map(({ chunk }) => chunk).join("\n"); | ||
| const highlighted = await cliHighlightText(code, language); | ||
| return ` | ||
| ${highlighted.split("\n").map((line, index) => { | ||
| const lineNumber = frame.source[index].lineNumber; | ||
| const alignedLineNumber = String(lineNumber).padStart(lineNumberCols, " "); | ||
| if (lineNumber === props.frame.lineNumber) { | ||
| return ` ${colors.bgRed(`${POINTER} ${alignedLineNumber} ${GUTTER} ${stripAnsi(line)}`)}`; | ||
| } | ||
| return ` ${colors.dim(alignedLineNumber)} ${colors.dim(GUTTER)} ${line}`; | ||
| }).join("\n")} | ||
| `; | ||
| } | ||
| }; | ||
| export { | ||
| ErrorStackSource | ||
| }; |
| import { | ||
| BaseComponent, | ||
| publicDirURL | ||
| } from "./chunk-PE3GG3TN.js"; | ||
| // src/templates/header/main.ts | ||
| var DARK_MODE_SVG = `<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="15" height="15" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M0 0h24v24H0z" stroke="none"/><path d="M12 3h.393a7.5 7.5 0 0 0 7.92 12.446A9 9 0 1 1 12 2.992z"/></svg>`; | ||
| var LIGHT_MODE_SVG = `<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="15" height="15" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M0 0h24v24H0z" stroke="none"/><circle cx="12" cy="12" r="4"/><path d="M3 12h1m8-9v1m8 8h1m-9 8v1M5.6 5.6l.7.7m12.1-.7-.7.7m0 11.4.7.7m-12.1-.7-.7.7"/></svg>`; | ||
| var Header = class extends BaseComponent { | ||
| cssFile = new URL("./header/style.css", publicDirURL); | ||
| scriptFile = new URL("./header/script.js", publicDirURL); | ||
| /** | ||
| * The toHTML method is used to output the HTML for the | ||
| * web view | ||
| */ | ||
| async toHTML() { | ||
| return `<header id="header"> | ||
| <div id="header-actions"> | ||
| <div id="toggle-theme-container"> | ||
| <input type="checkbox" id="toggle-theme-checkbox" /> | ||
| <label id="toggle-theme-label" for="toggle-theme-checkbox"> | ||
| <span id="light-theme-indicator" title="Light mode">${LIGHT_MODE_SVG}</span> | ||
| <span id="dark-theme-indicator" title="Dark mode">${DARK_MODE_SVG}</span> | ||
| </label> | ||
| </div> | ||
| </div> | ||
| </header>`; | ||
| } | ||
| /** | ||
| * The toANSI method is used to output the text for the console | ||
| */ | ||
| async toANSI() { | ||
| return ""; | ||
| } | ||
| }; | ||
| export { | ||
| Header | ||
| }; |
| import { | ||
| BaseComponent, | ||
| publicDirURL | ||
| } from "./chunk-PE3GG3TN.js"; | ||
| // src/templates/layout/main.ts | ||
| var Layout = class extends BaseComponent { | ||
| cssFile = new URL("./layout/style.css", publicDirURL); | ||
| scriptFile = new URL("./layout/script.js", publicDirURL); | ||
| /** | ||
| * The toHTML method is used to output the HTML for the | ||
| * web view | ||
| */ | ||
| async toHTML(props) { | ||
| return `<!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <title>${props.title}</title> | ||
| <!-- STYLES --> | ||
| <!-- GLOBAL SCRIPT --> | ||
| </head> | ||
| <body> | ||
| <div id="layout"> | ||
| ${await props.children()} | ||
| </div> | ||
| <!-- SCRIPTS --> | ||
| </body> | ||
| </html>`; | ||
| } | ||
| /** | ||
| * The toANSI method is used to output the text for the console | ||
| */ | ||
| async toANSI(props) { | ||
| return ` | ||
| ${await props.children()} | ||
| `; | ||
| } | ||
| }; | ||
| export { | ||
| Layout | ||
| }; |
| import { | ||
| colors, | ||
| htmlEscape | ||
| } from "./chunk-4L7RY2JA.js"; | ||
| import { | ||
| BaseComponent, | ||
| publicDirURL | ||
| } from "./chunk-PE3GG3TN.js"; | ||
| // src/templates/error_stack/main.ts | ||
| import { dump, themes } from "@poppinss/dumper/html"; | ||
| import { dump as dumpCli } from "@poppinss/dumper/console"; | ||
| var CHEVIRON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" width="24" height="24" stroke-width="2"> | ||
| <path d="M6 9l6 6l6 -6"></path> | ||
| </svg>`; | ||
| var EDITORS = { | ||
| textmate: "txmt://open?url=file://%f&line=%l", | ||
| macvim: "mvim://open?url=file://%f&line=%l", | ||
| emacs: "emacs://open?url=file://%f&line=%l", | ||
| sublime: "subl://open?url=file://%f&line=%l", | ||
| phpstorm: "phpstorm://open?file=%f&line=%l", | ||
| atom: "atom://core/open/file?filename=%f&line=%l", | ||
| vscode: "vscode://file/%f:%l" | ||
| }; | ||
| var ErrorStack = class extends BaseComponent { | ||
| cssFile = new URL("./error_stack/style.css", publicDirURL); | ||
| scriptFile = new URL("./error_stack/script.js", publicDirURL); | ||
| /** | ||
| * Returns the file's relative name from the CWD | ||
| */ | ||
| #getRelativeFileName(filePath) { | ||
| return filePath.replace(`${process.cwd()}/`, ""); | ||
| } | ||
| /** | ||
| * Returns the index of the frame that should be expanded by | ||
| * default | ||
| */ | ||
| #getFirstExpandedFrameIndex(frames) { | ||
| let expandAtIndex = frames.findIndex((frame) => frame.type === "app"); | ||
| if (expandAtIndex === -1) { | ||
| expandAtIndex = frames.findIndex((frame) => frame.type === "module"); | ||
| } | ||
| return expandAtIndex; | ||
| } | ||
| /** | ||
| * Returns the link to open the file within known code | ||
| * editors | ||
| */ | ||
| #getEditorLink(ide, frame) { | ||
| const editorURL = EDITORS[ide] || ide; | ||
| if (!editorURL || frame.type === "native") { | ||
| return { | ||
| text: this.#getRelativeFileName(frame.fileName) | ||
| }; | ||
| } | ||
| return { | ||
| href: editorURL.replace("%f", frame.fileName).replace("%l", String(frame.lineNumber)), | ||
| text: this.#getRelativeFileName(frame.fileName) | ||
| }; | ||
| } | ||
| /** | ||
| * Returns the HTML fragment for the frame location | ||
| */ | ||
| #renderFrameLocation(frame, ide) { | ||
| const { text, href } = this.#getEditorLink(ide, frame); | ||
| const fileName = `<a${href ? ` href="${href}"` : ""} class="stack-frame-filepath" title="${text}"> | ||
| ${htmlEscape(text)} | ||
| </a>`; | ||
| const functionName = frame.functionName ? `<span>in <code title="${frame.functionName}"> | ||
| ${htmlEscape(frame.functionName)} | ||
| </code></span>` : ""; | ||
| const loc = `<span>at line <code>${frame.lineNumber}:${frame.columnNumber}</code></span>`; | ||
| if (frame.type !== "native" && frame.source) { | ||
| return `<button class="stack-frame-location"> | ||
| ${fileName} ${functionName} ${loc} | ||
| </button>`; | ||
| } | ||
| return `<div class="stack-frame-location"> | ||
| ${fileName} ${functionName} ${loc} | ||
| </div>`; | ||
| } | ||
| /** | ||
| * Returns HTML fragment for the stack frame | ||
| */ | ||
| async #renderStackFrame(frame, index, expandAtIndex, props) { | ||
| const label = frame.type === "app" ? '<span class="frame-label">In App</span>' : ""; | ||
| const expandedClass = expandAtIndex === index ? " expanded" : ""; | ||
| const toggleButton = frame.type !== "native" && frame.source ? `<button class="stack-frame-toggle-indicator">${CHEVIRON}</button>` : ""; | ||
| return `<li class="stack-frame stack-frame-${frame.type}${expandedClass}"> | ||
| <div class="stack-frame-contents"> | ||
| ${this.#renderFrameLocation(frame, props.ide)} | ||
| <div class="stack-frame-extras"> | ||
| ${label} | ||
| ${toggleButton} | ||
| </div> | ||
| </div> | ||
| <div class="stack-frame-source"> | ||
| ${await props.sourceCodeRenderer(props.error, frame)} | ||
| </div> | ||
| </li>`; | ||
| } | ||
| /** | ||
| * Returns the ANSI output to print the stack frame on the | ||
| * terminal | ||
| */ | ||
| async #printStackFrame(frame, index, expandAtIndex, props) { | ||
| const fileName = this.#getRelativeFileName(frame.fileName); | ||
| const loc = `${fileName}:${frame.lineNumber}:${frame.columnNumber}`; | ||
| if (index === expandAtIndex) { | ||
| const functionName2 = frame.functionName ? `at ${frame.functionName} ` : ""; | ||
| const codeSnippet = await props.sourceCodeRenderer(props.error, frame); | ||
| return ` \u2043 ${functionName2}${colors.yellow(`(${loc})`)}${codeSnippet}`; | ||
| } | ||
| if (frame.type === "native") { | ||
| const functionName2 = frame.functionName ? `at ${colors.italic(frame.functionName)} ` : ""; | ||
| return colors.dim(` \u2043 ${functionName2}(${colors.italic(loc)})`); | ||
| } | ||
| const functionName = frame.functionName ? `at ${frame.functionName} ` : ""; | ||
| return ` \u2043 ${functionName}${colors.yellow(`(${loc})`)}`; | ||
| } | ||
| /** | ||
| * The toHTML method is used to output the HTML for the | ||
| * web view | ||
| */ | ||
| async toHTML(props) { | ||
| const frames = await Promise.all( | ||
| props.error.frames.map((frame, index) => { | ||
| return this.#renderStackFrame( | ||
| frame, | ||
| index, | ||
| this.#getFirstExpandedFrameIndex(props.error.frames), | ||
| props | ||
| ); | ||
| }) | ||
| ); | ||
| return `<section> | ||
| <div class="card"> | ||
| <div class="card-heading"> | ||
| <div> | ||
| <h3 class="card-title"> | ||
| Stack Trace | ||
| </h3> | ||
| </div> | ||
| </div> | ||
| <div class="card-body"> | ||
| <div id="stack-frames-wrapper"> | ||
| <div id="stack-frames-header"> | ||
| <div id="all-frames-toggle-wrapper"> | ||
| <label id="all-frames-toggle"> | ||
| <input type="checkbox" /> | ||
| <span> View All Frames </span> | ||
| </label> | ||
| </div> | ||
| <div> | ||
| <div class="toggle-switch"> | ||
| <button id="formatted-frames-toggle" class="active"> Pretty </button> | ||
| <button id="raw-frames-toggle"> Raw </button> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| <div id="stack-frames-body"> | ||
| <div id="stack-frames-formatted" class="visible"> | ||
| <ul id="stack-frames"> | ||
| ${frames.join("\n")} | ||
| </ul> | ||
| </div> | ||
| <div id="stack-frames-raw"> | ||
| ${dump(props.error.raw, { | ||
| styles: themes.cssVariables, | ||
| expand: true, | ||
| cspNonce: props.cspNonce, | ||
| inspectObjectPrototype: false, | ||
| inspectStaticMembers: false, | ||
| inspectArrayPrototype: false | ||
| })} | ||
| </div> | ||
| </div> | ||
| <div> | ||
| </div> | ||
| </div> | ||
| </section>`; | ||
| } | ||
| /** | ||
| * The toANSI method is used to output the text for the console | ||
| */ | ||
| async toANSI(props) { | ||
| const displayRaw = process.env.YOUCH_RAW; | ||
| if (displayRaw) { | ||
| const depth = Number.isNaN(Number(displayRaw)) ? 2 : Number(displayRaw); | ||
| return ` | ||
| ${colors.red("[RAW]")} | ||
| ${dumpCli(props.error.raw, { | ||
| depth, | ||
| inspectObjectPrototype: false, | ||
| inspectStaticMembers: false, | ||
| inspectArrayPrototype: false | ||
| })}`; | ||
| } | ||
| const frames = await Promise.all( | ||
| props.error.frames.map((frame, index) => { | ||
| return this.#printStackFrame( | ||
| frame, | ||
| index, | ||
| this.#getFirstExpandedFrameIndex(props.error.frames), | ||
| props | ||
| ); | ||
| }) | ||
| ); | ||
| if (frames.length) { | ||
| return ` | ||
| ${frames.join("\n")}`; | ||
| } | ||
| return ""; | ||
| } | ||
| }; | ||
| export { | ||
| ErrorStack | ||
| }; |
| import { | ||
| colors, | ||
| wordWrap | ||
| } from "./chunk-4L7RY2JA.js"; | ||
| import { | ||
| BaseComponent, | ||
| publicDirURL | ||
| } from "./chunk-PE3GG3TN.js"; | ||
| // src/templates/error_info/main.ts | ||
| var ERROR_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="24" height="24" fill="none"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 7v6m0 4.01.01-.011M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10Z"/></svg>`; | ||
| var HINT_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="24" height="24" fill="none"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m21 2-1 1M3 2l1 1m17 13-1-1M3 16l1-1m5 3h6m-5 3h4M12 3C8 3 5.952 4.95 6 8c.023 1.487.5 2.5 1.5 3.5S9 13 9 15h6c0-2 .5-2.5 1.5-3.5h0c1-1 1.477-2.013 1.5-3.5.048-3.05-2-5-6-5Z"/></svg>`; | ||
| var COPY_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M7 7m0 2.667a2.667 2.667 0 0 1 2.667 -2.667h8.666a2.667 2.667 0 0 1 2.667 2.667v8.666a2.667 2.667 0 0 1 -2.667 2.667h-8.666a2.667 2.667 0 0 1 -2.667 -2.667z" /><path d="M4.012 16.737a2.005 2.005 0 0 1 -1.012 -1.737v-10c0 -1.1 .9 -2 2 -2h10c.75 0 1.158 .385 1.5 1" /></svg>`; | ||
| function htmlAttributeEscape(value) { | ||
| return value.replace(/&/g, "&").replace(/"/g, """).replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">"); | ||
| } | ||
| var ErrorInfo = class extends BaseComponent { | ||
| cssFile = new URL("./error_info/style.css", publicDirURL); | ||
| scriptFile = new URL("./error_info/script.js", publicDirURL); | ||
| /** | ||
| * The toHTML method is used to output the HTML for the | ||
| * web view | ||
| */ | ||
| async toHTML(props) { | ||
| return `<section> | ||
| <h4 id="error-name">${props.error.name}</h4> | ||
| <h1 id="error-title">${props.title}</h1> | ||
| </section> | ||
| <section> | ||
| <div class="card"> | ||
| <div class="card-body"> | ||
| <h2 id="error-message"> | ||
| <span>${ERROR_ICON_SVG}</span> | ||
| <span>${props.error.message}</span> | ||
| <button | ||
| id="copy-error-btn" | ||
| data-error-text="${htmlAttributeEscape(`${props.error.name}: ${props.error.message}`)}" | ||
| onclick="copyErrorMessage(this)" | ||
| title="Copy error message" | ||
| aria-label="Copy error message to clipboard" | ||
| > | ||
| ${COPY_ICON_SVG} | ||
| </button> | ||
| </h2> | ||
| ${props.error.hint ? `<div id="error-hint"> | ||
| <span>${HINT_ICON_SVG}</span> | ||
| <span>${props.error.hint}</span> | ||
| </div>` : ""} | ||
| </div> | ||
| </div> | ||
| </section>`; | ||
| } | ||
| /** | ||
| * The toANSI method is used to output the text for the console | ||
| */ | ||
| async toANSI(props) { | ||
| const errorMessage = colors.red( | ||
| `\u2139 ${wordWrap(`${props.error.name}: ${props.error.message}`, { | ||
| width: process.stdout.columns, | ||
| indent: " ", | ||
| newLine: "\n", | ||
| escape: (value) => value | ||
| })}` | ||
| ); | ||
| const hint = props.error.hint ? ` | ||
| ${colors.blue("\u25C9")} ${colors.dim().italic( | ||
| wordWrap(props.error.hint.replace(/(<([^>]+)>)/gi, ""), { | ||
| width: process.stdout.columns, | ||
| indent: " ", | ||
| newLine: "\n", | ||
| escape: (value) => value | ||
| }) | ||
| )}` : ""; | ||
| return `${errorMessage}${hint}`; | ||
| } | ||
| }; | ||
| export { | ||
| ErrorInfo | ||
| }; |
| import { | ||
| BaseComponent, | ||
| publicDirURL | ||
| } from "./chunk-PE3GG3TN.js"; | ||
| // src/templates/error_metadata/main.ts | ||
| import { dump, themes } from "@poppinss/dumper/html"; | ||
| var ErrorMetadata = class extends BaseComponent { | ||
| cssFile = new URL("./error_metadata/style.css", publicDirURL); | ||
| #primitives = ["string", "boolean", "number", "undefined"]; | ||
| /** | ||
| * Formats the error row value | ||
| */ | ||
| #formatRowValue(value, dumpValue, cspNonce) { | ||
| if (dumpValue === true) { | ||
| return dump(value, { styles: themes.cssVariables, cspNonce }); | ||
| } | ||
| if (this.#primitives.includes(typeof value) || value === null) { | ||
| return value; | ||
| } | ||
| return dump(value, { styles: themes.cssVariables, cspNonce }); | ||
| } | ||
| /** | ||
| * Returns HTML fragment with HTML table containing rows | ||
| * metadata section rows | ||
| */ | ||
| #renderRows(rows, cspNonce) { | ||
| return `<table class="card-table"> | ||
| <tbody> | ||
| ${rows.map((row) => { | ||
| return `<tr> | ||
| <td class="table-key">${row.key}</td> | ||
| <td class="table-value"> | ||
| ${this.#formatRowValue(row.value, row.dump, cspNonce)} | ||
| </td> | ||
| </tr>`; | ||
| }).join("\n")} | ||
| </tbody> | ||
| </table>`; | ||
| } | ||
| /** | ||
| * Renders each section with its rows inside a table | ||
| */ | ||
| #renderSection(section, rows, cspNonce) { | ||
| return `<div> | ||
| <h4 class="card-subtitle">${section}</h4> | ||
| ${Array.isArray(rows) ? this.#renderRows(rows, cspNonce) : `<span>${this.#formatRowValue(rows.value, rows.dump, cspNonce)}</span>`} | ||
| </div>`; | ||
| } | ||
| /** | ||
| * Renders each group as a card | ||
| */ | ||
| #renderGroup(group, sections, cspNonce) { | ||
| return `<section class="metadata-group"> | ||
| <div class="card"> | ||
| <div class="card-heading"> | ||
| <h3 class="card-title">${group}</h3> | ||
| </div> | ||
| <div class="card-body"> | ||
| ${Object.keys(sections).map((section) => this.#renderSection(section, sections[section], cspNonce)).join("\n")} | ||
| </div> | ||
| </div> | ||
| </section>`; | ||
| } | ||
| /** | ||
| * The toHTML method is used to output the HTML for the | ||
| * web view | ||
| */ | ||
| async toHTML(props) { | ||
| const groups = props.metadata.toJSON(); | ||
| const groupsNames = Object.keys(groups); | ||
| if (!groupsNames.length) { | ||
| return ""; | ||
| } | ||
| return groupsNames.map((group) => this.#renderGroup(group, groups[group], props.cspNonce)).join("\n"); | ||
| } | ||
| /** | ||
| * The toANSI method is used to output the text for the console | ||
| */ | ||
| async toANSI() { | ||
| return ""; | ||
| } | ||
| }; | ||
| export { | ||
| ErrorMetadata | ||
| }; |
| // src/component.ts | ||
| import { readFile } from "fs/promises"; | ||
| var BaseComponent = class { | ||
| #cachedStyles; | ||
| #cachedScript; | ||
| /** | ||
| * A flag to know if we are in dev mode or not. In dev mode, | ||
| * the styles and scripts are refetched from the disk. | ||
| * Otherwise they are cached. | ||
| */ | ||
| #inDevMode; | ||
| /** | ||
| * Absolute path to the frontend JavaScript that should be | ||
| * injected within the HTML head. The JavaScript does not | ||
| * get transpiled, hence it should work cross browser by | ||
| * default. | ||
| */ | ||
| scriptFile; | ||
| /** | ||
| * Absolute path to the CSS file that should be injected | ||
| * within the HTML head. | ||
| */ | ||
| cssFile; | ||
| constructor(devMode) { | ||
| this.#inDevMode = devMode; | ||
| } | ||
| /** | ||
| * Returns the styles for the component. The null value | ||
| * is not returned if no styles are associated with | ||
| * the component | ||
| */ | ||
| async getStyles() { | ||
| if (!this.cssFile) { | ||
| return null; | ||
| } | ||
| if (this.#inDevMode) { | ||
| return await readFile(this.cssFile, "utf-8"); | ||
| } | ||
| this.#cachedStyles = this.#cachedStyles ?? await readFile(this.cssFile, "utf-8"); | ||
| return this.#cachedStyles; | ||
| } | ||
| /** | ||
| * Returns the frontend script for the component. The null | ||
| * value is not returned if no styles are associated | ||
| * with the component | ||
| */ | ||
| async getScript() { | ||
| if (!this.scriptFile) { | ||
| return null; | ||
| } | ||
| if (this.#inDevMode) { | ||
| return await readFile(this.scriptFile, "utf-8"); | ||
| } | ||
| this.#cachedScript = this.#cachedScript ?? await readFile(this.scriptFile, "utf-8"); | ||
| return this.#cachedScript; | ||
| } | ||
| }; | ||
| // src/public_dir.ts | ||
| var publicDirURL = new URL("./public/", import.meta.url); | ||
| export { | ||
| BaseComponent, | ||
| publicDirURL | ||
| }; |
| import { | ||
| colors | ||
| } from "./chunk-4L7RY2JA.js"; | ||
| import { | ||
| BaseComponent, | ||
| publicDirURL | ||
| } from "./chunk-PE3GG3TN.js"; | ||
| // src/templates/error_cause/main.ts | ||
| import { dump, themes } from "@poppinss/dumper/html"; | ||
| import { dump as dumpCli } from "@poppinss/dumper/console"; | ||
| var ErrorCause = class extends BaseComponent { | ||
| cssFile = new URL("./error_cause/style.css", publicDirURL); | ||
| /** | ||
| * The toHTML method is used to output the HTML for the | ||
| * web view | ||
| */ | ||
| async toHTML(props) { | ||
| if (!props.error.cause) { | ||
| return ""; | ||
| } | ||
| return `<section> | ||
| <div class="card"> | ||
| <div class="card-heading"> | ||
| <div> | ||
| <h3 class="card-title"> | ||
| Error Cause | ||
| </h3> | ||
| </div> | ||
| </div> | ||
| <div class="card-body"> | ||
| <div id="error-cause"> | ||
| ${dump(props.error.cause, { | ||
| cspNonce: props.cspNonce, | ||
| styles: themes.cssVariables, | ||
| inspectObjectPrototype: false, | ||
| inspectStaticMembers: false, | ||
| inspectArrayPrototype: false | ||
| })} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </section>`; | ||
| } | ||
| /** | ||
| * The toANSI method is used to output the text for the console | ||
| */ | ||
| async toANSI(props) { | ||
| if (!props.error.cause) { | ||
| return ""; | ||
| } | ||
| let depth = process.env.YOUCH_CAUSE ? Number(process.env.YOUCH_CAUSE) : 2; | ||
| if (Number.isNaN(depth)) { | ||
| depth = 2; | ||
| } | ||
| return ` | ||
| ${colors.red("[CAUSE]")} | ||
| ${dumpCli(props.error.cause, { | ||
| depth, | ||
| inspectObjectPrototype: false, | ||
| inspectStaticMembers: false, | ||
| inspectArrayPrototype: false | ||
| })}`; | ||
| } | ||
| }; | ||
| export { | ||
| ErrorCause | ||
| }; |
| import "#nitro/virtual/polyfills"; | ||
| export {}; |
| import "#nitro/virtual/polyfills"; | ||
| declare const _default: {}; | ||
| export default _default; |
| import "#nitro/virtual/polyfills"; | ||
| export {}; |
| import "#nitro/virtual/polyfills"; | ||
| export {}; |
| import type { APIGatewayProxyEvent, APIGatewayProxyEventV2 } from "aws-lambda"; | ||
| import type { ServerRequest } from "srvx"; | ||
| export declare function awsRequest(event: APIGatewayProxyEvent | APIGatewayProxyEventV2, context: unknown): ServerRequest; | ||
| export declare function awsResponseHeaders(response: Response); | ||
| export declare function awsResponseBody(response: Response): Promise<{ | ||
| body: string; | ||
| isBase64Encoded?: boolean; | ||
| }>; |
| import "#nitro/virtual/polyfills"; | ||
| export declare const handler: unknown; |
| import "#nitro/virtual/polyfills"; | ||
| import type { APIGatewayProxyEvent, APIGatewayProxyEventV2, APIGatewayProxyResult, APIGatewayProxyResultV2, Context } from "aws-lambda"; | ||
| export declare function handler(event: APIGatewayProxyEvent | APIGatewayProxyEventV2, context: Context): Promise<APIGatewayProxyResult | APIGatewayProxyResultV2>; |
| import type { Cookie } from "@azure/functions"; | ||
| export declare function getAzureParsedCookiesFromHeaders(headers: Headers): Cookie[]; |
| import "#nitro/virtual/polyfills"; | ||
| import type { HttpRequest, HttpResponse } from "@azure/functions"; | ||
| export declare function handle(context: { | ||
| res: HttpResponse; | ||
| }, req: HttpRequest); |
| import "#nitro/virtual/polyfills"; | ||
| declare const _default: {}; | ||
| export default _default; |
| import "#nitro/virtual/polyfills"; | ||
| import type * as CF from "@cloudflare/workers-types"; | ||
| import type { ServerRuntimeContext } from "srvx"; | ||
| type MaybePromise<T> = T | Promise<T>; | ||
| export declare function createHandler<Env>(hooks: { | ||
| fetch: (...params: [...Parameters<NonNullable<ExportedHandler<Env>["fetch"]>>, url: URL, cfContextExtras: any]) => MaybePromise<Response | CF.Response | undefined>; | ||
| }): { | ||
| fetch(request, env, context); | ||
| scheduled(controller, env, context); | ||
| email(message, env, context); | ||
| queue(batch, env, context); | ||
| tail(traces, env, context); | ||
| trace(traces, env, context); | ||
| }; | ||
| export declare function augmentReq(cfReq: Request | CF.Request, ctx: NonNullable<ServerRuntimeContext["cloudflare"]>); | ||
| export {}; |
| import "#nitro/virtual/polyfills"; | ||
| import { DurableObject } from "cloudflare:workers"; | ||
| declare const _default; | ||
| export default _default; | ||
| export declare class $DurableObject extends DurableObject { | ||
| constructor(state: DurableObjectState, env: Record<string, any>); | ||
| fetch(request: Request); | ||
| alarm(): void | Promise<void>; | ||
| webSocketMessage(client: WebSocket, message: ArrayBuffer | string); | ||
| webSocketClose(client: WebSocket, code: number, reason: string, wasClean: boolean); | ||
| } |
| import "#nitro/virtual/polyfills"; | ||
| declare const _default; | ||
| export default _default; |
| import "#nitro/virtual/polyfills"; | ||
| import type { Request as CFRequest, EventContext, ExecutionContext } from "@cloudflare/workers-types"; | ||
| /** | ||
| * Reference: https://developers.cloudflare.com/workers/runtime-apis/fetch-event/#parameters | ||
| */ | ||
| interface CFPagesEnv { | ||
| ASSETS: { | ||
| fetch: (request: CFRequest) => Promise<Response>; | ||
| }; | ||
| CF_PAGES: "1"; | ||
| CF_PAGES_BRANCH: string; | ||
| CF_PAGES_COMMIT_SHA: string; | ||
| CF_PAGES_URL: string; | ||
| [key: string]: any; | ||
| } | ||
| declare const _default: { | ||
| fetch(cfReq: CFRequest, env: CFPagesEnv, context: EventContext<CFPagesEnv, string, any>); | ||
| scheduled(event: any, env: CFPagesEnv, context: ExecutionContext); | ||
| }; | ||
| export default _default; |
| import type { NitroAppPlugin } from "nitro/types"; | ||
| declare const cloudflareDevPlugin: NitroAppPlugin; | ||
| export default cloudflareDevPlugin; |
| import "#nitro/virtual/polyfills"; | ||
| import type { Deno as _Deno } from "@deno/types"; | ||
| declare global { | ||
| var Deno: typeof _Deno; | ||
| } |
| import "#nitro/virtual/polyfills"; | ||
| declare const _default: {}; | ||
| export default _default; |
| import "#nitro/virtual/polyfills"; | ||
| import type { Context } from "@netlify/edge-functions"; | ||
| export default function netlifyEdge(netlifyReq: Request, context: Context); |
| import "#nitro/virtual/polyfills"; | ||
| import type { ServerRequest } from "srvx"; | ||
| declare const handler: (req: ServerRequest) => Promise<Response>; | ||
| export default handler; |
| import "#nitro/virtual/polyfills"; | ||
| declare const _default: {}; | ||
| export default _default; |
| import "#nitro/virtual/polyfills"; | ||
| export declare const middleware: unknown; | ||
| export declare const handleUpgrade: unknown; |
| import "#nitro/virtual/polyfills"; | ||
| declare const _default: {}; | ||
| export default _default; |
| import "#nitro/virtual/polyfills"; | ||
| declare const _default: {}; | ||
| export default _default; |
| import "#nitro/virtual/polyfills"; | ||
| import type { Handler } from "aws-lambda"; | ||
| type StormkitEvent = { | ||
| url: string; | ||
| path: string; | ||
| method: string; | ||
| body?: string; | ||
| query?: Record<string, Array<string>>; | ||
| headers?: Record<string, string>; | ||
| rawHeaders?: Array<string>; | ||
| }; | ||
| type StormkitResponse = { | ||
| headers?: Record<string, string>; | ||
| body?: string; | ||
| buffer?: string; | ||
| statusCode: number; | ||
| errorMessage?: string; | ||
| errorStack?: string; | ||
| }; | ||
| export declare const handler: Handler<StormkitEvent, StormkitResponse>; | ||
| export {}; |
| export declare const ISR_URL_PARAM = "__isr_route"; | ||
| export declare function isrRouteRewrite(reqUrl: string, xNowRouteMatches: string | null): [pathname: string, search: string] | undefined; |
| import "#nitro/virtual/polyfills"; | ||
| import type { NodeServerRequest, NodeServerResponse } from "srvx"; | ||
| export default function nodeHandler(req: NodeServerRequest, res: NodeServerResponse); |
| import "#nitro/virtual/polyfills"; | ||
| import type { ServerRequest } from "srvx"; | ||
| declare const _default: { | ||
| fetch(req: ServerRequest, context: { | ||
| waitUntil: (promise: Promise<any>) => void; | ||
| }); | ||
| }; | ||
| export default _default; |
| import "#nitro/virtual/polyfills"; | ||
| export {}; |
| import "#nitro/virtual/polyfills"; | ||
| declare const _default; | ||
| export default _default; |
| import { parentPort, threadId, workerData } from "node:worker_threads"; | ||
| import { Agent } from "undici"; | ||
| import { ModuleRunner, ESModulesEvaluator } from "vite/module-runner"; | ||
| import { getSocketAddress, isSocketSupported } from "get-port-please"; | ||
| // ----- Environment runners ----- | ||
| const envs = (globalThis.__nitro_vite_envs__ ??= { | ||
| nitro: undefined, | ||
| ssr: undefined, | ||
| }); | ||
| class EnvRunner { | ||
| constructor({ name, entry }) { | ||
| this.name = name; | ||
| this.entryPath = entry; | ||
| this.entry = undefined; | ||
| this.entryError = undefined; | ||
| // Create Vite Module Runner | ||
| // https://vite.dev/guide/api-environment-runtimes.html#modulerunner | ||
| this.runnerHooks = {}; | ||
| this.runner = new ModuleRunner( | ||
| { | ||
| transport: { | ||
| connect({ onMessage, onDisconnection }) { | ||
| parentPort.on("message", (payload) => { | ||
| if (payload?.type === "custom" && payload.viteEnv === name) { | ||
| onMessage(payload); | ||
| } | ||
| }); | ||
| parentPort.on("close", onDisconnection); | ||
| }, | ||
| send(payload) { | ||
| parentPort.postMessage({ ...payload, viteEnv: name }); | ||
| }, | ||
| }, | ||
| }, | ||
| new ESModulesEvaluator(), | ||
| process.env.NITRO_DEBUG ? console.debug : undefined | ||
| ); | ||
| this.reload(); | ||
| } | ||
| async reload() { | ||
| try { | ||
| this.entry = await this.runner.import(this.entryPath); | ||
| this.entryError = undefined; | ||
| } catch (error) { | ||
| console.error(error); | ||
| this.entryError = error; | ||
| } | ||
| } | ||
| async fetch(req, init) { | ||
| if (this.entryError) { | ||
| return renderError(req, this.entryError); | ||
| } | ||
| for (let i = 0; i < 5 && !(this.entry || this.entryError); i++) { | ||
| await new Promise((r) => setTimeout(r, 100 * Math.pow(2, i))); | ||
| } | ||
| if (this.entryError) { | ||
| return renderError(req, this.entryError); | ||
| } | ||
| if (!this.entry) { | ||
| throw httpError(503, `Vite environment "${this.name}" is unavailable`); | ||
| } | ||
| try { | ||
| const entryFetch = this.entry.fetch || this.entry.default?.fetch; | ||
| if (!entryFetch) { | ||
| throw httpError( | ||
| 500, | ||
| `No fetch handler exported from ${this.entryPath}` | ||
| ); | ||
| } | ||
| return await entryFetch(req, init); | ||
| } catch (error) { | ||
| return renderError(req, error); | ||
| } | ||
| } | ||
| } | ||
| // ----- RPC listeners ----- | ||
| const viteHostRequests = new Map(); | ||
| async function requestToViteHost( | ||
| name, | ||
| data, | ||
| id = Math.random().toString(16).slice(2), | ||
| timeout = 3000 | ||
| ) { | ||
| setTimeout(() => { | ||
| if (viteHostRequests.has(id)) { | ||
| viteHostRequests.delete(id); | ||
| reject(new Error(`Request to vite host timed out (${name}:${id})`)); | ||
| } | ||
| }, timeout); | ||
| let resolve, reject; | ||
| const promise = new Promise((_resolve, _reject) => { | ||
| resolve = (value) => { | ||
| viteHostRequests.delete(id); | ||
| return _resolve(value); | ||
| }; | ||
| reject = (err) => { | ||
| viteHostRequests.delete(id); | ||
| return _reject(err); | ||
| }; | ||
| }); | ||
| viteHostRequests.set(id, { resolve, reject }); | ||
| parentPort.postMessage({ | ||
| type: "custom", | ||
| event: "nitro:vite-invoke", | ||
| data: { name, id, data }, | ||
| }); | ||
| return promise; | ||
| } | ||
| parentPort.on("message", (payload) => { | ||
| if (payload?.type !== "custom") { | ||
| return; | ||
| } | ||
| switch (payload.event) { | ||
| case "nitro:vite-server-addr": { | ||
| viteServerAddr = payload.data; | ||
| break; | ||
| } | ||
| case "nitro:vite-env": { | ||
| const { name, entry } = payload.data; | ||
| if (envs[name]) { | ||
| console.error(`Vite environment "${name}" already registered!`); | ||
| } else { | ||
| envs[name] = new EnvRunner({ name, entry }); | ||
| } | ||
| break; | ||
| } | ||
| case "nitro:vite-invoke-response": { | ||
| const { id, data: response } = payload.data; | ||
| const req = viteHostRequests.get(id); | ||
| if (req) { | ||
| if (response.error) { | ||
| req.reject(response.error); | ||
| } else { | ||
| req.resolve(response.data); | ||
| } | ||
| } | ||
| break; | ||
| } | ||
| } | ||
| }); | ||
| // Trap unhandled errors to avoid worker crash | ||
| process.on("unhandledRejection", (error) => console.error(error)); | ||
| process.on("uncaughtException", (error) => console.error(error)); | ||
| // ----- RSC Support ----- | ||
| // define __VITE_ENVIRONMENT_RUNNER_IMPORT__ for RSC support | ||
| // https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-rsc/README.md#__vite_environment_runner_import__ | ||
| globalThis.__VITE_ENVIRONMENT_RUNNER_IMPORT__ = async function ( | ||
| environmentName, | ||
| id | ||
| ) { | ||
| const env = envs[environmentName]; | ||
| if (!env) { | ||
| throw new Error(`Vite environment "${environmentName}" is not registered`); | ||
| } | ||
| return env.runner.import(id); | ||
| }; | ||
| // ----- Server ----- | ||
| async function reload() { | ||
| try { | ||
| await Promise.all(Object.values(envs).map((env) => env?.reload())); | ||
| } catch (error) { | ||
| console.error(error); | ||
| } | ||
| } | ||
| // eslint-disable-next-line unicorn/prefer-top-level-await | ||
| reload(); | ||
| if (workerData.server) { | ||
| const { createServer } = await import("node:http"); | ||
| const { toNodeHandler } = await import("srvx/node"); | ||
| const server = createServer( | ||
| toNodeHandler(async (req, init) => { | ||
| const viteEnv = | ||
| init?.viteEnv || req?.headers.get("x-vite-env") || "nitro"; // TODO | ||
| const env = envs[viteEnv]; | ||
| if (!env) { | ||
| return renderError( | ||
| req, | ||
| httpError(500, `Unknown vite environment "${viteEnv}"`) | ||
| ); | ||
| } | ||
| return env.fetch(req, init); | ||
| }) | ||
| ); | ||
| server.on("upgrade", (req, socket, head) => { | ||
| const handleUpgrade = envs["nitro"]?.entry?.handleUpgrade; | ||
| handleUpgrade?.(req, socket, head); | ||
| }); | ||
| parentPort.on("message", async (message) => { | ||
| if (message?.type === "full-reload") { | ||
| await reload(); | ||
| } else if (message?.event === "shutdown") { | ||
| server.close(() => { | ||
| parentPort.postMessage({ event: "exit" }); | ||
| }); | ||
| } | ||
| }); | ||
| await listen(server); | ||
| const address = server.address(); | ||
| parentPort?.postMessage({ | ||
| event: "listen", | ||
| address: | ||
| typeof address === "string" | ||
| ? { socketPath: address } | ||
| : { host: "localhost", port: address?.port }, | ||
| }); | ||
| } | ||
| // ----- HTML Transform ----- | ||
| globalThis.__transform_html__ = async function (html) { | ||
| html = await requestToViteHost("transformHTML", html).catch((error) => { | ||
| console.warn("Failed to transform HTML via Vite:", error); | ||
| return html; | ||
| }); | ||
| return html; | ||
| }; | ||
| // ----- Error handling ----- | ||
| function httpError(status, message) { | ||
| const error = new Error(message || `HTTP Error ${status}`); | ||
| error.status = status; | ||
| error.name = "NitroViteError"; | ||
| return error; | ||
| } | ||
| async function renderError(req, error) { | ||
| if (req.headers.get("accept")?.includes("application/json")) { | ||
| return new Response( | ||
| JSON.stringify( | ||
| { | ||
| status: error.status || 500, | ||
| name: error.name || "Error", | ||
| message: error.message, | ||
| stack: (error.stack || "") | ||
| .split("\n") | ||
| .splice(1) | ||
| .map((l) => l.trim()), | ||
| }, | ||
| null, | ||
| 2 | ||
| ), | ||
| { | ||
| status: error.status || 500, | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| "Cache-Control": "no-store, max-age=0, must-revalidate", | ||
| Pragma: "no-cache", | ||
| Expires: "0", | ||
| }, | ||
| } | ||
| ); | ||
| } | ||
| const { Youch } = await import("youch"); | ||
| const youch = new Youch(); | ||
| return new Response(await youch.toHTML(error), { | ||
| status: error.status || 500, | ||
| headers: { | ||
| "Content-Type": "text/html", | ||
| "Cache-Control": "no-store, max-age=0, must-revalidate", | ||
| Pragma: "no-cache", | ||
| Expires: "0", | ||
| }, | ||
| }); | ||
| } | ||
| // ----- Internal Utils ----- | ||
| async function listen(server) { | ||
| const listenAddr = (await isSocketSupported()) | ||
| ? getSocketAddress({ | ||
| name: `nitro-vite-${threadId}`, | ||
| pid: true, | ||
| random: true, | ||
| }) | ||
| : { port: 0, host: "localhost" }; | ||
| return new Promise((resolve, reject) => { | ||
| try { | ||
| server.listen(listenAddr, () => resolve()); | ||
| } catch (error) { | ||
| reject(error); | ||
| } | ||
| }); | ||
| } | ||
| function fetchAddress(addr, input, inputInit) { | ||
| let url; | ||
| let init; | ||
| if (input instanceof Request) { | ||
| url = new URL(input.url); | ||
| init = { | ||
| method: input.method, | ||
| headers: input.headers, | ||
| body: input.body, | ||
| ...inputInit, | ||
| }; | ||
| } else { | ||
| url = new URL(input); | ||
| init = inputInit; | ||
| } | ||
| init = { | ||
| duplex: "half", | ||
| redirect: "manual", | ||
| ...init, | ||
| }; | ||
| if (addr.socketPath) { | ||
| url.protocol = "http:"; | ||
| return fetch(url, { | ||
| ...init, | ||
| ...fetchSocketOptions(addr.socketPath), | ||
| }); | ||
| } | ||
| const origin = `http://${addr.host}${addr.port ? `:${addr.port}` : ""}`; | ||
| const outURL = new URL(url.pathname + url.search, origin); | ||
| return fetch(outURL, init); | ||
| } | ||
| function fetchSocketOptions(socketPath) { | ||
| if ("Bun" in globalThis) { | ||
| // https://bun.sh/guides/http/fetch-unix | ||
| return { unix: socketPath }; | ||
| } | ||
| if ("Deno" in globalThis) { | ||
| // https://github.com/denoland/deno/pull/29154 | ||
| return { | ||
| client: Deno.createHttpClient({ | ||
| // @ts-expect-error Missing types? | ||
| transport: "unix", | ||
| path: socketPath, | ||
| }), | ||
| }; | ||
| } | ||
| // https://github.com/nodejs/undici/issues/2970 | ||
| return { | ||
| dispatcher: new Agent({ connect: { socketPath } }), | ||
| }; | ||
| } |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Network access
Supply chain riskThis module accesses the network.
Found 3 instances
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 2 instances
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
High entropy strings
Supply chain riskContains high entropy strings. This could be a sign of encrypted data, leaked secrets or obfuscated code.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Network access
Supply chain riskThis module accesses the network.
Found 2 instances
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 14 instances
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 2 instances
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
High entropy strings
Supply chain riskContains high entropy strings. This could be a sign of encrypted data, leaked secrets or obfuscated code.
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
2929265
16.55%369
21.78%4
-20%151
-13.71%108
-1.82%21
16.67%90
1.12%61630
-0.86%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
Updated
Updated
Updated
Updated
Updated