diff --git a/docs/2.deploy/20.providers/edgeone.md b/docs/2.deploy/20.providers/edgeone.md new file mode 100644 index 0000000000..2984f49021 --- /dev/null +++ b/docs/2.deploy/20.providers/edgeone.md @@ -0,0 +1,22 @@ +# EdgeOne + +> Deploy Nitro apps to EdgeOne. + +**Preset:** `edgeone-pages` + +:read-more{to="https://pages.edgeone.ai/"} + + +## Using the control panel + +1. In the [EdgeOne pages control panel](https://console.tencentcloud.com/edgeone/pages), click **Create project**. +2. Choose **Import Git repository** as your deployment method. We support deployment on GitHub, GitLab, Gitee, and CNB. +3. Choose the GitHub **repository** and **branch** containing your application code. +4. Complete your project setup. +5. Click the **Deploy** button. + +## Using the EdgeOne CLI + +You can also install the Pages scaffolding tool. For detailed installation and usage, refer to [EdgeOne CLI](https://pages.edgeone.ai/document/edgeone-cli). + +Once configured, use the edgeone pages deploy command to deploy the project. During deployment, the CLI will first automatically build the project, then upload and publish the build artifacts. diff --git a/src/presets/_all.gen.ts b/src/presets/_all.gen.ts index 0aabd48fde..352e150408 100644 --- a/src/presets/_all.gen.ts +++ b/src/presets/_all.gen.ts @@ -11,6 +11,7 @@ import _cleavr from "./cleavr/preset.ts"; import _cloudflare from "./cloudflare/preset.ts"; import _deno from "./deno/preset.ts"; import _digitalocean from "./digitalocean/preset.ts"; +import _edgeone from "./edgeone/preset.ts"; import _firebase from "./firebase/preset.ts"; import _flightcontrol from "./flightcontrol/preset.ts"; import _genezio from "./genezio/preset.ts"; @@ -40,6 +41,7 @@ export default [ ..._cloudflare, ..._deno, ..._digitalocean, + ..._edgeone, ..._firebase, ..._flightcontrol, ..._genezio, diff --git a/src/presets/_types.gen.ts b/src/presets/_types.gen.ts index 974925a21a..7ab4e53265 100644 --- a/src/presets/_types.gen.ts +++ b/src/presets/_types.gen.ts @@ -20,6 +20,6 @@ export interface PresetOptions { export const presetsWithConfig = ["awsAmplify","awsLambda","azure","cloudflare","firebase","netlify","vercel"] as const; -export type PresetName = "alwaysdata" | "aws-amplify" | "aws-lambda" | "azure-swa" | "base-worker" | "bun" | "cleavr" | "cloudflare-dev" | "cloudflare-durable" | "cloudflare-module" | "cloudflare-pages" | "cloudflare-pages-static" | "deno" | "deno-deploy" | "deno-server" | "digital-ocean" | "firebase-app-hosting" | "flight-control" | "genezio" | "github-pages" | "gitlab-pages" | "heroku" | "iis-handler" | "iis-node" | "koyeb" | "netlify" | "netlify-edge" | "netlify-static" | "nitro-dev" | "nitro-prerender" | "node" | "node-cluster" | "node-middleware" | "node-server" | "platform-sh" | "render-com" | "standard" | "static" | "stormkit" | "vercel" | "vercel-static" | "winterjs" | "zeabur" | "zeabur-static" | "zerops" | "zerops-static"; +export type PresetName = "alwaysdata" | "aws-amplify" | "aws-lambda" | "azure-swa" | "base-worker" | "bun" | "cleavr" | "cloudflare-dev" | "cloudflare-durable" | "cloudflare-module" | "cloudflare-pages" | "cloudflare-pages-static" | "deno" | "deno-deploy" | "deno-server" | "digital-ocean" | "edgeone-pages" | "firebase-app-hosting" | "flight-control" | "genezio" | "github-pages" | "gitlab-pages" | "heroku" | "iis-handler" | "iis-node" | "koyeb" | "netlify" | "netlify-edge" | "netlify-static" | "nitro-dev" | "nitro-prerender" | "node" | "node-cluster" | "node-middleware" | "node-server" | "platform-sh" | "render-com" | "standard" | "static" | "stormkit" | "vercel" | "vercel-static" | "winterjs" | "zeabur" | "zeabur-static" | "zerops" | "zerops-static"; -export type PresetNameInput = "alwaysdata" | "aws-amplify" | "awsAmplify" | "aws_amplify" | "aws-lambda" | "awsLambda" | "aws_lambda" | "azure-swa" | "azureSwa" | "azure_swa" | "base-worker" | "baseWorker" | "base_worker" | "bun" | "cleavr" | "cloudflare-dev" | "cloudflareDev" | "cloudflare_dev" | "cloudflare-durable" | "cloudflareDurable" | "cloudflare_durable" | "cloudflare-module" | "cloudflareModule" | "cloudflare_module" | "cloudflare-pages" | "cloudflarePages" | "cloudflare_pages" | "cloudflare-pages-static" | "cloudflarePagesStatic" | "cloudflare_pages_static" | "deno" | "deno-deploy" | "denoDeploy" | "deno_deploy" | "deno-server" | "denoServer" | "deno_server" | "digital-ocean" | "digitalOcean" | "digital_ocean" | "firebase-app-hosting" | "firebaseAppHosting" | "firebase_app_hosting" | "flight-control" | "flightControl" | "flight_control" | "genezio" | "github-pages" | "githubPages" | "github_pages" | "gitlab-pages" | "gitlabPages" | "gitlab_pages" | "heroku" | "iis-handler" | "iisHandler" | "iis_handler" | "iis-node" | "iisNode" | "iis_node" | "koyeb" | "netlify" | "netlify-edge" | "netlifyEdge" | "netlify_edge" | "netlify-static" | "netlifyStatic" | "netlify_static" | "nitro-dev" | "nitroDev" | "nitro_dev" | "nitro-prerender" | "nitroPrerender" | "nitro_prerender" | "node" | "node-cluster" | "nodeCluster" | "node_cluster" | "node-middleware" | "nodeMiddleware" | "node_middleware" | "node-server" | "nodeServer" | "node_server" | "platform-sh" | "platformSh" | "platform_sh" | "render-com" | "renderCom" | "render_com" | "standard" | "static" | "stormkit" | "vercel" | "vercel-static" | "vercelStatic" | "vercel_static" | "winterjs" | "zeabur" | "zeabur-static" | "zeaburStatic" | "zeabur_static" | "zerops" | "zerops-static" | "zeropsStatic" | "zerops_static" | (string & {}); +export type PresetNameInput = "alwaysdata" | "aws-amplify" | "awsAmplify" | "aws_amplify" | "aws-lambda" | "awsLambda" | "aws_lambda" | "azure-swa" | "azureSwa" | "azure_swa" | "base-worker" | "baseWorker" | "base_worker" | "bun" | "cleavr" | "cloudflare-dev" | "cloudflareDev" | "cloudflare_dev" | "cloudflare-durable" | "cloudflareDurable" | "cloudflare_durable" | "cloudflare-module" | "cloudflareModule" | "cloudflare_module" | "cloudflare-pages" | "cloudflarePages" | "cloudflare_pages" | "cloudflare-pages-static" | "cloudflarePagesStatic" | "cloudflare_pages_static" | "deno" | "deno-deploy" | "denoDeploy" | "deno_deploy" | "deno-server" | "denoServer" | "deno_server" | "digital-ocean" | "digitalOcean" | "digital_ocean" | "edgeone-pages" | "edgeonePages" | "edgeone_pages" | "firebase-app-hosting" | "firebaseAppHosting" | "firebase_app_hosting" | "flight-control" | "flightControl" | "flight_control" | "genezio" | "github-pages" | "githubPages" | "github_pages" | "gitlab-pages" | "gitlabPages" | "gitlab_pages" | "heroku" | "iis-handler" | "iisHandler" | "iis_handler" | "iis-node" | "iisNode" | "iis_node" | "koyeb" | "netlify" | "netlify-edge" | "netlifyEdge" | "netlify_edge" | "netlify-static" | "netlifyStatic" | "netlify_static" | "nitro-dev" | "nitroDev" | "nitro_dev" | "nitro-prerender" | "nitroPrerender" | "nitro_prerender" | "node" | "node-cluster" | "nodeCluster" | "node_cluster" | "node-middleware" | "nodeMiddleware" | "node_middleware" | "node-server" | "nodeServer" | "node_server" | "platform-sh" | "platformSh" | "platform_sh" | "render-com" | "renderCom" | "render_com" | "standard" | "static" | "stormkit" | "vercel" | "vercel-static" | "vercelStatic" | "vercel_static" | "winterjs" | "zeabur" | "zeabur-static" | "zeaburStatic" | "zeabur_static" | "zerops" | "zerops-static" | "zeropsStatic" | "zerops_static" | (string & {}); diff --git a/src/presets/edgeone/preset.ts b/src/presets/edgeone/preset.ts new file mode 100644 index 0000000000..209057a5e3 --- /dev/null +++ b/src/presets/edgeone/preset.ts @@ -0,0 +1,31 @@ +import { defineNitroPreset } from "../_utils/preset.ts"; +import { writeEdgeOneRoutes } from "./utils.ts"; +import type { Nitro } from "nitro/types"; + +const edgeone = defineNitroPreset( + { + entry: "./edgeone/runtime/edgeone", + extends: "node-server", + serveStatic: true, + output: { + dir: "{{ rootDir }}/.edgeone", + serverDir: "{{ output.dir }}/server-handler", + publicDir: "{{ output.dir }}/assets", + }, + rollupConfig: { + output: { + entryFileNames: "handler.js", + }, + }, + hooks: { + async compiled(nitro: Nitro) { + await writeEdgeOneRoutes(nitro); + }, + }, + }, + { + name: "edgeone-pages" as const, + } +); + +export default [edgeone] as const; diff --git a/src/presets/edgeone/runtime/edgeone.ts b/src/presets/edgeone/runtime/edgeone.ts new file mode 100644 index 0000000000..63b0373a7d --- /dev/null +++ b/src/presets/edgeone/runtime/edgeone.ts @@ -0,0 +1,20 @@ +import "#nitro/virtual/polyfills"; +import { NodeRequest } from "srvx/node"; +import { useNitroApp } from "nitro/app"; +import type { IncomingMessage } from "node:http"; + +const nitroApp = useNitroApp(); + +interface EdgeOneRequest extends IncomingMessage { + url: string; + method: string; + headers: Record; +} + +// EdgeOne bootstrap expects: async (req, context) => Response +export default async function handle(req: EdgeOneRequest) { + // Use srvx NodeRequest to convert Node.js request to Web Request + const request = new NodeRequest({ req }); + + return nitroApp.fetch(request); +} diff --git a/src/presets/edgeone/utils.ts b/src/presets/edgeone/utils.ts new file mode 100644 index 0000000000..dd0108110e --- /dev/null +++ b/src/presets/edgeone/utils.ts @@ -0,0 +1,128 @@ +import type { Nitro } from "nitro/types"; +import fsp from "node:fs/promises"; +import { relative, dirname, join } from "pathe"; +import consola from "consola"; +import { colors } from "consola/utils"; +interface FrameworkRoute { + path: string; + isStatic?: boolean; + isr?: number; +} + +export async function writeEdgeOneRoutes(nitro: Nitro) { + // Ensure routes are synced + nitro.routing.sync(); + const meta = { + conf: { + ssr404: true, + }, + frameworkRoutes: [] as FrameworkRoute[], + }; + + // 1. Get all API routes (server-side route handlers) + const apiRoutes = nitro.routing.routes.routes + .filter((route) => { + // Filter out middleware and wildcard routes (e.g., /**) + const handler = Array.isArray(route.data) ? route.data[0] : route.data; + return handler && !handler.middleware && route.route !== "/**"; + }) + .map((route) => ({ + path: route.route, + method: route.method || "*", + handler: Array.isArray(route.data) ? route.data[0] : route.data, + })); + for (const route of apiRoutes) { + meta.frameworkRoutes.push({ + path: route.path, + }); + } + + // 2. Get all page routes (prerendered routes) + const pageRoutes = (nitro._prerenderedRoutes || []).map((route) => ({ + path: route.route, + fileName: route.fileName, + contentType: route.contentType, + })); + + // 3. Get user-defined prerender routes + const userPrerenderRoutes = nitro.options.prerender?.routes || []; + // 4. Get all routes marked as prerender in route rules + const prerenderRouteRules = Object.entries(nitro.options.routeRules || {}) + .filter(([_, rules]) => rules.prerender) + .map(([path]) => path); + + // 5. Get all routes with SWR/cache settings from route rules + // Note: `swr: true` shortcut is normalized to `cache: { swr: true }` after config resolution + const swrRouteRules = Object.entries(nitro.options.routeRules || {}) + .filter(([_, rules]) => { + // Check if cache.swr is enabled (normalized form) + if (rules.cache && typeof rules.cache === "object" && rules.cache.swr) { + return true; + } + return false; + }) + .map(([path, rules]) => ({ + path, + cache: rules.cache as { swr?: boolean; maxAge?: number }, + })); + for (const route of swrRouteRules) { + const maxAge = route.cache.maxAge; + for (const frameworkRoute of meta.frameworkRoutes) { + if (frameworkRoute.path === route.path) { + Reflect.set(frameworkRoute, "isStatic", false); + Reflect.set(frameworkRoute, "isr", maxAge); + } + } + } + + // Merge all prerender routes + const allPrerenderRoutes = [ + ...new Set([ + ...userPrerenderRoutes, + ...prerenderRouteRules, + ...pageRoutes.map((r) => r.path), + ]), + ]; + for (const route of allPrerenderRoutes) { + meta.frameworkRoutes.push({ + path: route, + isStatic: true, + }); + } + + await writeFile( + join(nitro.options.output.dir, "meta.json"), + JSON.stringify(meta, null, 2) + ); + await writeFile( + join(nitro.options.output.serverDir, "meta.json"), + JSON.stringify(meta, null, 2) + ); + + // Return all route information + return { + apiRoutes, + pageRoutes, + userPrerenderRoutes, + prerenderRouteRules, + allPrerenderRoutes, + swrRouteRules, + }; +} + +function prettyPath(p: string, highlight = true) { + p = relative(process.cwd(), p); + return highlight ? colors.cyan(p) : p; +} + +async function writeFile(file: string, contents: Buffer | string, log = false) { + await fsp.mkdir(dirname(file), { recursive: true }); + await fsp.writeFile( + file, + contents, + typeof contents === "string" ? "utf8" : undefined + ); + if (log) { + consola.info("Generated", prettyPath(file)); + } +}