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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions docs/2.deploy/20.providers/edgeone.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 2 additions & 0 deletions src/presets/_all.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -40,6 +41,7 @@ export default [
..._cloudflare,
..._deno,
..._digitalocean,
..._edgeone,
..._firebase,
..._flightcontrol,
..._genezio,
Expand Down
4 changes: 2 additions & 2 deletions src/presets/_types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 & {});
31 changes: 31 additions & 0 deletions src/presets/edgeone/preset.ts
Original file line number Diff line number Diff line change
@@ -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;
20 changes: 20 additions & 0 deletions src/presets/edgeone/runtime/edgeone.ts
Original file line number Diff line number Diff line change
@@ -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();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ› οΈ Refactor suggestion | 🟠 Major

Move useNitroApp() call inside the handler function.

Calling useNitroApp() at module scope may cause timing issues if the Nitro app is not fully initialized when this module loads. Other Nitro presets typically call useNitroApp() inside the handler function to ensure the app is ready.

πŸ”Ž Proposed fix
-const nitroApp = useNitroApp();
-
 interface EdgeOneRequest extends IncomingMessage {
   url: string;
   method: string;
   headers: Record<string, string | string[] | undefined>;
 }
 
 // EdgeOne bootstrap expects: async (req, context) => Response
 export default async function handle(req: EdgeOneRequest) {
+  const nitroApp = useNitroApp();
   // Use srvx NodeRequest to convert Node.js request to Web Request
   const request = new NodeRequest({ req });
🧰 Tools
πŸͺ› Biome (2.1.2)

[error] 6-6: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

πŸ€– Prompt for AI Agents
In src/presets/edgeone/runtime/edgeone.ts around line 6, the call to
useNitroApp() is made at module scope; move this call into the handler function
so the Nitro app is resolved at request time. Update the handler to call const
nitroApp = useNitroApp() at the start of the handler (or lazily when first
needed) and replace the module-scoped reference with this local variable; ensure
no other module-level code depends on nitroApp and adjust imports/refs
accordingly.


interface EdgeOneRequest extends IncomingMessage {
url: string;
method: string;
headers: Record<string, string | string[] | undefined>;
}

// EdgeOne bootstrap expects: async (req, context) => Response
export default async function handle(req: EdgeOneRequest) {
Comment on lines +14 to +15
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

Add the missing context parameter to match EdgeOne's handler signature.

The comment indicates EdgeOne expects async (req, context) => Response, but the function signature only accepts req. If EdgeOne passes a context object, it should be captured in the signature to avoid runtime errors and enable potential use of platform-specific context.

πŸ”Ž Proposed fix
-// EdgeOne bootstrap expects: async (req, context) => Response
-export default async function handle(req: EdgeOneRequest) {
+// EdgeOne bootstrap expects: async (req, context) => Response  
+export default async function handle(req: EdgeOneRequest, context?: any) {
   const nitroApp = useNitroApp();
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// EdgeOne bootstrap expects: async (req, context) => Response
export default async function handle(req: EdgeOneRequest) {
// EdgeOne bootstrap expects: async (req, context) => Response
export default async function handle(req: EdgeOneRequest, context?: any) {
const nitroApp = useNitroApp();
πŸ€– Prompt for AI Agents
In src/presets/edgeone/runtime/edgeone.ts around lines 14-15 the default export
handler only declares a single parameter (req) but EdgeOne expects the signature
async (req, context) => Response; modify the function declaration to accept the
second parameter (context) β€” e.g., async function handle(req, context) β€” and if
available, add an appropriate type for context (EdgeOneContext or any) to the
signature so the runtime-provided context is captured without altering handler
behavior.

// Use srvx NodeRequest to convert Node.js request to Web Request
const request = new NodeRequest({ req });

return nitroApp.fetch(request);
Comment on lines 17 to 19
Copy link
Member

@pi0 pi0 Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks a very strange signature mixing node rew as incoming and web Response (result of fetch) as outgoing. Can you please confirm that:

  1. You pass a fully compliant Node.js IncomingMessage.
  2. Accept a fully compliant Web Response object

Is there an alternative that both req and res are the same format (either Node or Web, but web req is prefered)?

}
128 changes: 128 additions & 0 deletions src/presets/edgeone/utils.ts
Original file line number Diff line number Diff line change
@@ -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();
Copy link
Member

@gxres042 gxres042 Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is calling routing.sync() in preset really necessary? πŸ€”

My small backend project based on Nitro (* v3) can deploy to EdgeOne Pages properly without this.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

During our CD process, routing is allocated based on the request path. I call this to prevent route loss issues in more complex projects.

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);
}
}
}
Comment on lines +68 to +76
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

SWR routes may not match if added after API routes.

The SWR/ISR annotation loop (lines 68-76) iterates over meta.frameworkRoutes to find matching paths and update them with isStatic: false and isr: maxAge. However, this only works for routes already added to frameworkRoutes (i.e., API routes). If a SWR route path isn't an API route but comes from prerender routes (added later at line 86-91), it won't be annotated with ISR settings.

Consider reordering: collect all routes first, then apply SWR annotations, or apply SWR settings when adding prerender routes.

πŸ€– Prompt for AI Agents
In src/presets/edgeone/utils.ts around lines 68 to 76, the loop that annotates
SWR/ISR on meta.frameworkRoutes runs before prerender routes are appended (lines
~86-91), so SWR paths added later won't get isStatic/isr set; fix by either
moving the SWR annotation block to run after all routes are collected (i.e.,
after prerender routes are added) or, when adding prerender routes, apply the
SWR settings there by looking up the matching swrRouteRules and setting
isStatic/isr on the newly pushed route so all routes receive ISR annotations
regardless of insertion order.


// 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,
Comment on lines +104 to +105
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please clarify, what is difference between handling api routes and pages routes in platform? (possibly linking relavant docs to code might be useful)

Additionally, (also important) we need to register a fallback route that handles any uncaught route pattern (middleware for example can also catch routes)

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));
}
}