Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ staticwebapp.config.json
playground/firebase.json
.zeabur
.apphosting
.edgeone

test/fixture/functions
.data
Expand Down
22 changes: 22 additions & 0 deletions docs/2.deploy/20.providers/edgeone-pages.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# EdgeOne Pages

> Deploy Nitro apps to EdgeOne Pages.

**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 @@ -41,6 +42,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 @@ -22,6 +22,6 @@ export interface PresetOptions {

export const presetsWithConfig = ["awsAmplify","awsLambda","azure","cloudflare","firebase","netlify","vercel","zephyr"] 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" | "zephyr" | "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" | "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" | "zephyr" | "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" | "zephyr" | "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" | "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" | "zephyr" | "zerops" | "zerops-static" | "zeropsStatic" | "zerops_static" | (string & {});
32 changes: 32 additions & 0 deletions src/presets/edgeone/preset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { defineNitroPreset } from "../_utils/preset.ts";
import { writeEdgeOneConfig } from "./utils.ts";
import type { Nitro } from "nitro/types";

const edgeone = defineNitroPreset(
{
extends: "node-server",
entry: "./edgeone/runtime/edgeone",
serveStatic: false, // EdgeOne serves static assets from `.edgeone/assets/`
output: {
dir: "{{ rootDir }}/.edgeone",
serverDir: "{{ output.dir }}/cloud-functions/ssr-node",
publicDir: "{{ output.dir }}/assets/{{ baseURL }}",
},
rollupConfig: {
output: {
entryFileNames: "handler.js",
},
},
hooks: {
async compiled(nitro: Nitro) {
await writeEdgeOneConfig(nitro);
},
},
},
{
name: "edgeone-pages" as const,
aliases: ["edgeone"] as const,
}
);
Comment on lines +5 to +30
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟑 Minor

🧩 Analysis chain

🌐 Web query:

EdgeOne Pages cloud functions entry file naming convention handler.js

πŸ’‘ Result:

In EdgeOne Pages Cloud Functions (Node.js runtime), there is no specific required naming convention mandating "handler.js" as the entry file name. Instead, functions use flexible file naming based on the desired URL route. Cloud Functions are placed in the ./cloud-functions/ directory (sometimes referred to as node-functions in docs). The file name and path determine the access route: - Arbitrary .js names map directly to routes. Examples from official docs: - ./cloud-functions/api/hello.js β†’ example.com/api/hello - ./cloud-functions/api/nodeinfo.js β†’ example.com/api/nodeinfo - ./cloud-functions/index.js β†’ example.com/ - For frameworks like Express/Koa, use [[default]].js as the entry file in a subdirectory (e.g., ./cloud-functions/express/[[default]].js) and export the app instance (export default app;). The entry function must be exported as default (e.g., export default function onRequest(context) {...}), but the file can be named freely except for framework catch-all cases. No sources mention "handler.js" as a convention; examples use descriptive names like hello.js or route-specific names.

Citations:


Use [[default]].js instead of handler.js for the entry file name to match EdgeOne Pages conventions.

The output paths and compiled hook are correct, and extending node-server is appropriate. However, the entryFileNames: "handler.js" doesn't align with EdgeOne Pages naming conventions. EdgeOne Pages routes functions based on file names within the cloud-functions/ directory. For SSR applications, the documented pattern is [[default]].js (used in framework examples like Express templates). Change the rollupConfig to:

rollupConfig: {
  output: {
    entryFileNames: "[[default]].js",
  },
},
πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/presets/edgeone/preset.ts` around lines 5 - 29, The preset defines an
EdgeOne Pages bundle but uses the wrong entry filename; in the edgeone preset
created via defineNitroPreset (symbol: edgeone) update the
rollupConfig.output.entryFileNames value from "handler.js" to the EdgeOne Pages
convention "[[default]].js" so that the generated server function inside
serverDir/cloud-functions will be routed correctly (look for rollupConfig and
entryFileNames in the edgeone object).


export default [edgeone] as const;
19 changes: 19 additions & 0 deletions src/presets/edgeone/runtime/edgeone.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
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<string, string | string[] | undefined>;
}

// 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);
}
161 changes: 161 additions & 0 deletions src/presets/edgeone/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/**
* EdgeOne Pages Build Output API v3 config generator.
*
* Writes `.edgeone/cloud-functions/ssr-node/config.json` describing how the
* platform should route incoming requests between static assets (served from
* `.edgeone/assets/`) and the SSR function (`handler.js`).
*
* Spec: https://pages.edgeone.ai/document/building-output-configuration
*/
import type { Nitro } from "nitro/types";
import { join } from "pathe";
import { joinURL } from "ufo";
import { writeFile } from "../../utils/fs.ts";

type SourceRoute = {
src: string;
dest?: string;
headers?: Record<string, string>;
methods?: string[];
continue?: boolean;
status?: number;
};

type HandlerRoute = {
handle: "filesystem";
};

type Route = SourceRoute | HandlerRoute;

interface EdgeOneConfig {
version: 3;
routes: Route[];
}

/**
* Convert a Nitro/h3 route pattern to a RE2-compatible regex string.
*
* EdgeOne's `routes[].src` uses Go's RE2 engine (no lookaround, no backrefs).
* We do this in a single pass so a later replacement can't match a token
* (e.g. the `*` inside `(.*)`) that a previous replacement just inserted.
*
* "/about" -> "^/about$"
* "/api/posts/:id" -> "^/api/posts/([^/]+)$"
* "/blog/*" -> "^/blog/([^/]+)$"
* "/blog/**" -> "^/blog/(.*)$"
*/
function routeToRegex(route: string, baseURL = "/"): string {
const withBase = joinURL(baseURL, route);
return (
"^" +
withBase.replace(/\*\*|\*|:[^/]+/g, (m) => {
if (m === "**") return "(.*)";
return "([^/]+)";
}) +
"$"
);
}

export async function writeEdgeOneConfig(nitro: Nitro) {
nitro.routing.sync();

const baseURL = nitro.options.baseURL || "/";

const config: EdgeOneConfig = {
version: 3,
routes: [],
};

// Phase 1 β€” rules evaluated before the filesystem handler (redirects, headers).
// Sorted shallow-to-deep so more specific rules override more general ones.
const rules = Object.entries(nitro.options.routeRules || {}).sort(
(a, b) => a[0].split(/\/(?!\*)/).length - b[0].split(/\/(?!\*)/).length
);

config.routes.push(
...rules
.filter(([_, routeRules]) => routeRules.redirect || routeRules.headers)
.map(([path, routeRules]) => {
const route: SourceRoute = {
src: routeToRegex(path, baseURL),
};
if (routeRules.redirect) {
route.status = routeRules.redirect.status || 302;
route.headers = {
Location: joinURL(baseURL, routeRules.redirect.to.replace("/**", "/$1")),
};
}
if (routeRules.headers) {
route.headers = { ...route.headers, ...(routeRules.headers as Record<string, string>) };
if (!routeRules.redirect) {
route.continue = true;
}
}
return route;
})
);

// The filesystem handler serves any matching file under `.edgeone/assets/`.
// Requests that don't match a static file fall through to the rules below,
// which forward dynamic paths to the SSR function.
config.routes.push({ handle: "filesystem" });

// Phase 2 β€” dynamic routes evaluated after the filesystem handler.
const apiRoutes = nitro.routing.routes.routes
.filter((route) => {
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 || "*",
}));

for (const route of apiRoutes) {
const sourceRoute: SourceRoute = {
src: routeToRegex(route.path, baseURL),
};
if (route.method !== "*") {
sourceRoute.methods = [route.method.toUpperCase()];
}
config.routes.push(sourceRoute);
}

// SSR page routes declared by the framework (e.g. Nuxt) plus any scanned handlers.
const ssrRoutes = [
...new Set([
...(nitro.options.ssrRoutes || []),
...[...nitro.scannedHandlers, ...nitro.options.handlers]
.filter((h) => !h.middleware && h.route && h.route !== "/**")
.map((h) => h.route!),
]),
];

for (const route of ssrRoutes) {
if (apiRoutes.some((r) => r.path === route)) {
continue;
}
config.routes.push({
src: routeToRegex(route, baseURL),
});
}

// Final catch-all forwards anything unmatched above to the SSR function.
// Includes requests without the baseURL prefix so the runtime can redirect
// or normalize them instead of returning a platform-level 404.
config.routes.push({
src: "^" + joinURL(baseURL, "/(.*)") + "$",
});
Comment thread
pi0 marked this conversation as resolved.
if (baseURL !== "/") {
config.routes.push({
src: "^/(.*)$",
});
}

const configContent = JSON.stringify(config, null, 2);
await writeFile(join(nitro.options.output.serverDir, "config.json"), configContent, true);

return {
apiRoutes,
};
}
Loading