Skip to content

Conversation

@yzuyr
Copy link
Member

@yzuyr yzuyr commented Dec 2, 2025

Summary by CodeRabbit

  • New Features

    • Enhanced routing (async matching, nested layouts, catch-all, typed params) and public router utilities (useRouter, useRoute, href, setRouterContext); browser-friendly client exports
    • Added example app pages/components (home, about, users, auth/login, 404)
  • Bug Fixes

    • Removed noisy debug logs
    • Pinned Vite to a specific version across apps/packages
  • Tests

    • Added comprehensive tests for generator, scanner, and virtual router utilities
  • Chores

    • Added test script, browser build config, TypeScript libs, .gitignore update, and CI workflow adjustments

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Dec 2, 2025

Walkthrough

End-to-end routing was rewritten: scanner, generator, and router now support async matching, nested layouts, dynamic and catch‑all segments; a new shared router-utils module plus virtual/client re-exports were added; many tests and fixtures were introduced; Vite was pinned in multiple package.json files; small playground and CI tweaks applied.

Changes

Cohort / File(s) Summary
Playground app updates
apps/playground/package.json, apps/playground/src/app/note.ts, apps/playground/src/server.ts, apps/playground/vite.config.ts, apps/playground/.gitignore
Pinned Vite to 7.2.6; DB insert now returns the insert result; removed a debug server log; moved oxide() plugin to the start of the plugins array; added .output to .gitignore.
Starter app
apps/starter/package.json
Pinned Vite to 7.2.6 (removed caret).
Framework package metadata & build
packages/framework/package.json, packages/framework/bunup.config.ts, packages/framework/tsconfig.json
Added ./client export and test script; pinned Vite to 7.2.6; added browser-targeted bunup build entry for src/client.ts; added ["ES2022","DOM"] to tsconfig libs.
Router component
packages/framework/src/components/router.svelte
Large async routing rewrite: exported Location/RouteParams types, async route matching, catch‑all/dynamic segment handling, component stack for nested layouts/pages, SSR init, improved 404/error handling, and recursive render traversal.
Route generator
packages/framework/src/generator.ts
Public API changed: generate now accepts RouteNode[] and returns GeneratedOutput { moduleCode, typeDefinitions }.
Route scanner
packages/framework/src/scanner.ts
Scanner default pagesDir changed to src/app; replaced tree build with routeMap → organize/processLayoutLevel pipeline; added filename validation, layout-child association, nested layout recursion, dynamic/catch‑all handling, and filters for hidden/node_modules entries.
Virtual module, shared utils & client
packages/framework/src/virtual.ts, packages/framework/src/shared/router-utils.ts, packages/framework/src/client.ts
New shared router-utils module with types, ROUTER_CONTEXT_KEY, useRouter, useRoute, href, setRouterContext; virtual.ts re-exports them; client.ts re-exports a browser-safe subset and types.
Framework public API
packages/framework/src/index.ts
Re-exported virtual module types and utilities (Location, RouteParams, Route, Router as RouterInterface, RouterContext, useRouter, useRoute, href, setRouterContext, ROUTER_CONTEXT_KEY).
Plugin cleanup
packages/framework/src/plugin.ts
Removed several debug logs; generator now receives filtered routes (hasComponent); simplified load() return handling; removed some hot-update logs.
Tests & fixtures (new)
packages/framework/tests/*, packages/framework/tests/fixtures/app/*
Added tests: generator.test.ts, scanner.test.ts, virtual.test.ts; added Svelte fixtures (auth/login, about, catch-all, index, users layout, users index, users/[id]) to exercise routing behaviors.
Other package.json updates
packages/framework/package.json, apps/playground/package.json, apps/starter/package.json
Pinned Vite devDependency to 7.2.6 across multiple package.json files and added ./client export + test script in framework package.json.
CI workflow
.github/workflows/build.yml
Renamed CI step to "Build framework", added "Run unit tests" step (working-directory packages/framework), added pull_request trigger, and narrowed publish to framework package only.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Browser
    participant Router as Router.svelte
    participant Scanner as RouteScanner
    participant Generator as RouteGenerator
    participant Utils as RouterUtils
    participant Layout
    participant Page

    Browser->>Router: navigate(path)
    Router->>Scanner: async findMatchingRoute(path)
    Scanner-->>Router: matched route nodes + params
    Router->>Generator: async resolve imports & helpers
    Generator-->>Router: moduleCode + typeDefinitions
    Router->>Utils: setRouterContext(routerContext)
    Router->>Layout: render layout stack
    Layout->>Page: render page component
    Page-->>Router: rendered fragment
    Router-->>Browser: update DOM
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Focus review areas:
    • packages/framework/src/components/router.svelte — async control flow, SSR initialization, nested render stack, error/404 handling.
    • packages/framework/src/scanner.ts — routeMap organization, filename validation, dynamic/catch‑all correctness, layout association.
    • packages/framework/src/generator.ts — emitted module structure, typeDefinitions shape, import handling.
    • packages/framework/src/shared/router-utils.ts & virtual/client exports — context safety, href encoding, SSR guards.
    • tests/fixtures — ensure tests align with routing semantics.

Poem

🐇 I hopped through code with nimble paws,

I threaded routes and mapped their claws,
Layouts stack and params sing,
SSR wakes up to routing spring,
A joyful hop — new paths and laws.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main objective: implementing framework code generation for preview functionality, which aligns with the substantial changes across the framework package.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/preview-fw-implementation

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 33c8f7e and e2c551a.

📒 Files selected for processing (1)
  • .github/workflows/build.yml (2 hunks)
🔇 Additional comments (3)
.github/workflows/build.yml (3)

6-7: Good: Added pull_request trigger for main branch.

Running the CI workflow on pull requests to main is a best practice that enables early validation before merge.


22-24: Test script is properly defined in packages/framework/package.json.

The test script exists with value "bun test", so the workflow step bun run test will execute correctly.


26-26: Remove concern about ORPC publishing—ORPC is now part of framework, not a separate package.

The workflow correctly publishes only ./packages/framework, which includes the ORPC module as part of the framework package. ORPC was merged into framework (see commit 5b5452d: "chore(packages): merge oxide and orpc"), so there is no separate ORPC package to publish. This is not a breaking change.

Likely an incorrect or invalid review comment.


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

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: 5

🧹 Nitpick comments (14)
apps/playground/src/app/note.ts (1)

10-15: Confirm the returned value shape and avoid leaking driver metadata

You now return whatever db.insert(note).values(...) yields. Please double‑check that this is actually the created note (or array of notes) and not a driver‑specific metadata object (e.g., a run result / command result). If consumers expect the created note, you likely want something along the lines of:

-      const result = await db.insert(note).values({
-        name: input.name,
-        content: input.content,
-        userId: context.user?.id,
-      });
-      return result;
+      const [createdNote] = await db
+        .insert(note)
+        .values({
+          name: input.name,
+          content: input.content,
+          userId: context.user.id,
+        })
+        .returning();
+
+      return createdNote;

This also avoids returning raw DB result structures and tight coupling to the DB client.

Minor: since you early‑return when !context.user, you can safely drop the optional chaining and use context.user.id directly.

packages/framework/src/scanner.ts (4)

1-2: Unused import: basename.

The basename function is imported but never used in the file.

-import { join, relative, extname, basename } from "node:path";
+import { join, relative, extname } from "node:path";

47-49: Consider logging or distinguishing error types.

The empty catch block handles the expected case of a missing directory, but also silently swallows permission errors or other unexpected failures. For debugging purposes, consider checking the error code or adding debug-level logging.

-    } catch {
-      // Directory doesn't exist
+    } catch (error: unknown) {
+      // Only silently ignore ENOENT (directory doesn't exist)
+      if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
+        console.warn(`Warning: Could not scan directory ${dir}:`, error);
+      }
     }

249-258: Unnecessary dot character in validation regex.

The comment mentions dots are allowed for catch-all routes like [...rest], but the three dots in [...rest] are stripped with the extension before validation. After removing .svelte from [...rest].svelte, the remaining [...rest] doesn't require literal dots in the pattern since they're inside brackets. Allowing dots could permit unexpected filenames like foo..bar.svelte.

-  // Allow: a-z, A-Z, 0-9, _, -, [, ], (, ), . (for catch-all routes like [...rest])
+  // Allow: a-z, A-Z, 0-9, _, -, [, ], (, ) for dynamic and catch-all routes
   // Reject files with spaces, @, $, and other special characters
-  const validPattern = /^[a-zA-Z0-9_\-\[\]().]+$/;
+  const validPattern = /^[a-zA-Z0-9_\-\[\]()]+$/;

119-202: Consider caching for performance with large route trees.

The processLayoutLevel method iterates over routeMap for each layout file found, resulting in O(n²) complexity. For typical applications with fewer than 100 routes this is fine, but for monorepos or large apps, consider building an index by path prefix for O(n) lookups.

packages/framework/src/generator.ts (3)

41-43: Window shadowing pattern may cause confusion.

Declaring const window shadows the global window object. While this works for SSR compatibility, consider using a more explicit name like browserWindow or globalWindow to avoid confusion during debugging.

-// Declare window for SSR compatibility
-const window = typeof globalThis !== 'undefined' && 'window' in globalThis ? globalThis.window : undefined;
+// Reference browser window for SSR compatibility (undefined in non-browser environments)
+const browserWindow = typeof globalThis !== 'undefined' && 'window' in globalThis ? globalThis.window : undefined;

Then update references on lines 61-62 and 66-67 accordingly.


353-360: Simplify catch-all parameter extraction.

The logic to find the catch-all parameter name is unnecessarily complex. It can be simplified by directly accessing the last element of the params array.

-      const catchAllParam = route.params?.find(p =>
-        route.params.indexOf(p) === route.params.length - 1
-      ) || 'catchAll';
+      const catchAllParam = route.params?.[route.params.length - 1] || 'catchAll';

291-295: Consider secondary sort criteria for route stability.

The sort only uses route type priority (static=1, dynamic=2, catch-all=3). Routes with the same priority retain their original order, which may vary. Consider adding path length or specificity as a secondary criterion for more deterministic matching.

   const sortedRoutes = allRoutes.sort((a, b) => {
     const aPriority = getRoutePriority(a.fullPath || a.path);
     const bPriority = getRoutePriority(b.fullPath || b.path);
-    return aPriority - bPriority;
+    if (aPriority !== bPriority) return aPriority - bPriority;
+    // Secondary: more segments = more specific
+    const aSegments = (a.fullPath || a.path).split('/').filter(Boolean).length;
+    const bSegments = (b.fullPath || b.path).split('/').filter(Boolean).length;
+    return bSegments - aSegments;
   });
packages/framework/tests/fixtures/app/(auth)/login.svelte (1)

2-6: Unused params prop.

The params prop is declared but never used in this component. Consider removing it if not needed, or use it to pre-fill form fields if that was the intent.

-  interface Props {
-    params?: Record<string, string>;
-  }
-
-  let { params = {} }: Props = $props();
+  // No props needed for this fixture
packages/framework/tests/scanner.test.ts (1)

33-38: Use dirname instead of join(path, "..") for parent directory.

Using join(fullPath, "..") is less readable than the idiomatic dirname(fullPath) from node:path.

+import { join, dirname } from "node:path";
...
  async function createFile(path: string, content = "<div>test</div>") {
    const fullPath = join(appDir, path);
-   const dir = join(fullPath, "..");
+   const dir = dirname(fullPath);
    await mkdir(dir, { recursive: true });
    await writeFile(fullPath, content);
  }
packages/framework/tests/virtual.test.ts (1)

225-276: TypeScript interface tests don't validate types at runtime.

These tests create object literals and check typeof on properties, but they don't actually verify TypeScript interface compliance. The typeof virtualModule.Location returns undefined at runtime since interfaces don't exist in JavaScript. Consider removing these tests or converting them to compile-time type checks in a separate .d.ts test file.

If you want to verify type exports, consider using a type-only test file:

// virtual.types.test.ts (compile-time only)
import type { Location, RouteParams, Route, Router } from "../src/virtual";

// These will fail compilation if types are wrong
const _location: Location = { pathname: "/", search: "", hash: "" };
const _params: RouteParams = { id: "123" };
packages/framework/src/components/router.svelte (3)

96-121: Missing await on async route search.

searchRoutes is declared as async and returns a Promise, but findMatchingRoute returns searchRoutes(routes) without awaiting it. This means findMatchingRoute returns a Promise, which is then awaited in updateLocation (line 226), so it works but the function signature is misleading.

Either make the outer function explicitly async and await the result, or make the naming clearer:

-  function findMatchingRoute(pathname: string): any {
+  async function findMatchingRoute(pathname: string): Promise<any> {
     async function searchRoutes(routeList: any[], parentPath = '', ancestors: any[] = []): Promise<any> {
       // ...
     }
 
-    return searchRoutes(routes);
+    return await searchRoutes(routes);
   }

449-462: Redundant props spreading.

The component receives params={currentParams} and also spreads {...currentParams}. This passes each parameter twice - once under params and once at the top level. If any param name conflicts with a component prop, this could cause issues.

Choose one approach for passing params:

    {#if comp.type === 'layout'}
-     <Component params={currentParams} {...currentParams}>
+     <Component params={currentParams}>
        {#snippet children()}
          {@render renderComponents(components, index + 1)}
        {/snippet}
      </Component>
    {:else}
-     <Component params={currentParams} {...currentParams} />
+     <Component params={currentParams} />
    {/if}

Or if spreading is preferred for convenience, document this convention and be aware of potential conflicts.


364-368: Silent error catch may hide issues.

The empty catch block silently swallows errors during SSR initialization. Consider logging at debug level or being more specific about expected exceptions.

    } catch {
-     // Contexts not available during client-side rendering
+     // Contexts not available during client-side rendering - this is expected
+     // and not an error condition
    }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 20866ba and 36b57e5.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (22)
  • apps/playground/package.json (1 hunks)
  • apps/playground/src/app/note.ts (1 hunks)
  • apps/playground/src/server.ts (0 hunks)
  • apps/playground/vite.config.ts (1 hunks)
  • packages/framework/package.json (1 hunks)
  • packages/framework/src/components/router.svelte (11 hunks)
  • packages/framework/src/generator.ts (4 hunks)
  • packages/framework/src/index.ts (1 hunks)
  • packages/framework/src/plugin.ts (1 hunks)
  • packages/framework/src/scanner.ts (8 hunks)
  • packages/framework/src/virtual.ts (1 hunks)
  • packages/framework/tests/fixtures/app/(auth)/login.svelte (1 hunks)
  • packages/framework/tests/fixtures/app/about.svelte (1 hunks)
  • packages/framework/tests/fixtures/app/catch-all.svelte (1 hunks)
  • packages/framework/tests/fixtures/app/index.svelte (1 hunks)
  • packages/framework/tests/fixtures/app/users.svelte (1 hunks)
  • packages/framework/tests/fixtures/app/users/[id].svelte (1 hunks)
  • packages/framework/tests/fixtures/app/users/index.svelte (1 hunks)
  • packages/framework/tests/generator.test.ts (1 hunks)
  • packages/framework/tests/scanner.test.ts (1 hunks)
  • packages/framework/tests/virtual.test.ts (1 hunks)
  • packages/framework/tsconfig.json (1 hunks)
💤 Files with no reviewable changes (1)
  • apps/playground/src/server.ts
🧰 Additional context used
🧬 Code graph analysis (4)
packages/framework/src/virtual.ts (1)
packages/framework/src/index.ts (8)
  • Location (13-13)
  • RouteParams (14-14)
  • Route (15-15)
  • Router (16-16)
  • useRouter (18-18)
  • useRoute (18-18)
  • href (18-18)
  • setRouterContext (18-18)
packages/framework/tests/generator.test.ts (2)
packages/framework/src/index.ts (2)
  • PluginContext (8-8)
  • RouteNode (6-6)
packages/framework/src/generator.ts (1)
  • RouteGenerator (3-467)
packages/framework/tests/scanner.test.ts (2)
packages/framework/src/index.ts (1)
  • PluginContext (8-8)
packages/framework/src/scanner.ts (1)
  • RouteScanner (5-355)
packages/framework/src/generator.ts (1)
packages/framework/src/types.ts (1)
  • RouteNode (27-38)
🔇 Additional comments (35)
apps/playground/vite.config.ts (1)

29-29: Plugin order is correct—oxide() must run first to provide virtual modules.

The oxide() plugin is correctly positioned first in the plugins array because it needs to register the virtual module system (e.g., $oxide) that downstream code depends on. The server.ts file imports from this virtual module (import { router } from "$oxide"), confirming that oxide() must initialize before other plugins process files.

The plugin order is not arbitrary—it's essential for the framework to function. No reordering is needed.

packages/framework/tsconfig.json (1)

2-5: LGTM!

The addition of DOM and ES2022 library typings aligns with the framework's new routing utilities that use URLSearchParams and modern JavaScript features.

packages/framework/tests/fixtures/app/index.svelte (1)

1-12: LGTM!

The component correctly demonstrates the props interface pattern used throughout the fixture set. The unused params prop is acceptable for a test fixture.

packages/framework/package.json (1)

29-30: LGTM!

The test script addition enables the new test suite introduced in this PR and follows Bun's standard testing conventions.

packages/framework/tests/fixtures/app/users/index.svelte (1)

1-21: LGTM!

The fixture appropriately demonstrates a user list component with hardcoded test data. The unused params prop maintains consistency with other fixtures in the test suite.

packages/framework/tests/fixtures/app/users.svelte (1)

1-17: LGTM!

The layout component correctly demonstrates Svelte 5's Snippet pattern for nested routing. The props interface and children rendering follow best practices.

packages/framework/tests/fixtures/app/users/[id].svelte (1)

1-22: LGTM!

The dynamic route component correctly demonstrates Svelte 5's $derived runes for reactive computation. The fallback to empty string for missing params and "Unknown User" for unrecognized IDs provides safe defaults for test scenarios.

packages/framework/src/plugin.ts (1)

301-304: LGTM!

The refactor to assign the result to a local variable before returning improves code readability while maintaining the same functionality. The cleanup of debug logs (mentioned in the summary) reduces noise during development.

packages/framework/src/virtual.ts (5)

1-35: LGTM!

The interface definitions are well-structured and the SSR-safe window declaration appropriately guards against server-side access. Using a Symbol for the context key is best practice to prevent naming collisions. The separation of public exports (Location, RouteParams, Route, Router) from internal types (RouterContext) maintains a clean API surface.


37-62: LGTM!

The useRouter() implementation correctly:

  • Validates context availability with a clear error message
  • Delegates navigation to the context's navigate method
  • Guards back() and forward() with SSR-safe window checks

The silent no-op behavior for history navigation in SSR environments is appropriate since server-side rendering has no browser history to manipulate.


64-80: LGTM!

The useRoute() implementation correctly extracts route information and creates a URLSearchParams instance for convenient query parameter access. The error handling is consistent with useRouter(), and the DOM typing support added in tsconfig.json enables the URLSearchParams usage.


82-100: LGTM!

The href() template tag helper provides a clean API for URL construction with proper handling of:

  • URLSearchParams for query strings
  • Arrays for path segments
  • Automatic encoding via encodeURIComponent for other values

The defensive ?? "" operators handle edge cases gracefully, and the explicit String() conversion before encoding ensures predictable behavior for all value types.


102-104: LGTM!

The setRouterContext() wrapper correctly delegates to Svelte's setContext with the private symbol key. This establishes the router context for downstream useRouter() and useRoute() calls.

packages/framework/src/index.ts (1)

10-18: LGTM! Clean public API consolidation.

The aliasing of Router type as RouterInterface correctly avoids naming collision with the Router component exported on line 3. The virtual module exports are well-organized.

packages/framework/src/scanner.ts (1)

85-117: LGTM! Layout organization logic is correct.

The algorithm properly handles both layout-based routes and standalone routes that aren't part of any layout hierarchy.

packages/framework/src/generator.ts (2)

399-430: LGTM! Type definitions are well-structured.

The generated types properly expose RouteRecord with optional fullPath, and the RouteParams interface provides strong typing for route parameters.


46-89: Well-implemented router hooks with clear error handling.

The useRouter and useRoute functions correctly check for context and provide helpful error messages. The SSR-safe window checks for back() and forward() are appropriate.

packages/framework/tests/fixtures/app/about.svelte (1)

1-12: LGTM as test fixture.

The component correctly uses Svelte 5 runes syntax. The params prop is declared but unused in the template, which is acceptable for a test fixture that may be testing prop passing without displaying the values.

packages/framework/tests/fixtures/app/catch-all.svelte (1)

1-18: LGTM! Good catch-all route fixture.

The component correctly demonstrates catch-all route parameter usage with Svelte 5's $derived rune. The conditional display of the path and fallback to empty string are appropriate patterns.

packages/framework/tests/scanner.test.ts (6)

40-49: LGTM!

Good test coverage for basic route file scanning with clear assertions on file detection.


75-93: LGTM!

Solid test coverage for dynamic route parameter extraction and naming conventions.


95-111: LGTM!

Good coverage of catch-all route handling with proper parameter extraction.


179-189: LGTM!

Good test for filtering out invalid file names - validates scanner security against potentially problematic file names.


250-259: LGTM!

Important test ensuring hidden directories and node_modules are properly excluded from scanning.


148-158: Child route paths are relative to parent layout by design.

The test correctly expects child routes under a layout to have relative paths ("/index" and "/profile"). This is the intended behavior in the scanner implementation—child routes are scoped relative to their parent layout, not absolute from the application root. Root-level routes have absolute paths (e.g., "/login", "/users/:id"), while layout children use relative paths within their parent scope.

packages/framework/tests/virtual.test.ts (2)

23-56: LGTM!

Good test coverage for useRouter including error handling when called outside context and verification of navigation methods.


147-206: LGTM!

Comprehensive test coverage for the href template tag with various interpolation scenarios including URL encoding, arrays, URLSearchParams, and edge cases.

packages/framework/tests/generator.test.ts (5)

23-36: LGTM!

Well-designed helper function with sensible defaults that reduces test boilerplate while allowing targeted overrides.


70-92: LGTM!

Good test verifying the default async import behavior with negative assertion to ensure sync imports aren't generated.


94-121: LGTM!

Properly tests the sync import mode by reconfiguring the context and verifying both import statement and component reference generation.


278-318: LGTM!

Comprehensive test for type definition generation including RouteRecord interface, RouteNames union type, RouteParams interface with per-route parameter types, and virtual module function signatures.


368-393: LGTM!

Important test ensuring non-component routes (layouts without their own component) are filtered from the generated output.

packages/framework/src/components/router.svelte (3)

4-24: LGTM!

Clean type definitions for Location and RouteParams with proper Symbol-based context key for isolation.


132-202: LGTM!

Well-structured route matching implementation with proper handling of exact matches, dynamic segments, and catch-all routes. The URL decoding of path segments is a good security practice.


278-314: LGTM!

Good separation of layout and page component loading with proper error handling and state management.

<div class="error">{error}</div>
{/if}

<form onsubmit={handleSubmit}>
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

Missing preventDefault on form submit.

The onsubmit handler doesn't prevent the default form submission, which will cause a full page reload before the handleSubmit logic completes.

-  <form onsubmit={handleSubmit}>
+  <form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
📝 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
<form onsubmit={handleSubmit}>
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
🤖 Prompt for AI Agents
In packages/framework/tests/fixtures/app/(auth)/login.svelte around line 35, the
form uses a plain onsubmit attribute and doesn't prevent the browser's default
submit behavior; replace it with Svelte's event modifier to prevent default and
wire the handler properly by changing the attribute to
on:submit|preventDefault={handleSubmit} (or alternatively ensure handleSubmit
receives the event and calls event.preventDefault()) so the page won't reload
before your submit logic runs.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/framework/src/components/router.svelte (1)

95-120: Missing await on async searchRoutes call.

searchRoutes is declared as async (line 96) but the call at line 119 is not awaited. This causes findMatchingRoute to return a Promise wrapping undefined instead of the actual match result.

-  return searchRoutes(routes);
+  return await searchRoutes(routes);
♻️ Duplicate comments (1)
packages/framework/src/components/router.svelte (1)

165-176: Catch-all parameter extraction logic remains fragile.

This was previously flagged: the condition route.params.indexOf(p) === route.params.length - 1 selects the last param regardless of whether it's actually the catch-all. This logic depends on implementation details of the scanner that may not hold.

🧹 Nitpick comments (2)
packages/framework/src/shared/router-utils.ts (1)

94-98: Array elements in href are not URL-encoded.

When an array is passed, value.join('/') concatenates without encoding individual elements. If array elements contain special characters (e.g., /, ?, #), the resulting URL could be malformed.

Consider encoding each array element:

     } else if (Array.isArray(value)) {
-      result += value.join("/");
+      result += value.map(v => encodeURIComponent(String(v))).join("/");
     } else {
packages/framework/src/components/router.svelte (1)

5-19: Consider importing types from the shared module instead of re-declaring.

Location, RouteParams, and RouterContext are already defined in packages/framework/src/shared/router-utils.ts. Re-declaring them here creates duplication that may drift out of sync.

- import { ROUTER_CONTEXT_KEY } from '@oxidejs/framework/client';
+ import { ROUTER_CONTEXT_KEY, type Location, type RouteParams, type RouterContext } from '@oxidejs/framework/client';

- export interface Location {
-   pathname: string;
-   search: string;
-   hash: string;
- }
-
- export interface RouteParams {
-   [key: string]: string;
- }
-
- interface RouterContext {
-   navigate: (path: string, options?: { replace?: boolean }) => void;
-   location: () => Location;
-   params: () => RouteParams;
- }
+ export type { Location, RouteParams };

Note: You'll need to add RouterContext to the client.ts exports if it's not already exported as a type.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8bf92e5 and 274c970.

📒 Files selected for processing (11)
  • packages/framework/bunup.config.ts (1 hunks)
  • packages/framework/package.json (2 hunks)
  • packages/framework/src/client.ts (1 hunks)
  • packages/framework/src/components/router.svelte (11 hunks)
  • packages/framework/src/generator.ts (5 hunks)
  • packages/framework/src/index.ts (1 hunks)
  • packages/framework/src/plugin.ts (2 hunks)
  • packages/framework/src/shared/router-utils.ts (1 hunks)
  • packages/framework/src/virtual.ts (1 hunks)
  • packages/framework/tests/generator.test.ts (1 hunks)
  • packages/framework/tests/virtual.test.ts (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • packages/framework/tests/virtual.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/framework/src/plugin.ts
  • packages/framework/tests/generator.test.ts
  • packages/framework/src/index.ts
🧰 Additional context used
🧬 Code graph analysis (1)
packages/framework/src/generator.ts (2)
packages/framework/src/index.ts (1)
  • RouteNode (6-6)
packages/framework/src/types.ts (1)
  • RouteNode (27-38)
🔇 Additional comments (10)
packages/framework/src/shared/router-utils.ts (2)

3-30: LGTM!

The type definitions are well-structured and provide a clear contract for the routing API surface.


32-39: SSR-safe window shim is correctly implemented.

The guard for globalThis.window ensures safe operation during server-side rendering.

packages/framework/bunup.config.ts (1)

13-19: LGTM!

The browser-targeted build configuration correctly complements the main Node build and aligns with the new ./client export in package.json.

packages/framework/package.json (2)

18-21: LGTM!

The new ./client export correctly exposes the browser-safe module surface with proper type definitions.


39-39: Vite 7.2.6 is a patch release with no breaking changes. This version is safe to use and does not introduce any compatibility concerns with the framework.

packages/framework/src/client.ts (1)

1-11: LGTM!

The browser-safe client module correctly re-exports only the minimal runtime value (ROUTER_CONTEXT_KEY) and types needed for client-side use, avoiding Node.js dependencies.

packages/framework/src/components/router.svelte (2)

3-3: Good fix for the Symbol mismatch issue.

Importing ROUTER_CONTEXT_KEY from @oxidejs/framework/client ensures the same Symbol instance is used across the codebase, resolving the previously flagged critical issue.


448-462: LGTM!

The recursive renderComponents snippet cleanly handles nested layout rendering with proper slot-based composition for child components.

packages/framework/src/virtual.ts (1)

1-12: LGTM!

Clean barrel export that centralizes the public API surface from the shared router utilities module.

packages/framework/src/generator.ts (1)

220-257: LGTM on type generation.

The generated TypeScript interfaces correctly mirror the runtime types and provide proper type safety for route names and parameters.

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 (2)
packages/framework/src/components/router.svelte (1)

169-179: Simplify catch-all parameter extraction logic.

The catch-all parameter extraction combines multiple fallback strategies (startsWith('catch-'), position-based, literal 'catchAll'), making it fragile. Consider simplifying by relying on a single, consistent convention from the scanner.

Apply this diff to simplify:

-        // Look for explicit catch- prefixed params, fallback to 'catchAll'
-        const catchAllParam = route.params?.find((p: string) => p.startsWith('catch-')) || 'catchAll';
-
         const remainingSegments = pathSegments.slice(catchAllIndex);
-        const paramName = catchAllParam.startsWith('catch-')
-          ? catchAllParam.replace('catch-', '')
-          : catchAllParam;
+        // Use the last param as catch-all, defaulting to 'catchAll' if none defined
+        const paramName = route.params?.[route.params.length - 1] || 'catchAll';
 
         params[paramName] = remainingSegments.join('/');
packages/framework/src/generator.ts (1)

84-91: Enhance matchRoute to support catch-all routes.

The current implementation only handles basic dynamic segments (:param) but not catch-all routes (*), which are now supported by the router. Consider extending the regex pattern to handle catch-all segments.

Apply this diff to add catch-all support:

 export function matchRoute(pathname) {
   const flattened = flattenRoutes(routes);
   return flattened.find(route => {
-    const routePath = route.path.replace(/:[^/]+/g, '[^/]+');
+    const routePath = route.path
+      .replace(/:[^/]+/g, '[^/]+')  // Dynamic segments
+      .replace(/\*/g, '.*');         // Catch-all segments
     const regex = new RegExp(\`^\${routePath}$\`);
     return regex.test(pathname);
   });
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 274c970 and eeba56e.

📒 Files selected for processing (5)
  • packages/framework/src/components/router.svelte (11 hunks)
  • packages/framework/src/generator.ts (4 hunks)
  • packages/framework/tests/fixtures/app/(auth)/login.svelte (1 hunks)
  • packages/framework/tests/generator.test.ts (1 hunks)
  • packages/framework/tests/virtual.test.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/framework/tests/fixtures/app/(auth)/login.svelte
🧰 Additional context used
🧬 Code graph analysis (2)
packages/framework/tests/generator.test.ts (2)
packages/framework/src/index.ts (2)
  • PluginContext (8-8)
  • RouteNode (6-6)
packages/framework/src/generator.ts (1)
  • RouteGenerator (9-346)
packages/framework/src/generator.ts (2)
packages/framework/src/index.ts (1)
  • RouteNode (6-6)
packages/framework/src/types.ts (1)
  • RouteNode (27-38)
🔇 Additional comments (7)
packages/framework/tests/generator.test.ts (1)

1-423: Excellent test coverage for RouteGenerator.

The test suite comprehensively validates the new generate() API surface and covers all major scenarios including async/sync imports, route parameters, metadata, aliases, nested routes, helper functions, type definitions, catch-all routes, and edge cases like routes without parameters. The tests are well-structured and follow a consistent pattern.

packages/framework/tests/virtual.test.ts (1)

1-268: Comprehensive test suite for the virtual module API.

The tests thoroughly validate all public exports (useRouter, useRoute, href, setRouterContext) and their behavior, including error handling when called outside router context, URL construction with encoding, and TypeScript interface contracts. The use of mock.module and dynamic imports ensures clean test isolation.

packages/framework/src/components/router.svelte (2)

228-262: Robust error handling in updateLocation.

The try-catch block properly wraps the async route matching logic and handles all failure scenarios (no match, catch-all fallback, errors), ensuring the router state remains consistent.


452-467: Clean recursive rendering pattern for nested layouts.

The renderComponents snippet elegantly handles nested layout rendering by recursively calling itself for children. The distinction between layout and page components is clear, and the implementation follows Svelte 5 conventions.

packages/framework/src/generator.ts (3)

44-49: Correct use of shared module re-exports.

Re-exporting router utilities from the shared module ensures a single ROUTER_CONTEXT_KEY Symbol instance is used throughout the codebase, preventing context mismatch issues.


287-308: Well-structured parameter type generation.

The generateParamInterfaces method correctly generates per-route parameter typings, using Record<string, never> for routes without parameters and properly handling nested routes through recursion.


72-82: This review concern is not valid. The template literal escaping at lines 78 and 88 is correct and produces syntactically valid JavaScript. The escaped backticks (\``) in the template literal source code properly render as literal backticks in the generated output, creating valid template literals (e.g., `` :${key}` ``) that will interpolate correctly at runtime. The generated utility functions have been verified to be syntactically correct.

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: 3

🧹 Nitpick comments (2)
packages/framework/src/generator.ts (1)

168-171: Optional: Simplify child filtering logic.

The second .filter((entry) => entry) is redundant since processRoute already returns "" for routes without components (line 135), and the first filter already checks hasComponent.

For clarity, you could either:

  1. Keep both filters for explicitness (current approach is fine)
  2. Remove the second filter and rely on the first:
       const childEntries = route.children
         .filter((child) => child.hasComponent)
-        .map((child) => processRoute(child, depth + 1))
-        .filter((entry) => entry);
+        .map((child) => processRoute(child, depth + 1));
packages/framework/tests/generator.test.ts (1)

1-423: LGTM! Comprehensive test coverage for the generator.

The test suite thoroughly covers:

  • Basic route generation
  • Import mode switching (async/sync)
  • Route features (parameters, meta, aliases, nesting)
  • Helper function generation
  • Type definition generation
  • Edge cases (catch-all params, routes without components)

Consider adding tests for edge cases:

  • Empty route arrays
  • Routes with circular references in children
  • Invalid route data (missing required fields)
  • Very deeply nested routes (performance/stack overflow)

These would improve robustness but are not critical for the current implementation.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eeba56e and 33c8f7e.

📒 Files selected for processing (4)
  • apps/playground/.gitignore (1 hunks)
  • packages/framework/src/client.ts (1 hunks)
  • packages/framework/src/generator.ts (4 hunks)
  • packages/framework/tests/generator.test.ts (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • apps/playground/.gitignore
🧰 Additional context used
🧬 Code graph analysis (2)
packages/framework/tests/generator.test.ts (1)
packages/framework/src/generator.ts (1)
  • RouteGenerator (9-346)
packages/framework/src/generator.ts (2)
packages/framework/src/index.ts (1)
  • RouteNode (6-6)
packages/framework/src/types.ts (1)
  • RouteNode (27-38)
🔇 Additional comments (7)
packages/framework/src/client.ts (3)

1-3: LGTM!

The comment block clearly explains the purpose and rationale for this browser-safe client module.


4-10: All exported symbols exist in the source module and no Node.js dependencies detected.

The exports for ROUTER_CONTEXT_KEY, useRouter, useRoute, href, and setRouterContext are all properly defined in ./shared/router-utils.ts. No Node.js-specific code (fs, path, process, etc.) was detected, confirming browser-safety.


11-17: Verify type exports and renaming consistency.

The Router type is renamed to RouterInterface on export. All five exported types (Location, RouteParams, Router, Route, RouterContext) are confirmed to exist in the source module ./shared/router-utils.js. The renaming is intentional and consistent: both client.ts and index.ts use Router as RouterInterface to avoid naming conflicts with the Router component also exported from index.ts.

packages/framework/src/generator.ts (4)

1-7: LGTM! Clean interface definition.

The new GeneratedOutput interface properly separates module code from type definitions, improving the API surface for consumers.


16-42: LGTM! Public API change is well-designed.

The updated signature generate(routes: RouteNode[]): GeneratedOutput provides better structure for consumers who need both module code and type definitions separately.


44-49: LGTM! Symbol duplication issue properly resolved.

Re-exporting from the shared module ensures a single Symbol instance is used across the codebase, correctly addressing the previous review concern.


209-345: LGTM! Type generation is comprehensive and handles edge cases.

The type generation methods properly handle empty routes (using "never"), generate per-route parameter types, and create a complete module declaration for downstream consumers.

Comment on lines +72 to +82
export function generatePath(name, params = {}) {
const route = findRouteByName(name);
if (!route) return null;
let path = route.path;
Object.entries(params).forEach(([key, value]) => {
path = path.replace(new RegExp(\`:\${key}\`, 'g'), encodeURIComponent(String(value)));
});
return 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 | 🟡 Minor

Performance: RegExp created on every iteration.

The generatePath function creates a new RegExp object for each parameter, which is inefficient when a route has multiple parameters.

Consider caching the regex or using a single pass replacement:

 export function generatePath(name, params = {}) {
   const route = findRouteByName(name);
   if (!route) return null;
 
-  let path = route.path;
-  Object.entries(params).forEach(([key, value]) => {
-    path = path.replace(new RegExp(\`:\${key}\`, 'g'), encodeURIComponent(String(value)));
-  });
-
-  return path;
+  return route.path.replace(/:([^/]+)/g, (match, paramName) => {
+    return paramName in params 
+      ? encodeURIComponent(String(params[paramName]))
+      : match;
+  });
 }
🤖 Prompt for AI Agents
In packages/framework/src/generator.ts around lines 72–82, the current loop
creates a new RegExp for each param which is wasteful; change to a single-pass
replacement or cache regexes: replace the loop with one call to
path.replace(/:([A-Za-z0-9_]+)/g, (m, key) => { if key in params return
encodeURIComponent(String(params[key])); return m; }) or, if you prefer per-key
regexes, build a Map of RegExp objects outside the loop and reuse them when
replacing each param so you avoid allocating a new RegExp per iteration.

Comment on lines +84 to +91
export function matchRoute(pathname) {
const flattened = flattenRoutes(routes);
return flattened.find(route => {
const routePath = route.path.replace(/:[^/]+/g, '[^/]+');
const regex = new RegExp(\`^\${routePath}$\`);
return regex.test(pathname);
});
}
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

Incomplete route matching: catch-all and optional parameters not handled.

The matchRoute function only handles dynamic segments (:param) but doesn't support catch-all routes (*) or optional parameters, which are mentioned in the test suite and likely supported by the scanner.

Verify the scanner's route pattern capabilities and ensure matchRoute handles all supported patterns:

#!/bin/bash
# Check if scanner produces catch-all or optional parameter patterns
rg -nC3 --type=ts "catch.*all|optional.*param|\\\*|\\[.*\\]" packages/framework/src/scanner.ts
🤖 Prompt for AI Agents
In packages/framework/src/generator.ts around lines 84 to 91, matchRoute
currently only converts :param segments to [^/]+ and builds a RegExp, which
misses catch-all (*) and optional parameters (e.g. :id? or [..rest] style) used
by the scanner; update the conversion logic to: first escape literal regex
characters in route.path, then replace catch-all tokens (e.g. '*' or '[...name]'
or ':...name') with '.*' (or '(?:.*)' as appropriate), replace optional params
(':name?' or '[name?]') with '(?:/[^/]+)?' so the segment is optional, and
replace required params (':name' or '[name]') with '/[^/]+'. Ensure
leading/trailing slashes are handled consistently and anchors ^ and $ remain;
rebuild the RegExp from that transformed string so matchRoute supports dynamic,
optional and catch-all patterns consistent with the scanner.

Comment on lines +93 to +106
export function getRouteParams(pathname, route) {
const params = {};
const pathParts = pathname.split('/').filter(Boolean);
const routeParts = route.path.split('/').filter(Boolean);
return `${imports}${routesArray}\n\nexport default routes;\n\n${helpers}`;
routeParts.forEach((part, index) => {
if (part.startsWith(':')) {
const paramName = part.slice(1);
params[paramName] = pathParts[index];
}
});
return params;
}
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

Missing URI decoding and catch-all support in parameter extraction.

getRouteParams should decode URI components (user input may contain encoded characters) and doesn't handle catch-all routes that capture multiple segments.

Apply this diff to decode parameters:

 export function getRouteParams(pathname, route) {
   const params = {};
   const pathParts = pathname.split('/').filter(Boolean);
   const routeParts = route.path.split('/').filter(Boolean);
 
   routeParts.forEach((part, index) => {
     if (part.startsWith(':')) {
       const paramName = part.slice(1);
-      params[paramName] = pathParts[index];
+      params[paramName] = decodeURIComponent(pathParts[index] || '');
     }
   });
 
   return params;
 }

Additionally, verify if catch-all routes need special handling here.

📝 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
export function getRouteParams(pathname, route) {
const params = {};
const pathParts = pathname.split('/').filter(Boolean);
const routeParts = route.path.split('/').filter(Boolean);
return `${imports}${routesArray}\n\nexport default routes;\n\n${helpers}`;
routeParts.forEach((part, index) => {
if (part.startsWith(':')) {
const paramName = part.slice(1);
params[paramName] = pathParts[index];
}
});
return params;
}
export function getRouteParams(pathname, route) {
const params = {};
const pathParts = pathname.split('/').filter(Boolean);
const routeParts = route.path.split('/').filter(Boolean);
routeParts.forEach((part, index) => {
if (part.startsWith(':')) {
const paramName = part.slice(1);
params[paramName] = decodeURIComponent(pathParts[index] || '');
}
});
return params;
}

@pkg-pr-new
Copy link

pkg-pr-new bot commented Dec 2, 2025

Open in StackBlitz

npm i https://pkg.pr.new/oxidejs/oxide/@oxidejs/framework@1

commit: e2c551a

@yzuyr yzuyr merged commit dc45f6c into main Dec 2, 2025
3 checks passed
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.

2 participants