-
-
Notifications
You must be signed in to change notification settings - Fork 813
feat: add edgeone-pages preset with build output api v3
#4170
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
d55edbd
feat(edgeone): add EdgeOne Pages preset with Build Output API v3
87f51c3
chore: apply automated updates
autofix-ci[bot] 102d3c9
Merge branch 'main' into feat/edgeone-pages-v3-clean
pi0 0692916
fix(edgeone): correctly convert wildcard routes to regex
pi0 302a6c0
docs(edgeone): add spec link and clarify routing comments
pi0 f7810c8
fix: prepend baseURL
pi0 628a2a4
disable serveStatic explicitly
pi0 c744a52
add alias
pi0 a8d4444
chore: apply automated updates
autofix-ci[bot] 399d48d
up
pi0 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, | ||
| } | ||
| ); | ||
|
|
||
| export default [edgeone] as const; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, "/(.*)") + "$", | ||
| }); | ||
|
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, | ||
| }; | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
π§© 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]].jsinstead ofhandler.jsfor the entry file name to match EdgeOne Pages conventions.The output paths and compiled hook are correct, and extending
node-serveris appropriate. However, theentryFileNames: "handler.js"doesn't align with EdgeOne Pages naming conventions. EdgeOne Pages routes functions based on file names within thecloud-functions/directory. For SSR applications, the documented pattern is[[default]].js(used in framework examples like Express templates). Change therollupConfigto:π€ Prompt for AI Agents