Skip to content

Conversation

@q153877011
Copy link

πŸ”— Linked issue

#2973

❓ Type of change

  • [ βœ… ] πŸ“– Documentation (updates to the documentation, readme, or JSdoc annotations)
  • [ βœ… ] ✨ New feature (a non-breaking change that adds functionality)

πŸ“š Description

Hi, I’m a developer of EdgeOne Pages. In this PR, I’m adding a preset to support deploying Nitro projects on EdgeOne Pages.

I’ve completed the implementation and deployment testing. This update includes some configuration for the Nitro build package, while additional platform-side network request settings have been moved into the platform CI workflow.

Please review. Thanks.

@q153877011 q153877011 requested a review from pi0 as a code owner December 18, 2025 09:43
@vercel
Copy link

vercel bot commented Dec 18, 2025

Someone is attempting to deploy a commit to the Nitro Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link

coderabbitai bot commented Dec 18, 2025

πŸ“ Walkthrough

Walkthrough

Adds EdgeOne deployment support: documentation, preset registration and types, an EdgeOne Nitro preset with build hooks, a runtime adapter converting EdgeOne Node.js requests to Nitro, and a utility that generates route metadata (meta.json) for API, prerendered, and SWR/cached routes.

Changes

Cohort / File(s) Summary
Documentation
docs/2.deploy/20.providers/edgeone.md
New guide describing the EdgeOne preset, control panel deployment flow, EdgeOne CLI flow (build β†’ upload β†’ publish), and supported deployment sources (GitHub, GitLab, Gitee, CNB).
Preset Registration
src/presets/_all.gen.ts
Import of the EdgeOne preset and inclusion via spread in the default exported presets array.
Type Definitions
src/presets/_types.gen.ts
Added \"edgeone-pages\" to PresetName and input aliases (edgeonePages, edgeone_pages) to PresetNameInput.
Preset Implementation
src/presets/edgeone/preset.ts
New preset defined with defineNitroPreset: extends node-server, serveStatic: true, custom entry ./edgeone/runtime/edgeone, output dir mappings, rollup entryFileNames set to handler.js, and a compiled hook that invokes route metadata writer.
Runtime Adapter
src/presets/edgeone/runtime/edgeone.ts
New default async handler that adapts EdgeOne-style Node.js requests into a NodeRequest and forwards them to nitroApp.fetch().
Route Metadata Utilities
src/presets/edgeone/utils.ts
New exported writeEdgeOneRoutes(nitro: Nitro) that collects API routes, prerendered pages, user prerender routes, detects prerender route rules and SWR/cache rules, annotates framework routes (isStatic/isr), deduplicates, and writes meta.json to output and server dirs.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Areas requiring extra attention:

  • src/presets/edgeone/utils.ts β€” route collection, routeRules analysis (prerender, cache.swr), deduplication and metadata shape.
  • Preset integration (src/presets/edgeone/preset.ts & _all.gen.ts) β€” output mappings and compiled hook invocation.
  • Runtime request translation (src/presets/edgeone/runtime/edgeone.ts) β€” correctness of NodeRequest creation and header/body propagation.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
βœ… Passed checks (2 passed)
Check name Status Explanation
Title check βœ… Passed The title 'feat: add edgeone preset' follows conventional commits format with 'feat' prefix and clearly describes the main change of adding an EdgeOne preset.
Description check βœ… Passed The description directly relates to the changeset by explaining the EdgeOne preset addition, implementation status, and testing completion.
✨ Finishing touches
  • πŸ“ Generate docstrings
πŸ§ͺ Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❀️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@q153877011 q153877011 changed the title Added preset setting for edgeone; feat(edgeone): Added preset setting for edgeone; Dec 18, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (2)
src/presets/edgeone/runtime/edgeone.ts (1)

15-15: Unused context parameter.

The context parameter is declared but never used. If EdgeOne's bootstrap API requires this signature, consider prefixing with underscore (_context) to indicate it's intentionally unused, or document why it's present.

πŸ”Ž Consider this diff:
-export default async function handle(req: EdgeOneRequest, context: unknown) {
+export default async function handle(req: EdgeOneRequest, _context: unknown) {
src/presets/edgeone/utils.ts (1)

68-77: Optimize SWR route matching performance.

The nested iteration creates O(n*m) complexity. For better performance with many routes, consider using a Map to index frameworkRoutes by path.

πŸ”Ž Consider this optimization:
+  // Create a map for faster lookups
+  const routeMap = new Map(
+    meta.frameworkRoutes.map((route, index) => [route.path, index])
+  );
+
   swrRouteRules.forEach((route) => {
     const maxAge = route.cache.maxAge;
-    meta.frameworkRoutes.forEach((frameworkRoute) => {
-      if (frameworkRoute.path === route.path) {
-        Reflect.set(frameworkRoute, "isStatic", false);
-        Reflect.set(frameworkRoute, "isr", maxAge);
-      }
-    });
+    const index = routeMap.get(route.path);
+    if (index !== undefined) {
+      const frameworkRoute = meta.frameworkRoutes[index];
+      Reflect.set(frameworkRoute, "isStatic", false);
+      Reflect.set(frameworkRoute, "isr", maxAge);
+    }
   });
πŸ“œ Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 0d0a2f9 and 53f6523.

πŸ“’ Files selected for processing (7)
  • docs/2.deploy/20.providers/edgeone.md (1 hunks)
  • src/presets/_all.gen.ts (2 hunks)
  • src/presets/_types.gen.ts (1 hunks)
  • src/presets/edgeone/preset.ts (1 hunks)
  • src/presets/edgeone/runtime/_utils.ts (1 hunks)
  • src/presets/edgeone/runtime/edgeone.ts (1 hunks)
  • src/presets/edgeone/utils.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
πŸ“š Learning: 2025-12-03T19:09:13.905Z
Learnt from: medz
Repo: nitrojs/nitro PR: 3834
File: src/presets/cloudflare/server-entry.ts:63-63
Timestamp: 2025-12-03T19:09:13.905Z
Learning: In the Nitro Cloudflare preset (src/presets/cloudflare/server-entry.ts), native errors from oxc-parser and Node.js readFile operations should be allowed to bubble up naturally without wrapping, as their native error messages are more user-friendly and provide better diagnostic information.

Applied to files:

  • src/presets/edgeone/preset.ts
  • src/presets/_all.gen.ts
🧬 Code graph analysis (5)
src/presets/edgeone/utils.ts (1)
src/types/nitro.ts (1)
  • Nitro (16-37)
src/presets/edgeone/runtime/edgeone.ts (1)
src/presets/edgeone/runtime/_utils.ts (1)
  • normalizeHeaders (1-9)
src/presets/edgeone/runtime/_utils.ts (1)
src/runtime/internal/route-rules.ts (1)
  • headers (14-19)
src/presets/edgeone/preset.ts (3)
src/presets/_utils/preset.ts (1)
  • defineNitroPreset (6-18)
src/types/nitro.ts (1)
  • Nitro (16-37)
src/presets/edgeone/utils.ts (1)
  • writeEdgeOneRoutes (11-106)
src/presets/_types.gen.ts (1)
src/presets/index.ts (2)
  • PresetName (5-5)
  • PresetNameInput (6-6)
πŸͺ› Biome (2.1.2)
src/presets/edgeone/runtime/edgeone.ts

[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)

πŸͺ› LanguageTool
docs/2.deploy/20.providers/edgeone.md

[uncategorized] ~13-~13: The official name of this software platform is spelled with a capital β€œH”.
Context: ...oyment method. We support deployment on Github, GitLab, Gitee, and CNB. 3. Choose the ...

(GITHUB)

πŸ”‡ Additional comments (4)
src/presets/_types.gen.ts (1)

23-25: LGTM!

The addition of "edgeone" to both PresetName and PresetNameInput type unions is correct and aligns with the new EdgeOne preset implementation.

src/presets/_all.gen.ts (1)

14-14: LGTM!

The EdgeOne preset is correctly imported and exported following the established pattern for other presets.

Also applies to: 44-44

src/presets/edgeone/runtime/_utils.ts (1)

1-9: LGTM!

The normalizeHeaders function correctly converts header values to a HeadersInit compatible format, properly handling undefined values and joining array values with comma-space separator as per HTTP standards.

src/presets/edgeone/preset.ts (1)

5-29: LGTM!

The EdgeOne preset is well-structured and follows Nitro preset conventions:

  • Properly extends "node-server" as the base preset
  • Configures appropriate output directories for EdgeOne deployment
  • Generates route metadata during compilation via the writeEdgeOneRoutes hook

Comment on lines 33 to 37
apiRoutes.forEach((route) => {
meta.frameworkRoutes.push({
path: route.path,
});
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Potential duplicate routes in frameworkRoutes.

API routes are added to frameworkRoutes (lines 33-37), and later prerender routes are also added (lines 87-92). If the same path exists in both collections, it will appear twice in frameworkRoutes. Consider checking for duplicates or using a Map/Set to track paths.

πŸ”Ž Consider this approach to prevent duplicates:
+  // Track added paths to prevent duplicates
+  const addedPaths = new Set<string>();
+
   apiRoutes.forEach((route) => {
+    if (!addedPaths.has(route.path)) {
+      meta.frameworkRoutes.push({
+        path: route.path,
+      });
+      addedPaths.add(route.path);
+    }
-    meta.frameworkRoutes.push({
-      path: route.path,
-    });
   });
   
   // ... later ...
   
   allPrerenderRoutes.forEach((route) => {
+    if (!addedPaths.has(route)) {
+      meta.frameworkRoutes.push({
+        path: route,
+        isStatic: true,
+      });
+      addedPaths.add(route);
+    }
-    meta.frameworkRoutes.push({
-      path: route,
-      isStatic: true,
-    });
   });

Also applies to: 87-92

πŸ€– Prompt for AI Agents
In src/presets/edgeone/utils.ts around lines 33-37 (and similarly 87-92), API
and prerender routes are pushed into meta.frameworkRoutes without checking for
existing paths, causing duplicates; fix by tracking seen paths (e.g., a Set) and
only push a route if its path is not already present, or deduplicate
meta.frameworkRoutes after population by filtering unique path entries so each
path appears once.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
docs/2.deploy/20.providers/edgeone.md (1)

22-22: Format the CLI command as code for better readability.

The command edgeone pages deploy should be formatted as inline code to follow standard documentation conventions and improve readability.

πŸ”Ž Apply this diff to format the command:
-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.
+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.
πŸ“œ Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 53f6523 and e81d848.

πŸ“’ Files selected for processing (3)
  • docs/2.deploy/20.providers/edgeone.md (1 hunks)
  • src/presets/edgeone/runtime/edgeone.ts (1 hunks)
  • src/presets/edgeone/utils.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/presets/edgeone/utils.ts
🧰 Additional context used
🧬 Code graph analysis (1)
src/presets/edgeone/runtime/edgeone.ts (1)
src/presets/edgeone/runtime/_utils.ts (1)
  • normalizeHeaders (1-9)
πŸͺ› Biome (2.1.2)
src/presets/edgeone/runtime/edgeone.ts

[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)

πŸ”‡ Additional comments (3)
src/presets/edgeone/runtime/edgeone.ts (3)

17-20: LGTM! URL construction is secure and robust.

The URL construction correctly:

  • Prioritizes EdgeOne-specific eo-pages-host header
  • Falls back to standard host header with a safe default
  • Defaults to https protocol for security (addressing previous review feedback)

22-27: Verify body handling for GET and HEAD requests.

Request using GET or HEAD method cannot have a body; null is returned in these cases per the Fetch API specification. The code passes req.body to the Request constructor for all HTTP methods, which will throw a TypeError if a non-empty body is provided with GET or HEAD requests.

Please verify that:

  • EdgeOne sets req.body to undefined, null, or an empty string for GET and HEAD requests, or
  • Consider conditionally passing the body only for methods that support it

8-13: The body property is strictly typed as string, but the Web Request API's BodyInit type accepts multiple formats (string | Blob | ArrayBuffer | FormData | URLSearchParams | ReadableStream | null). Verify whether EdgeOne's runtime always provides request bodies as pre-buffered strings, or if the type should be broader to handle binary data and multipart form submissions that the platform may deliver.

@medz
Copy link
Contributor

medz commented Dec 19, 2025

Hey @q153877011 I noticed that in https://github.com/nitrojs/nitro/pull/3884/changes#diff-653eb6c213f3982301def6b933a232ceb95b4947cf50cdc70c5ed425223a192aR23 you created a new Web Request using basic information. The EdgeOne preset is based on the Node preset. Furthermore, EdgeOne's handler passes a Node IncomingMessage (https://nodejs.org/api/http.html#class-httpincomingmessage).

Nitro relies on srvx, such as exported handlers. Could we directly use:

export default toNodeHandler(nitroApp.fetch);

Or, if a Request is required, we can use:

const request = new NodeRequest({ req });

PS: I haven't actually tested this on EdgeOne, but my intuition tells me this is the right approach.

@medz
Copy link
Contributor

medz commented Dec 19, 2025

https://github.com/nitrojs/nitro/pull/3884/changes#diff-653eb6c213f3982301def6b933a232ceb95b4947cf50cdc70c5ed425223a192aR16

Additionally, I noticed that EdgeOne has its own Context, which I think should also be communicated to Nitro, for example, using event.context.edgeone and with type-friendly syntax (like event.context.cloudflare). What do you think?

@medz
Copy link
Contributor

medz commented Dec 19, 2025

Sorry, I might have gone too far with my comments πŸ˜‚. This is because it's a concern of mine. When I saw this PR, I was thinking that besides deploying my application on Cloudflare/Vercel, EdgeOne might be a good option for providing services to users in China. So I carefully reviewed this PR and offered some of my thoughts.

To summarize:

  1. I think the handling of the Request object is not user-friendly enough, and Srvx can already handle the compatibility conversion between internal requests (there's no need to write a simplified version).
  2. The EdgeOne Context is missing; users don't have access to the EdgeOne platform context.
  3. These are EdgeOne pages; should they be named edgeone-pages by default? (PS: What I really want might be an EdgeOne Function πŸ˜‚. On a side note: I don't understand the difference between pages and function. It seems the product itself doesn't have a clear definition like a Vercel Function.)

@medz medz mentioned this pull request Dec 19, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/presets/edgeone/runtime/edgeone.ts (1)

15-19: Consider exposing EdgeOne platform context to user code.

Based on the PR feedback from medz, EdgeOne-specific context should be exposed to Nitro via event.context.edgeone so users can access platform-specific features. Currently, the EdgeOne context parameter is not being forwarded to Nitro's event system.

If EdgeOne provides a context object with platform-specific data (environment variables, request metadata, etc.), consider passing it through the NodeRequest constructor or attaching it to the Nitro event after request construction. This would align with existing patterns like event.context.cloudflare in other presets.

Based on PR feedback from medz.

πŸ“œ Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between e81d848 and 5827462.

πŸ“’ Files selected for processing (5)
  • docs/2.deploy/20.providers/edgeone.md
  • playground/nitro.config.ts
  • src/presets/_types.gen.ts
  • src/presets/edgeone/preset.ts
  • src/presets/edgeone/runtime/edgeone.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/presets/_types.gen.ts
  • docs/2.deploy/20.providers/edgeone.md
  • src/presets/edgeone/preset.ts
🧰 Additional context used
🧠 Learnings (1)
πŸ“š Learning: 2025-12-03T19:09:13.905Z
Learnt from: medz
Repo: nitrojs/nitro PR: 3834
File: src/presets/cloudflare/server-entry.ts:63-63
Timestamp: 2025-12-03T19:09:13.905Z
Learning: In the Nitro Cloudflare preset (src/presets/cloudflare/server-entry.ts), native errors from oxc-parser and Node.js readFile operations should be allowed to bubble up naturally without wrapping, as their native error messages are more user-friendly and provide better diagnostic information.

Applied to files:

  • playground/nitro.config.ts
πŸͺ› Biome (2.1.2)
src/presets/edgeone/runtime/edgeone.ts

[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)

πŸ”‡ Additional comments (1)
playground/nitro.config.ts (1)

4-4: LGTM! Playground configuration updated for EdgeOne testing.

The preset name edgeone-pages aligns with the new preset registration and type definitions introduced in this PR.

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.

Comment on lines +14 to +15
// EdgeOne bootstrap expects: async (req, context) => Response
export default async function handle(req: EdgeOneRequest) {
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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (2)
src/presets/edgeone/runtime/edgeone.ts (2)

6-6: Move useNitroApp() call inside the handler function.

Calling useNitroApp() at module scope can cause timing issues if the Nitro app is not fully initialized when this module loads. Other Nitro presets call useNitroApp() inside the handler 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 });

14-15: 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 to avoid potential runtime issues and enable use of platform-specific context.

πŸ”Ž Proposed fix
 // EdgeOne bootstrap expects: async (req, context) => Response
-export default async function handle(req: EdgeOneRequest) {
+export default async function handle(req: EdgeOneRequest, context?: any) {
   const nitroApp = useNitroApp();
🧹 Nitpick comments (2)
src/presets/edgeone/runtime/edgeone.ts (2)

8-12: Consider simplifying the EdgeOneRequest interface.

The interface redeclares properties (url, method, headers) that already exist on IncomingMessage. If the intent is to make these properties required or adjust their types, consider documenting why, or simply use IncomingMessage directly if the type differences aren't needed.


16-19: Consider exposing EdgeOne platform context to user code.

The current implementation uses NodeRequest correctly (aligning with reviewer medz's suggestions), but doesn't expose EdgeOne-specific context to Nitro. Consider capturing the EdgeOne context parameter and making it available via event.context.edgeone, similar to how other presets expose platform context (e.g., event.context.cloudflare). This would allow users to access EdgeOne-specific features in their handlers.

Based on reviewer medz's feedback in PR objectives.

Example approach

After capturing the context parameter, you could pass it to the request creation:

const request = new NodeRequest({ 
  req,
  context: { edgeone: context }
});

Or use Nitro's event context mechanism to attach it, depending on how NodeRequest handles context propagation.

πŸ“œ Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 5827462 and bd99387.

πŸ“’ Files selected for processing (4)
  • docs/2.deploy/20.providers/edgeone.md
  • src/presets/_types.gen.ts
  • src/presets/edgeone/preset.ts
  • src/presets/edgeone/runtime/edgeone.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/presets/_types.gen.ts
  • docs/2.deploy/20.providers/edgeone.md
🧰 Additional context used
🧠 Learnings (2)
πŸ““ Common learnings
Learnt from: medz
Repo: nitrojs/nitro PR: 3834
File: src/presets/cloudflare/server-entry.ts:63-63
Timestamp: 2025-12-03T19:09:13.905Z
Learning: In the Nitro Cloudflare preset (src/presets/cloudflare/server-entry.ts), native errors from oxc-parser and Node.js readFile operations should be allowed to bubble up naturally without wrapping, as their native error messages are more user-friendly and provide better diagnostic information.
πŸ“š Learning: 2025-12-03T19:09:13.905Z
Learnt from: medz
Repo: nitrojs/nitro PR: 3834
File: src/presets/cloudflare/server-entry.ts:63-63
Timestamp: 2025-12-03T19:09:13.905Z
Learning: In the Nitro Cloudflare preset (src/presets/cloudflare/server-entry.ts), native errors from oxc-parser and Node.js readFile operations should be allowed to bubble up naturally without wrapping, as their native error messages are more user-friendly and provide better diagnostic information.

Applied to files:

  • src/presets/edgeone/runtime/edgeone.ts
  • src/presets/edgeone/preset.ts
πŸͺ› Biome (2.1.2)
src/presets/edgeone/runtime/edgeone.ts

[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)

πŸ”‡ Additional comments (1)
src/presets/edgeone/preset.ts (1)

5-29: Preset configuration looks good.

The preset configuration appropriately extends "node-server", enables static file serving, and sets up correct output directories. The name is explicitly set to "edgeone-pages" (line 27), addressing the naming consideration from past reviews.

@q153877011
Copy link
Author

q153877011 commented Dec 22, 2025

Sorry, I might have gone too far with my comments πŸ˜‚. This is because it's a concern of mine. When I saw this PR, I was thinking that besides deploying my application on Cloudflare/Vercel, EdgeOne might be a good option for providing services to users in China. So I carefully reviewed this PR and offered some of my thoughts.

To summarize:

  1. I think the handling of the Request object is not user-friendly enough, and Srvx can already handle the compatibility conversion between internal requests (there's no need to write a simplified version).
  2. The EdgeOne Context is missing; users don't have access to the EdgeOne platform context.
  3. These are EdgeOne pages; should they be named edgeone-pages by default? (PS: What I really want might be an EdgeOne Function πŸ˜‚. On a side note: I don't understand the difference between pages and function. It seems the product itself doesn't have a clear definition like a Vercel Function.)

Hello @medz @gxres042. Changes have been made based on your feedback(bd99387c6231ee66eaef2b8675a4a57a43b67e37):

  1. Request handling: Removed the custom simplified conversion and now rely on Srvx for internal Request compatibility handling.
  2. Handler context: The context parameter was kept as a placeholder during rapid platform iteration but isn’t actually used at the moment. Since it may confuse users πŸ˜‚, it has been removed for now; we’ll revisit and reintroduce a clearer integration point once the related capabilities are defined.
  3. Preset: Updated the preset to edgeone-pages. EdgeOne Pages supports both centralized Node.js Functions and edge-based Edge Functions. The runtime differences and adapters are handled in the CLI; if we later extract this as a standalone capability, we’ll reassess whether it should be integrated into Nitro.

@q153877011 q153877011 requested a review from gxres042 December 22, 2025 08:05
@pi0 pi0 changed the title feat(edgeone): Added preset setting for edgeone; feat: add edgeone preset Dec 22, 2025

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.

@q153877011
Copy link
Author

@pi0 May I ask when this content could be officially published? We have some upcoming ecosystem deployment work in conjunction with Nitro that we need to schedule accordingly.

Comment on lines 17 to 19
const request = new NodeRequest({ req });

return nitroApp.fetch(request);
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)?

Comment on lines +99 to +100
apiRoutes,
pageRoutes,
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)

Copy link
Member

@pi0 pi0 left a comment

Choose a reason for hiding this comment

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

@q153877011 thanks for PR ❀️

Here are next steps before merging support into v3:

  • Finalizing discussion on handler signature (Web or Node)
    • Possibly adding preset tests
  • Finalizing discussion on routing (catch-all)
  • Setting a continuous E2E deployment in nitro-deploys (i can do this later)
  • Possibly auto-detecting edge-one pages CI using a known environment variable for zero config (to be added here (optional, only if you have already one)

Feel free to message me in discord (@pi0) for quick discusison.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (3)
src/presets/edgeone/runtime/edgeone.ts (2)

6-6: Consider moving useNitroApp() inside the handler.

Calling useNitroApp() at module scope may cause timing issues if the Nitro app isn't fully initialized when the module loads. While the Biome warning about React hooks is a false positive (this isn't React), the underlying concern about initialization order is valid for Nitro presets.

πŸ”Ž Proposed fix
-const nitroApp = useNitroApp();
-
 interface EdgeOneRequest extends IncomingMessage {
   // ...
 }

 export default async function handle(req: EdgeOneRequest) {
+  const nitroApp = useNitroApp();
   const request = new NodeRequest({ req });
   return nitroApp.fetch(request);
 }

14-15: Add the missing context parameter to match EdgeOne's expected handler signature.

The comment on line 14 indicates EdgeOne expects async (req, context) => Response, but the function only accepts req. Capturing the context parameter would enable access to platform-specific features (as suggested in PR comments about exposing event.context.edgeone).

πŸ”Ž Proposed fix
-export default async function handle(req: EdgeOneRequest) {
+export default async function handle(req: EdgeOneRequest, _context?: unknown) {
   const nitroApp = useNitroApp();
src/presets/edgeone/utils.ts (1)

34-38: Potential duplicate routes in frameworkRoutes.

API routes are added to frameworkRoutes at lines 34-38, and prerender routes are appended at lines 86-91. If the same path exists in both collections (e.g., an API route that's also prerendered), it will appear twice in the output meta.json. Consider deduplicating or using a Map keyed by path.

πŸ”Ž Proposed fix using a Set to track added paths
+  const addedPaths = new Set<string>();
+
   for (const route of apiRoutes) {
+    if (addedPaths.has(route.path)) continue;
     meta.frameworkRoutes.push({
       path: route.path,
     });
+    addedPaths.add(route.path);
   }

   // ... later in the function ...

   for (const route of allPrerenderRoutes) {
+    if (addedPaths.has(route)) continue;
     meta.frameworkRoutes.push({
       path: route,
       isStatic: true,
     });
+    addedPaths.add(route);
   }

Also applies to: 86-91

🧹 Nitpick comments (2)
src/presets/edgeone/utils.ts (2)

113-116: prettyPath function is currently unused.

The prettyPath helper is defined but writeFile is always called with log = false (the default), so the logging path that uses prettyPath is never executed. Consider either:

  • Using log = true for at least one call to provide user feedback
  • Removing the function if logging is intentionally silent

72-73: Prefer direct property assignment over Reflect.set.

Using Reflect.set for simple property assignment on a plain object is unconventional and less readable. Direct assignment is clearer and has the same effect here.

πŸ”Ž Proposed simplification
       if (frameworkRoute.path === route.path) {
-        Reflect.set(frameworkRoute, "isStatic", false);
-        Reflect.set(frameworkRoute, "isr", maxAge);
+        frameworkRoute.isStatic = false;
+        (frameworkRoute as FrameworkRoute & { isr?: number }).isr = maxAge;
       }

Alternatively, add isr to the FrameworkRoute interface since it's being used:

 interface FrameworkRoute {
   path: string;
   isStatic?: boolean;
+  isr?: number;
 }
πŸ“œ Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between bd99387 and 197f4ab.

πŸ“’ Files selected for processing (3)
  • src/presets/edgeone/preset.ts
  • src/presets/edgeone/runtime/edgeone.ts
  • src/presets/edgeone/utils.ts
🧰 Additional context used
🧠 Learnings (2)
πŸ““ Common learnings
Learnt from: medz
Repo: nitrojs/nitro PR: 3834
File: src/presets/cloudflare/server-entry.ts:63-63
Timestamp: 2025-12-03T19:09:13.905Z
Learning: In the Nitro Cloudflare preset (src/presets/cloudflare/server-entry.ts), native errors from oxc-parser and Node.js readFile operations should be allowed to bubble up naturally without wrapping, as their native error messages are more user-friendly and provide better diagnostic information.
πŸ“š Learning: 2025-12-03T19:09:13.905Z
Learnt from: medz
Repo: nitrojs/nitro PR: 3834
File: src/presets/cloudflare/server-entry.ts:63-63
Timestamp: 2025-12-03T19:09:13.905Z
Learning: In the Nitro Cloudflare preset (src/presets/cloudflare/server-entry.ts), native errors from oxc-parser and Node.js readFile operations should be allowed to bubble up naturally without wrapping, as their native error messages are more user-friendly and provide better diagnostic information.

Applied to files:

  • src/presets/edgeone/runtime/edgeone.ts
  • src/presets/edgeone/preset.ts
🧬 Code graph analysis (2)
src/presets/edgeone/utils.ts (1)
src/types/nitro.ts (1)
  • Nitro (16-37)
src/presets/edgeone/preset.ts (3)
src/presets/_utils/preset.ts (1)
  • defineNitroPreset (6-18)
src/types/nitro.ts (1)
  • Nitro (16-37)
src/presets/edgeone/utils.ts (1)
  • writeEdgeOneRoutes (12-111)
πŸͺ› Biome (2.1.2)
src/presets/edgeone/runtime/edgeone.ts

[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)

πŸ”‡ Additional comments (3)
src/presets/edgeone/preset.ts (1)

5-29: LGTM! Well-structured preset definition.

The preset correctly extends node-server, configures appropriate output directories for EdgeOne's expected structure, and hooks into the compiled lifecycle to generate route metadata. The naming as edgeone-pages aligns with the PR discussion.

src/presets/edgeone/runtime/edgeone.ts (1)

1-20: Overall runtime implementation looks correct.

The use of srvx/node's NodeRequest to convert the Node.js IncomingMessage to a Web Request is the recommended approach per PR discussion (medz's suggestion to rely on srvx instead of custom conversion). The flow is clean: polyfills β†’ convert request β†’ delegate to nitroApp.fetch().

src/presets/edgeone/utils.ts (1)

12-110: Overall implementation is solid.

The function correctly extracts route metadata from multiple Nitro sources (API routes, prerendered pages, user-defined prerender routes, route rules with prerender/SWR settings) and writes a structured meta.json for EdgeOne's deployment pipeline. The return value provides useful debugging information.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants