Skip to content

feat: add fetch cache tags support and refactor for cognitive complexity#154

Merged
skiniks merged 17 commits intomainfrom
cold-gibbon
Mar 18, 2026
Merged

feat: add fetch cache tags support and refactor for cognitive complexity#154
skiniks merged 17 commits intomainfrom
cold-gibbon

Conversation

@skiniks
Copy link
Copy Markdown
Member

@skiniks skiniks commented Mar 17, 2026

Summary by CodeRabbit

  • New Features

    • Tag-aware fetch caching: responses now include merged tags.
    • Deployment helpers for Railway and Render with guided next-steps.
  • Improvements

    • Better proxy handling and header/result propagation.
    • Centralized navigation metadata, refined error handling, improved same-route/hash navigation.
    • Faster concurrent image scanning, enhanced sitemap generation, and more robust client-component loading.
    • Sidebar navigation refactor with improved expansion and ARIA behavior.
  • Bug Fixes

    • Route parameter matching corrected for edge cases.
  • Chores

    • Multiple dependency bumps; removal of legacy runtime shims and unused worker scripts.

skiniks added 3 commits March 17, 2026 13:24
- Update clap from 4.5.60 to 4.6.0
- Update tokio-tungstenite and tungstenite from 0.28.0 to 0.29.0
- Update image from 0.25.9 to 0.25.10
- Update tracing-subscriber from 0.3.22 to 0.3.23
- Update tempfile from 3.26.0 to 3.27.0
- Update @antfu/eslint-config from 7.7.2 to 7.7.3
- Update @typescript-eslint/utils from 8.57.0 to 8.57.1
- Update eslint-plugin-oxlint from 1.55.0 to 1.56.0
- Update knip from 5.86.0 to 5.87.0
- Update @sentry/react from 10.43.0 to 10.44.0
- Update @typescript/native-preview to 7.0.0-dev.20260317.1
- Update vite-plus from 0.1.11 to 0.1.12
- Add oxlint disable comment for nextTick function in init_node.js
- Rename REACT_INIT_SCRIPT to STREAMING_REACT_SETUP_SCRIPT for clarity
- Rename react_init.js to streaming_react_setup.js to match constant naming
- Extract Railway deployment logic into focused helper functions (updatePackageJsonForRailway, createRailwayToml, updateGitignoreForRailway, updateReadmeForRailway, printRailwaySuccessMessage)
- Extract Render deployment logic into focused helper functions (updatePackageJsonForRender, createRenderYaml, updateGitignoreForRender, updateReadmeForRender, printRenderSuccessMessage)
- Improve code organization by separating concerns and reducing function complexity
- Update all references to use new constant and file names across renderer and deployment modules
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 17, 2026

📝 Walkthrough

Walkthrough

This PR removes legacy React global shims, streaming and module-reload runtime extensions, and HMR stats; centralizes many resolvers/utilities; adds tag-aware fetch caching and proxy result processing; refactors router, deploy, Vite/image scanning, and proxy subsystems; and updates multiple dependency versions.

Changes

Cohort / File(s) Summary
Dependency updates
Cargo.toml, crates/rari/Cargo.toml, pnpm-workspace.yaml, packages/create-rari-app/.../package.json, web/package.json
Multiple dependency version bumps and small package.json script edits.
React runtime & RSC rendering
crates/rari/src/rsc/.../constants.rs, crates/rari/src/rsc/rendering/core/js/component_eval_setup.js, crates/rari/src/rsc/rendering/core/js/react_globals_setup.js, crates/rari/src/rsc/rendering/core/renderer.rs, crates/rari/src/rsc/rendering/core/utils.rs
Removed legacy React global shims and render/use polyfills; introduced a guarded minimal runtime in places and removed related init calls/constants.
Streaming React setup
crates/rari/src/rsc/rendering/streaming/constants.rs, crates/rari/src/rsc/rendering/streaming/renderer.rs, crates/rari/src/rsc/rendering/streaming/js/streaming_init.js, crates/rari/src/rsc/rendering/streaming/js/streaming_react_setup.js
Switched streaming init constant and removed dynamic React loading / createElement monkey-patching in streaming init script.
Runtime extensions & module-reload removal
crates/rari/src/runtime/ext/..., crates/rari/src/runtime/module_reload/*
Dropped streaming and module_reload extensions, removed HMR stats/history APIs, simplified configs and removed many module-reload runtime helpers.
Module loader / node stubs
crates/rari/src/runtime/module_loader/core.rs, crates/rari/src/runtime/module_loader/node_stubs.rs
Removed React/JSX-specific stubs and special-cases; moved to generic resolution and added CJS wrapping where needed.
Fetch caching + tags (client & server)
crates/rari/src/runtime/ext/web/init_fetch.js, crates/rari/src/runtime/ops.rs, crates/rari/src/server/middleware/request_context.rs
Added tag extraction to fetch init, included tags in cache keys, propagated tags to Rust cache calls, and merged/deduplicated tags server-side; op now returns tags field.
Proxy centralization & executor changes
packages/rari/src/proxy/shared/utils.ts, packages/rari/src/proxy/execute-proxy.ts, packages/rari/src/proxy/matcher.ts, packages/rari/src/proxy/runtime-executor.ts, packages/rari/src/proxy/executor.ts
Introduced shared proxy types and processProxyResult, moved matching helpers to dedicated functions, centralized result handling, added executor init deduplication (initializationPromise) and getProxyFunction(), and updated waitUntil handling.
Deploy tooling refactor & helpers
packages/deploy/src/utils.ts, packages/deploy/src/railway.ts, packages/deploy/src/render.ts
Extracted provider utilities (getRariVersion, updatePackageJsonForProvider, createOrBackupConfigFile, updateGitignoreForProvider) and refactored provider flows to use these helpers; create*Deployment functions changed from async to sync wrappers.
Vite resolution & image scanner
packages/rari/src/vite/alias-resolver.ts, packages/rari/src/vite/file-resolver.ts, packages/rari/src/vite/index.ts, packages/rari/src/vite/server-build.ts, packages/rari/src/vite/image-scanner.ts
Added resolveAlias/resolveWithExtensions/resolveIndexFile utilities and integrated them; rewrote image scanner to use async/promises, concurrency limiting, rolldown transforms, and safer extraction.
Router refactors & metadata handling
packages/rari/src/router/ClientRouter.tsx, packages/rari/src/router/navigation-utils.ts, packages/rari/src/router/navigation-error-handler.ts, packages/rari/src/router/props-extractor.ts, packages/rari/src/router/robots-generator.ts, packages/rari/src/router/sitemap-generator.ts
Modularized metadata updates, streaming vs non-streaming navigation handlers, centralized navigation error classification, refactored route-segment matching signature (matchRouteParams adds actualPath), and reorganized sitemap/robots generation flows.
Client component resolution & runtime
packages/rari/src/runtime/shared/get-client-component.ts, packages/rari/src/runtime-client.ts, crates/rari/src/rsc/rendering/streaming/renderer.rs
Introduced many resolution helpers (resolveById/Path/Name, getComponentFromInfo), hardened export handling for lazy components, added assertInitialized on runtime client, and switched streaming renderer to use the new setup script constant.
Shared utilities & minor UX
packages/rari/src/shared/http-utils.ts, packages/rari/src/shared/timer-utils.ts, packages/rari/src/vite/hmr-coordinator.ts, packages/rari/src/vite/hmr-error-handler.ts, packages/rari/src/cli.ts, web/src/components/Sidebar.tsx, web/src/lib/docs-navigation.ts
Added throwIfNotOk/assertResponseOk and clearTimer helpers; refactored CLI package-manager detection; modularized Sidebar/navigation rendering and exported NavItem with optional collapsible.
Tests & small edits
test/unit/router/navigation-utils.test.ts, test/unit/router/navigation-error-handler.test.ts, various minor runtime JS files
Updated tests for matchRouteParams failure cases, removed a navigation-error browser-event test, and applied small lint/comment adjustments.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hopped through code with nimble feet,

Trimmed old shims and made helpers neat.
Tags now travel, proxies sing in tune,
Routers dance beneath a brighter moon.
A crunchy carrot for the dev-team soon!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The pull request has no description provided by the author, but the template requires sections like Summary, Type of Change, Motivation and Context, Changes Made, Testing, and other important details. Add a comprehensive pull request description covering: what changes were made and why, type of change (new feature/refactoring), related issues, motivation/context, specific code/API changes, testing performed, and performance impact.
Docstring Coverage ⚠️ Warning Docstring coverage is 7.73% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main changes: adding fetch cache tags support and refactoring for cognitive complexity, which aligns with the substantial code changes throughout the codebase.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cold-gibbon
📝 Coding Plan
  • Generate coding plan for human review comments

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.

Copy link
Copy Markdown

@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: 25

Caution

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

⚠️ Outside diff range comments (8)
packages/deploy/src/railway.ts (1)

7-44: 🧹 Nitpick | 🔵 Trivial

Consider extracting shared deployment logic to reduce duplication.

updatePackageJsonForRailway is nearly identical to updatePackageJsonForRender in render.ts. The same applies to createRailwayToml/createRenderYaml, updateGitignoreForRailway/updateGitignoreForRender, etc.

Consider extracting shared logic into parameterized helpers in utils.ts:

// Example shared helper signature
function updatePackageJsonForProvider(cwd: string, options: {
  providerName: string
  deployScript: string
}) { ... }

This would reduce maintenance burden and ensure consistent behavior across providers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/deploy/src/railway.ts` around lines 7 - 44, The two functions
updatePackageJsonForRailway and updatePackageJsonForRender (and the parallel
createRailwayToml/createRenderYaml and
updateGitignoreForRailway/updateGitignoreForRender) duplicate deployment logic;
extract the shared behavior into parameterized helpers in utils.ts (e.g.,
updatePackageJsonForProvider(cwd, { providerName, deployScript, startScript,
dependency })) and refactor each provider-specific function to call these
helpers, moving common steps such as reading/writing package.json, backing up
existing start script, adding scripts/dependencies, and ensureMinimumNodeEngine
into the shared helpers to eliminate duplication and keep provider files thin.
packages/rari/src/proxy/runtime-executor.ts (1)

167-171: ⚠️ Potential issue | 🟡 Minor

Fire-and-forget Promise.allSettled has unreachable .catch().

Same issue as in execute-proxy.ts. Promise.allSettled never rejects.

Proposed fix
         if (waitUntilPromises.length > 0) {
-          Promise.allSettled(waitUntilPromises).catch((error) => {
-            console.error('[rari] Proxy: waitUntil promise failed:', error)
-          })
+          Promise.allSettled(waitUntilPromises).then((results) => {
+            for (const result of results) {
+              if (result.status === 'rejected') {
+                console.error('[rari] Proxy: waitUntil promise failed:', result.reason)
+              }
+            }
+          })
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/proxy/runtime-executor.ts` around lines 167 - 171, The code
calls Promise.allSettled(waitUntilPromises).catch(...), but allSettled never
rejects so the .catch is unreachable; update the logic in runtime-executor.ts to
remove the .catch and instead handle outcomes by calling
Promise.allSettled(waitUntilPromises).then(results => { inspect results for
status === 'rejected' and log errors }), referencing the waitUntilPromises
variable so any rejected promise reasons are logged (or otherwise handled)
inside the .then callback.
packages/rari/src/proxy/execute-proxy.ts (1)

168-172: ⚠️ Potential issue | 🟡 Minor

Fire-and-forget Promise.allSettled has unreachable .catch().

Promise.allSettled never rejects—it always fulfills with an array of settlement objects. The .catch() handler will never execute.

Proposed fix - remove unreachable catch or handle individual rejections
     if (waitUntilPromises.length > 0) {
-      Promise.allSettled(waitUntilPromises).catch((error) => {
-        console.error('[rari] Proxy: waitUntil promise failed:', error)
-      })
+      Promise.allSettled(waitUntilPromises).then((results) => {
+        for (const result of results) {
+          if (result.status === 'rejected') {
+            console.error('[rari] Proxy: waitUntil promise failed:', result.reason)
+          }
+        }
+      })
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/proxy/execute-proxy.ts` around lines 168 - 172, The call to
Promise.allSettled inside the execute-proxy logic uses a .catch on a promise
that never rejects; remove the unreachable .catch and either (A) simply await or
call Promise.allSettled(waitUntilPromises) without .catch, or (B) handle
individual failures by inspecting the returned array of settlement objects and
logging any entries with status === 'rejected'; update the code in
execute-proxy.ts where waitUntilPromises is processed (look for the
Promise.allSettled(...) call) to implement one of these approaches and ensure
rejected promises are logged or handled appropriately.
crates/rari/src/runtime/ops.rs (1)

292-300: 🧹 Nitpick | 🔵 Trivial

Return tags consistently from op_fetch_with_cache.

Line 298 adds tags only in one branch. Keep the response shape stable by including tags in all branches (empty array when unavailable).

Diff suggestion
                 Ok(serde_json::json!({
                     "ok": true,
                     "status": result.status,
                     "statusText": http_status_text(result.status),
                     "body": body_str,
                     "headers": headers_obj,
                     "cached": result.was_cached,
                     "tags": result.tags
                 }))
             }
             Err(e) => {
                 error!("Fetch failed for {}: {}", url, e);
                 Ok(serde_json::json!({
                     "ok": false,
                     "status": 500,
                     "statusText": "Internal Server Error",
                     "error": e.to_string(),
-                    "cached": false
+                    "cached": false,
+                    "tags": []
                 }))
             }
         }
     } else {
         match perform_simple_fetch(&url, &options).await {
             Ok((status, body, headers)) => Ok(serde_json::json!({
                 "ok": (200..300).contains(&status),
                 "status": status,
                 "statusText": http_status_text(status),
                 "body": body,
                 "headers": headers,
-                "cached": false
+                "cached": false,
+                "tags": []
             })),
             Err(e) => {
                 error!("Fetch failed for {}: {}", url, e);
                 Ok(serde_json::json!({
                     "ok": false,
                     "status": 500,
                     "statusText": "Internal Server Error",
                     "error": e,
-                    "cached": false
+                    "cached": false,
+                    "tags": []
                 }))
             }
         }
     }

Also applies to: 304-331

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/rari/src/runtime/ops.rs` around lines 292 - 300, The JSON response
from op_fetch_with_cache currently includes "tags" only in one branch (using
result.tags) which makes the response shape inconsistent; update all branches of
the op_fetch_with_cache response (including the alternate branches around lines
304-331) to always emit a "tags" field—use result.tags when available and an
empty array (e.g., Vec::new() / [] equivalent) when not—to ensure a stable
response shape across all Ok(...) and Err/... return paths.
crates/rari/src/server/middleware/request_context.rs (1)

117-131: ⚠️ Potential issue | 🟠 Major

Tags are dropped on cache hits, so tag coverage becomes incomplete.

On Line 122-131, a valid cached entry is returned immediately. The new tags parsed on Line 117-118 are only applied on Line 166-168 (fresh fetch path), so later requests with additional tags never update the cached entry.

Diff suggestion
         {
             let mut cache = self.fetch_cache.lock();
             if let Some(cached) = cache.get(&cache_key) {
                 let ttl_ms =
                     options.get("cacheTTLMs").and_then(|t| t.parse::<u64>().ok()).unwrap_or(60_000);

                 let elapsed_ms = cached.cached_at.elapsed().as_millis();

                 if elapsed_ms < ttl_ms as u128 {
                     let mut result = cached.clone();
+                    if !tags.is_empty() {
+                        for tag in &tags {
+                            if !result.tags.contains(tag) {
+                                result.tags.push(tag.clone());
+                            }
+                        }
+                        cache.put(cache_key.clone(), result.clone());
+                    }
                     result.was_cached = true;
                     return Ok(result);
                 }
                 cache.pop(&cache_key);
             }
         }

Also applies to: 166-168

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/rari/src/server/middleware/request_context.rs` around lines 117 - 131,
Cached responses returned from the fetch_cache currently ignore newly-parsed
tags (variable tags) so tag coverage never grows; update the cache-hit path in
the request context (where fetch_cache.lock(), cache.get(&cache_key),
cached.clone() and result.was_cached are used) to merge/union the newly-parsed
tags into the cached entry before returning, and likewise ensure the fresh-fetch
path (the code that applies tags on reply) also unions incoming tags with stored
tags so cache entries are updated with any new tags instead of dropping them.
packages/rari/src/router/robots-generator.ts (1)

197-204: ⚠️ Potential issue | 🟡 Minor

Validate that the dynamic robots file has a default export.

If the dynamic robots file lacks a default export, module.default will be undefined, causing generateRobotsTxt to fail with an unclear error. Add explicit validation.

🛡️ Proposed fix
     const module = await import(dataUrl)

+    if (module.default === undefined) {
+      throw new Error(`Robots file at ${robotsFile.path} must have a default export`)
+    }
+
     let robotsData: Robots
     if (typeof module.default === 'function') {
       const robotsResult = module.default()
       robotsData = robotsResult instanceof Promise ? await robotsResult : robotsResult
     }
     else {
       robotsData = module.default
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/router/robots-generator.ts` around lines 197 - 204, Check
that the imported dynamic robots module actually has a default export before
using it: in the robots-generator logic that assigns robotsData from
module.default, add an explicit validation that module && module.default !==
undefined; if missing, throw a clear error (or return a rejected Promise)
explaining the robots file must export a default (either an object or a
function). Keep the existing handling for a function export (call it and await
if it returns a Promise) and for a non-function default (assign directly to
robotsData), but ensure you surface a descriptive error immediately when
module.default is absent so generateRobotsTxt doesn't fail with an unclear
message.
packages/rari/src/image/Image.tsx (2)

97-129: 🧹 Nitpick | 🔵 Trivial

useImageLazyLoad is currently a no-op for network deferral.

The observer never gates src/srcSet; requests are still initiated immediately by render. This adds complexity without changing lazy-loading behavior. Either remove the hook and rely on native loading="lazy", or have it return visibility state and bind src/srcSet only after intersection.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/image/Image.tsx` around lines 97 - 129, The
useImageLazyLoad hook currently observes but doesn't prevent network requests;
change it to return a visibility boolean (e.g., isVisible) that becomes true
when the IntersectionObserver reports intersection and remains true thereafter,
ensure it respects shouldPreload, unoptimized, and loading === 'eager' (return
true immediately in those cases), clean up the observer on unmount, and update
the Image component to only set img src/srcSet when isVisible is true (or when
preload/unoptimized/eager require immediate load) so requests are deferred until
visible.

299-314: ⚠️ Potential issue | 🟠 Major

Guard AVIF/WEBP <source> emission when loader is set.

buildSrcSetString cannot encode format through loader, so Line 301-Line 314 can emit type="image/avif" / type="image/webp" sources that are not truly format-specific. This can lead to incorrect source selection behavior.

💡 Proposed fix
 function OptimizedImage({
@@
 }) {
   const defaultWidth = imgWidth || 1920
   const sizesArray = imgWidth ? [imgWidth] : DEFAULT_DEVICE_SIZES
+  const canEmitFormatSources = !loader
@@
   return (
     <picture ref={pictureRef}>
-      {DEFAULT_FORMATS.includes('avif') && (
+      {canEmitFormatSources && DEFAULT_FORMATS.includes('avif') && (
         <source
           type="image/avif"
           srcSet={buildSrcSetString(sizesArray, finalSrc, quality, 'avif', loader)}
           sizes={sizes}
         />
       )}
-      {DEFAULT_FORMATS.includes('webp') && (
+      {canEmitFormatSources && DEFAULT_FORMATS.includes('webp') && (
         <source
           type="image/webp"
           srcSet={buildSrcSetString(sizesArray, finalSrc, quality, 'webp', loader)}
           sizes={sizes}
         />
       )}
       {imgElement}
     </picture>
   )
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/image/Image.tsx` around lines 299 - 314, The AVIF/WEBP
<source> elements are emitted even when a custom loader is provided (loader),
but buildSrcSetString cannot force format through a loader; update the
conditional around the DEFAULT_FORMATS checks in the JSX (where pictureRef is
used and buildSrcSetString is called with sizesArray, finalSrc, quality,
'avif'/'webp', loader) to only render those <source> tags when no custom loader
is set (e.g., loader is null/undefined) or when the loader explicitly supports a
format query; in short, add a guard like "&& !loader" (or use a capability flag
on the loader) to the DEFAULT_FORMATS.includes('avif') and
DEFAULT_FORMATS.includes('webp') conditions so format-specific sources are only
emitted when buildSrcSetString can actually produce that format.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/deploy/src/railway.ts`:
- Around line 46-60: The function createRailwayDeployment is marked async but
contains no await calls (it only invokes updatePackageJsonForRailway,
createRailwayToml, updateGitignoreForRailway, updateReadmeForRailway, and
printRailwaySuccessMessage), so remove the unnecessary async keyword from the
createRailwayDeployment declaration to make it a regular synchronous function;
alternatively, if any of those helper functions are meant to be async, update
the calls to await them and keep async—prefer removing async here for the
current synchronous calls.

In `@packages/deploy/src/render.ts`:
- Around line 46-60: The function createRenderDeployment is marked async but
contains no await or asynchronous work; remove the async keyword from its
declaration to avoid misleading callers (or alternatively convert helper
functions like updatePackageJsonForRender, createRenderYaml,
updateGitignoreForRender, updateReadmeForRender and printRenderSuccessMessage to
return Promises and use await inside createRenderDeployment). Locate
createRenderDeployment and either drop the async modifier or update the
referenced helper functions to async equivalents and add awaits accordingly so
the function's signature matches its implementation.

In `@packages/rari/src/cli.ts`:
- Around line 82-84: The empty catch {} silently swallows errors; replace it
with catch (err) { /* explain why errors are ignored (e.g., best-effort read of
package.json) */ } and optionally emit a low-verbosity diagnostic (e.g.,
console.debug or the module logger) such as console.debug("Failed to read
package metadata:", err) so maintainers see malformed/permission errors while
preserving the fallback behavior in the surrounding code that reads package
metadata.

In `@packages/rari/src/proxy/execute-proxy.ts`:
- Around line 96-102: The empty catch in the body extraction block (in
execute-proxy.ts where result.text(), result.body and variable body are handled)
silently swallows errors; modify the catch to log the caught error at
debug/verbose level (using the module's existing logger such as processLogger or
logger, or console.debug if no logger exists) while still allowing execution to
continue, so failures extracting the response body are recorded for
troubleshooting without breaking proxy flow.

In `@packages/rari/src/proxy/matcher.ts`:
- Around line 72-73: Change the loose condition shape to a discriminated union
for condition.type (use 'header' | 'query' | 'cookie') and update the resolver
logic in the matcher functions that accept the parameter (the function whose
signature includes condition: { type: string, key: string, value?: string }) so
each branch is explicitly handled; replace any string-based checks with a switch
on condition.type, handle 'header', 'query', and 'cookie' cases, and add an
explicit default/unreachable path that throws an error so unknown types (e.g.,
'missing') cannot silently pass; also ensure the code that reads condition.value
is covered per-branch (or validated) so value resolution is exhaustive and
type-safe.
- Around line 83-86: The matching logic in matcher.ts incorrectly treats empty
string as falsy: change the checks that use if (!actualValue) and if
(condition.value && actualValue !== condition.value) to explicit null/undefined
checks so empty strings are allowed; specifically, replace the falsy test on
actualValue with a null/undefined check (e.g., actualValue == null) and replace
the truthy test on condition.value with an explicit undefined check (e.g.,
condition.value !== undefined) before comparing actualValue and condition.value,
updating the relevant code paths in the function that references actualValue and
condition.value so has/missing and value:'' cases behave correctly.

In `@packages/rari/src/proxy/runtime-executor.ts`:
- Around line 29-139: The functions and interfaces duplicated between
runtime-executor.ts and execute-proxy.ts (extractProxyHeaders, checkForRewrite,
checkForRedirect, handleContinueWithHeaders, handleDirectResponse,
processProxyResult, and the SimpleRequest/SimpleProxyResult interfaces) should
be moved into a new shared module (e.g., proxy-helpers.ts) that exports the
interfaces and those helper functions; update runtime-executor.ts and
execute-proxy.ts to import these symbols from the shared module and remove the
local duplicates so both files reuse the single implementation for consistency
and easier maintenance.
- Around line 97-104: The empty catch silently swallows errors when extracting
the response body (the block that assigns to the local variable `body` using
`result.text()` or `result.body`); update the catch to capture the thrown error
(e.g., `err`) and emit a debug-level log including context (e.g., "failed to
extract body from proxy result") using the project's logger used elsewhere
(mirroring the same fix from `execute-proxy.ts`), then preserve the current
fallback behavior so `body` stays undefined on failure.

In `@packages/rari/src/router/ClientRouter.tsx`:
- Around line 320-343: The history entry is being written too early in
updateHistoryState; change call sites so updateHistoryState (and any direct
window.history.replaceState/pushState at the other referenced locations) are
invoked only after the navigation fetch completes successfully. Specifically,
move the creation/persistence of HistoryState (route, navigationId,
scrollPosition, timestamp, key) and the pushState/replaceState call to the
success branch of the fetch/route-loading logic that triggers navigation
(instead of before the request), and ensure on fetch failure you do not modify
history or the address bar so currentRoute and rendered content remain
consistent.
- Around line 197-210: The updateDocumentMetadata function currently only
updates metadata keys that are present, causing stale head tags to persist when
a route omits keys; modify updateDocumentMetadata to explicitly clear/remove
metadata for keys that are absent by calling the appropriate cleanup routines
(e.g., remove or reset title/description via updateBasicMetadata, remove
canonical via updateCanonicalLink when metadata.canonical is undefined, remove
robots via updateRobotsMetadata when metadata.robots is undefined, and similarly
clear OG and Twitter tags via updateOpenGraphMetadata and updateTwitterMetadata
when their objects are missing); ensure each helper function
(updateBasicMetadata, updateCanonicalLink, updateRobotsMetadata,
updateOpenGraphMetadata, updateTwitterMetadata) supports removal semantics so
omitted fields remove the corresponding <meta>/<link> tags from <head>.
- Around line 515-519: completeNavigation() updates currentRoute and clears
error but doesn't persist the new navigationId, so subsequent navigate() calls
keep reusing the old id; update the setNavigationState call in
completeNavigation() (and the similar updates around the other occurrences) to
also write the new navigationId (the id you compute/derive for this successful
navigation, e.g., nextId or computedNavigationId) into
navigationState.navigationId so the next navigate() will increment from the
persisted value and emitted events/history entries remain uniquely correlated.
- Around line 395-430: The streaming UTF-8 decoder's internal buffer isn't
flushed after the read loop, so trailing partial multi-byte bytes can be lost;
after the while loop that reads from response.body.getReader() (where reader,
decoder, buffer are used and abort handling calls
cleanupAbortedNavigation(actualTargetPath, navigationId)), call decoder.decode()
with no arguments and append the result to buffer (i.e., buffer +=
decoder.decode()) before the final if (buffer.trim()) check so any remaining
bytes are emitted.

In `@packages/rari/src/router/navigation-error-handler.ts`:
- Around line 137-143: The network-error detection currently only checks error
instanceof TypeError && error.message.includes('fetch'), which misses
browser-specific fetch failure messages (e.g., Firefox "NetworkError...", Safari
"Load failed"). Update the condition that decides to call handleNetworkError to
match a broader set of failure messages (for example by testing error.message
against a case-insensitive regex covering "failed to fetch", "networkerror",
"load failed" and similar tokens) while keeping the TypeError check; change the
conditional in navigation-error-handler.ts (the branch that invokes
handleNetworkError(error, url)) to use that broader message test so Safari and
other browsers' fetch failures are routed to handleNetworkError instead of
handleUnknownError.

In `@packages/rari/src/router/props-extractor.ts`:
- Around line 181-193: The current mergeTitleField collapses a parent title
template into a plain string when an intermediate segment provides a string,
losing the template for deeper segments; update mergeTitleField so that if
parentTitle is an object with a template and childTitle is a string you do not
replace the template with the interpolated string but instead return the parent
title object (preserving parentTitle.template) so deeper merges can still apply
the template (i.e., in mergeTitleField, when typeof childTitle === 'string' and
typeof parentTitle === 'object' && parentTitle.template, return parentTitle
rather than parentTitle.template.replace('%s', childTitle)).
- Around line 132-136: The default ServerSidePropsResult object currently
materializes optional fields (e.g., notFound, revalidate, redirect) on the
success path; change the initial state in props-extractor.ts so it only sets
props: {} (do not set notFound/redirect/revalidate) and let the existing helpers
(the functions that mutate/return the ServerSidePropsResult) add those optional
fields when needed; locate the const state: ServerSidePropsResult and remove the
extra optional keys so the shape remains sparse unless the helpers explicitly
set them.

In `@packages/rari/src/router/robots-generator.ts`:
- Line 28: The generateRuleLines function currently types its parameter as any;
replace that with a concrete Robots rule type to restore type safety by either
extracting the rule type from the imported Robots type (e.g., use
Robots["rules"] or the appropriate member/type alias) or by declaring an
explicit interface (e.g., RobotsRule with userAgent, allow, disallow,
crawlDelay) and changing the signature to function generateRuleLines(rule:
RobotsRule): string[]; update any internal references to match the chosen type
so TypeScript validates property access.

In `@packages/rari/src/router/sitemap-generator.ts`:
- Around line 313-328: The generateMultipleSitemaps function uses untrusted
sitemap IDs directly in file paths; validate or sanitize each id returned by
module.generateSitemaps() before writing. Ensure you build outputPath using the
safe sitemapDir (not outDir) and normalize/resolve the final path, then verify
the resolved path starts with sitemapDir; if it does not, throw an error. Also
sanitize the id (e.g., strip path separators or allow only a safe pattern)
before using it in filenames so module.default({ id }) and the written file name
cannot perform path traversal.
- Around line 112-125: In addVideoComplexFields, the relationship attribute
values for video.restriction and video.platform are interpolated unescaped;
update the template strings to escape those attribute values (e.g., call
escapeXml on video.restriction.relationship and video.platform.relationship) so
special XML characters are encoded before being placed inside the
relationship="..." attribute; keep the existing escapeXml usage for content and
uploader.info as-is.
- Around line 157-185: Replace the unsafe any types on the sitemap helper
functions with the concrete sitemap entry type exported by your Sitemap
definition (e.g., import and use the SitemapEntry or SitemapItem
interface/type). Update the signatures of addSitemapEntryFields,
addAlternateLanguages, and addMediaContent to accept that specific type, and
adjust any internal property accesses (entry.lastModified,
entry.alternates.languages, entry.images, entry.videos) to reflect the real type
shape so the compiler can verify presence/optional properties instead of relying
on any.

In `@packages/rari/src/runtime/shared/get-client-component.ts`:
- Around line 123-124: The code currently uses a truthy check "if (component)
return component" which incorrectly treats valid falsy exports (e.g., 0, '',
false) as unresolved; update the check to a nullish check so only null/undefined
are treated as missing (for example replace with "if (component != null) return
component" or "if (component !== null && component !== undefined) return
component"), referencing the local variable "component" in get-client-component
to ensure valid falsy exports are returned.
- Around line 141-143: The sync resolver in get-client-component.ts normalizes
leading "./" before calling resolveByPath (using normalizedPath), but
getClientComponentAsync still uses the raw path causing mismatches; update
getClientComponentAsync to perform the same normalization (e.g., compute
normalizedPath = path.startsWith('./') ? path.slice(2) : path) and use that
normalizedPath when calling resolveByPath with clientComponents and
clientComponentPaths so sync and async lookups are consistent.

In `@packages/rari/src/vite/image-scanner.ts`:
- Around line 199-211: The fallback path in extractImageUsages silently drops
aliased imports from 'rari/image' because extractImageUsagesWithRegex only
searches for literal <Image ...> tags; compute the set of aliases from the
original source (e.g., by parsing import statements for "rari/image" or adding a
helper extractImageAliases(content)) before the transform attempt, and pass that
alias set into extractImageUsagesWithRegex so the regex fallback also matches
usages like <HeroImage ...> or named-import aliases; update the
extractImageUsagesWithRegex signature and all other fallback call sites (the
other try/catch around lines 215-220) to accept and use the alias set, and keep
existing behavior for when no aliases are found.
- Around line 184-191: The current regex in processImageIdentifiers
(createElementRegex) fails on nested prop objects because `([^}]+)` stops at the
first `}`; replace the flat-regex approach with brace-balanced extraction or
proper AST traversal: either (A) after locating `React.createElement(
<identifier> ,` find the `{` index in transformedCode and scan forward counting
braces until the matching `}` to extract the full propsString, then pass that to
parseImageProps, or (B) parse transformedCode into an AST and walk for
CallExpressions whose callee is `React.createElement` and whose first argument
matches the identifier, then extract the ObjectExpression props node and adapt
parseImageProps to accept the AST node; update processImageIdentifiers, remove
reliance on ESCAPE_REGEX-based createElementRegex, and ensure parseImageProps
can handle the extracted full props content.

In `@web/src/components/Sidebar.tsx`:
- Around line 439-459: The pure helpers shouldExpandSection and shouldExpandItem
are defined inside the Sidebar component and get recreated each render; move
them to top-level (outside the Sidebar function) as plain functions so they
aren’t reallocated on every render. Extract both functions (preserving their
signatures that reference NavItem and pathname: string | null), place them above
the Sidebar component, and export them if needed elsewhere; update any
imports/types used by these functions so they compile after relocation.
- Line 145: The check using the hardcoded string 'Getting Started' in the
expression that sets showSectionChevron is fragile; replace it with a stable
identifier by adding an id or collapsible flag to the navigation data and use
that instead (e.g., read section.id === 'getting-started' or
section.collapsible). Update the NavItem type to include the chosen property and
change the logic in the Sidebar where showSectionChevron, hasSectionItems, and
section.label are referenced so the chevron decision uses section.id or
section.collapsible rather than the literal label.

---

Outside diff comments:
In `@crates/rari/src/runtime/ops.rs`:
- Around line 292-300: The JSON response from op_fetch_with_cache currently
includes "tags" only in one branch (using result.tags) which makes the response
shape inconsistent; update all branches of the op_fetch_with_cache response
(including the alternate branches around lines 304-331) to always emit a "tags"
field—use result.tags when available and an empty array (e.g., Vec::new() / []
equivalent) when not—to ensure a stable response shape across all Ok(...) and
Err/... return paths.

In `@crates/rari/src/server/middleware/request_context.rs`:
- Around line 117-131: Cached responses returned from the fetch_cache currently
ignore newly-parsed tags (variable tags) so tag coverage never grows; update the
cache-hit path in the request context (where fetch_cache.lock(),
cache.get(&cache_key), cached.clone() and result.was_cached are used) to
merge/union the newly-parsed tags into the cached entry before returning, and
likewise ensure the fresh-fetch path (the code that applies tags on reply) also
unions incoming tags with stored tags so cache entries are updated with any new
tags instead of dropping them.

In `@packages/deploy/src/railway.ts`:
- Around line 7-44: The two functions updatePackageJsonForRailway and
updatePackageJsonForRender (and the parallel createRailwayToml/createRenderYaml
and updateGitignoreForRailway/updateGitignoreForRender) duplicate deployment
logic; extract the shared behavior into parameterized helpers in utils.ts (e.g.,
updatePackageJsonForProvider(cwd, { providerName, deployScript, startScript,
dependency })) and refactor each provider-specific function to call these
helpers, moving common steps such as reading/writing package.json, backing up
existing start script, adding scripts/dependencies, and ensureMinimumNodeEngine
into the shared helpers to eliminate duplication and keep provider files thin.

In `@packages/rari/src/image/Image.tsx`:
- Around line 97-129: The useImageLazyLoad hook currently observes but doesn't
prevent network requests; change it to return a visibility boolean (e.g.,
isVisible) that becomes true when the IntersectionObserver reports intersection
and remains true thereafter, ensure it respects shouldPreload, unoptimized, and
loading === 'eager' (return true immediately in those cases), clean up the
observer on unmount, and update the Image component to only set img src/srcSet
when isVisible is true (or when preload/unoptimized/eager require immediate
load) so requests are deferred until visible.
- Around line 299-314: The AVIF/WEBP <source> elements are emitted even when a
custom loader is provided (loader), but buildSrcSetString cannot force format
through a loader; update the conditional around the DEFAULT_FORMATS checks in
the JSX (where pictureRef is used and buildSrcSetString is called with
sizesArray, finalSrc, quality, 'avif'/'webp', loader) to only render those
<source> tags when no custom loader is set (e.g., loader is null/undefined) or
when the loader explicitly supports a format query; in short, add a guard like
"&& !loader" (or use a capability flag on the loader) to the
DEFAULT_FORMATS.includes('avif') and DEFAULT_FORMATS.includes('webp') conditions
so format-specific sources are only emitted when buildSrcSetString can actually
produce that format.

In `@packages/rari/src/proxy/execute-proxy.ts`:
- Around line 168-172: The call to Promise.allSettled inside the execute-proxy
logic uses a .catch on a promise that never rejects; remove the unreachable
.catch and either (A) simply await or call Promise.allSettled(waitUntilPromises)
without .catch, or (B) handle individual failures by inspecting the returned
array of settlement objects and logging any entries with status === 'rejected';
update the code in execute-proxy.ts where waitUntilPromises is processed (look
for the Promise.allSettled(...) call) to implement one of these approaches and
ensure rejected promises are logged or handled appropriately.

In `@packages/rari/src/proxy/runtime-executor.ts`:
- Around line 167-171: The code calls
Promise.allSettled(waitUntilPromises).catch(...), but allSettled never rejects
so the .catch is unreachable; update the logic in runtime-executor.ts to remove
the .catch and instead handle outcomes by calling
Promise.allSettled(waitUntilPromises).then(results => { inspect results for
status === 'rejected' and log errors }), referencing the waitUntilPromises
variable so any rejected promise reasons are logged (or otherwise handled)
inside the .then callback.

In `@packages/rari/src/router/robots-generator.ts`:
- Around line 197-204: Check that the imported dynamic robots module actually
has a default export before using it: in the robots-generator logic that assigns
robotsData from module.default, add an explicit validation that module &&
module.default !== undefined; if missing, throw a clear error (or return a
rejected Promise) explaining the robots file must export a default (either an
object or a function). Keep the existing handling for a function export (call it
and await if it returns a Promise) and for a non-function default (assign
directly to robotsData), but ensure you surface a descriptive error immediately
when module.default is absent so generateRobotsTxt doesn't fail with an unclear
message.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7dfa4fb2-e2f1-4dc6-b117-1f9c21e5d517

📥 Commits

Reviewing files that changed from the base of the PR and between 33e70ce and 2a82dcc.

⛔ Files ignored due to path filters (2)
  • Cargo.lock is excluded by !**/*.lock
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (28)
  • Cargo.toml
  • crates/rari/Cargo.toml
  • crates/rari/src/rsc/rendering/streaming/constants.rs
  • crates/rari/src/rsc/rendering/streaming/js/streaming_react_setup.js
  • crates/rari/src/rsc/rendering/streaming/renderer.rs
  • crates/rari/src/runtime/ext/node/init_node.js
  • crates/rari/src/runtime/ext/web/init_fetch.js
  • crates/rari/src/runtime/ops.rs
  • crates/rari/src/server/middleware/request_context.rs
  • packages/create-rari-app/templates/default/package.json
  • packages/deploy/src/railway.ts
  • packages/deploy/src/render.ts
  • packages/rari/src/cli.ts
  • packages/rari/src/image/Image.tsx
  • packages/rari/src/proxy/execute-proxy.ts
  • packages/rari/src/proxy/matcher.ts
  • packages/rari/src/proxy/runtime-executor.ts
  • packages/rari/src/router/ClientRouter.tsx
  • packages/rari/src/router/navigation-error-handler.ts
  • packages/rari/src/router/navigation-utils.ts
  • packages/rari/src/router/props-extractor.ts
  • packages/rari/src/router/robots-generator.ts
  • packages/rari/src/router/sitemap-generator.ts
  • packages/rari/src/runtime/shared/get-client-component.ts
  • packages/rari/src/vite/image-scanner.ts
  • pnpm-workspace.yaml
  • web/package.json
  • web/src/components/Sidebar.tsx

Comment thread packages/deploy/src/railway.ts Outdated
Comment thread packages/deploy/src/render.ts Outdated
Comment thread packages/rari/src/cli.ts
Comment thread packages/rari/src/proxy/execute-proxy.ts Outdated
Comment thread packages/rari/src/proxy/matcher.ts Outdated
Comment thread packages/rari/src/runtime/shared/get-client-component.ts Outdated
Comment thread packages/rari/src/vite/image-scanner.ts Outdated
Comment thread packages/rari/src/vite/image-scanner.ts Outdated
Comment thread web/src/components/Sidebar.tsx Outdated
Comment thread web/src/components/Sidebar.tsx Outdated
- Extract shared deployment logic into reusable provider-agnostic utilities
- Create http-utils.ts and timer-utils.ts for common HTTP and timing operations
- Add alias-resolver.ts and file-resolver.ts to vite plugin for improved module resolution
- Refactor railway.ts and render.ts to use updatePackageJsonForProvider and updateGitignoreForProvider
- Implement createOrBackupConfigFile utility for consistent config file handling across providers
- Add cache tag accumulation logic in request_context.rs to preserve tags across cache hits
- Update fetch response objects to include empty tags array for consistency
- Consolidate navigation and routing error handling in ClientRouter and navigation-error-handler
- Improve HMR coordination and error handling in vite plugin
- Reduce code duplication across deployment providers by centralizing common patterns
Copy link
Copy Markdown

@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: 21

Caution

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

⚠️ Outside diff range comments (4)
packages/rari/src/vite/hmr-error-handler.ts (1)

24-25: 🧹 Nitpick | 🔵 Trivial

Inconsistent timer clearing pattern.

recordError() still uses the inline if (this.resetTimer) clearTimeout(this.resetTimer) pattern while reset() and dispose() use the new clearTimer utility. Consider using clearTimer here as well for consistency.

♻️ Suggested refactor for consistency
   recordError(error: Error): void {
     this.errorCount++
     this.lastError = error
 
-    if (this.resetTimer)
-      clearTimeout(this.resetTimer)
+    this.resetTimer = clearTimer(this.resetTimer)
 
     this.resetTimer = setTimeout(() => {
       this.reset()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/vite/hmr-error-handler.ts` around lines 24 - 25,
recordError currently clears the timeout with an inline check ("if
(this.resetTimer) clearTimeout(this.resetTimer)"); replace that with the shared
utility clearTimer to match reset() and dispose(). Update the recordError method
to call clearTimer(this.resetTimer) (and ensure any resulting value handling
matches how reset() and dispose() manage this.resetTimer) so timer clearing is
consistent across the HMR error handler.
packages/rari/src/proxy/execute-proxy.ts (1)

29-31: 🧹 Nitpick | 🔵 Trivial

Consider typing the executor interface to avoid any cast.

The (executor as any).proxyFn assertion bypasses type checking. If feasible, consider extending the executor's type definition to include proxyFn.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/proxy/execute-proxy.ts` around lines 29 - 31, The code is
bypassing type-checking by using (executor as any).proxyFn; update the
executor's type instead: extend or create an interface/type (e.g.,
ExecutorWithProxy or augment the existing Executor) that includes proxyFn?:
Function (or the specific function signature) and change the function parameter
or local variable type so you can access proxyFn without casting; alternatively
add a narrow type guard that asserts executor has proxyFn before using
it—replace the (executor as any).proxyFn access with a typed access to the
proxyFn property on the newly typed executor so TypeScript enforces the shape
and signature of proxyFn.
packages/deploy/src/render.ts (1)

48-122: 🧹 Nitpick | 🔵 Trivial

Consider extracting shared provider README/message generation to utilities.

This block duplicates the Railway implementation pattern almost verbatim. A small provider-templating helper would reduce future drift and cognitive complexity.

Also applies to: 124-142

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/deploy/src/render.ts` around lines 48 - 122, The Render README
generation duplicates logic used for other providers; extract the shared
templating into a utility (e.g., a new function like
createProviderReadmeSection(providerName: string, body: string) or
formatProviderReadme(provider: 'render'|'railway', contentTemplate: string) and
move the provider-specific block (currently renderReadmeSection inside
updateReadmeForRender) into a provider-specific template passed to that helper;
then update updateReadmeForRender to call the utility (use the same helper from
the Railway implementation) to generate the final README content and reuse the
helper for create-or-update logic so file existence, append vs create, and
logSuccess calls are handled consistently.
packages/deploy/src/utils.ts (1)

123-133: ⚠️ Potential issue | 🟠 Major

ensureMinimumNodeEngine currently under-enforces the declared minimum version.

MIN_NODE_VERSION is >=22.12.0, but the sufficiency logic effectively checks only major version in many cases. For example, engines.node: ">=22.0.0" can pass even though it allows unsupported 22.0–22.11.

Suggested fix (enforce full semver floor, not only major)
 export const MIN_NODE_VERSION = '>=22.12.0'
+const LOWER_BOUND_SEMVER_REGEX = /^>=?\s*(\d+)\.(\d+)\.(\d+)/
+
+function parseLowerBoundSemver(range: string): [number, number, number] | null {
+  const m = range.trim().match(LOWER_BOUND_SEMVER_REGEX)
+  if (!m) return null
+  return [Number.parseInt(m[1], 10), Number.parseInt(m[2], 10), Number.parseInt(m[3], 10)]
+}
+
+function compareSemver(a: [number, number, number], b: [number, number, number]): number {
+  if (a[0] !== b[0]) return a[0] - b[0]
+  if (a[1] !== b[1]) return a[1] - b[1]
+  return a[2] - b[2]
+}
 
 export function ensureMinimumNodeEngine(packageJson: any, minVersion: string = MIN_NODE_VERSION): boolean {
   packageJson.engines = packageJson.engines || {}
@@
   if (packageJson.engines.node) {
-    if (!isNodeVersionSufficient(packageJson.engines.node, minMajor)) {
+    const required = parseLowerBoundSemver(minVersion)
+    const current = parseLowerBoundSemver(packageJson.engines.node)
+    const isSufficient = required && current
+      ? compareSemver(current, required) >= 0
+      : isNodeVersionSufficient(packageJson.engines.node, minMajor)
+
+    if (!isSufficient) {
       logWarn(`Current engines.node value "${packageJson.engines.node}" may not meet the required minimum of ${minVersion}`)
       logWarn(`Updating to ${minVersion} for deployment compatibility`)
       packageJson.engines.node = minVersion
       return true
     }

Also applies to: 125-145

♻️ Duplicate comments (3)
packages/rari/src/router/props-extractor.ts (1)

178-190: ⚠️ Potential issue | 🟠 Major

Title template is collapsed for intermediate segments, breaking deeper inheritance.

This issue was previously flagged: when an intermediate layout provides a string title and a parent provides a template, the template is interpolated into a plain string. Subsequent page titles then lose access to the template.

Example chain:

  • Root: { template: '%s | Site' }
  • Docs layout: 'Docs' → merged: 'Docs | Site' (now a string)
  • API page: 'API' → merged: 'API' (parent is string, no template)

Expected final: 'API | Site'

To preserve the template for deeper segments, return an object that keeps the template intact:

,

Proposed fix
 function mergeTitleField(parentTitle: MetadataResult['title'], childTitle: MetadataResult['title']): MetadataResult['title'] {
   if (childTitle === undefined)
     return parentTitle
 
   if (typeof childTitle === 'string') {
-    if (typeof parentTitle === 'object' && parentTitle?.template)
-      return parentTitle.template.replace('%s', childTitle)
+    if (typeof parentTitle === 'object' && parentTitle?.template) {
+      return {
+        ...parentTitle,
+        default: parentTitle.template.replace('%s', childTitle),
+      }
+    }
 
     return childTitle
   }
 
   return childTitle
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/router/props-extractor.ts` around lines 178 - 190, The
mergeTitleField function collapses templates when childTitle is a string by
returning the interpolated string; instead, when parentTitle is an object with a
template and childTitle is a string, return an object that preserves
parentTitle.template and also contains the interpolated/rendered title (so
deeper segments can still inherit the template). Update mergeTitleField
(handling of parentTitle and childTitle) to return an object with the original
template plus a rendered/title field (using parentTitle.template.replace('%s',
childTitle)) rather than returning the plain string.
packages/rari/src/router/robots-generator.ts (1)

36-51: 🧹 Nitpick | 🔵 Trivial

Consider grouping User-Agent lines before directives.

The current implementation emits Allow/Disallow lines for each User-Agent separately. While functionally correct, the standard robots.txt convention is to group multiple User-Agent lines together before their shared directives, resulting in a more compact output.

Current output for {userAgent: ['*', 'Googlebot'], allow: '/public'}:

User-Agent: *
Allow: /public

User-Agent: Googlebot
Allow: /public

Conventional format:

User-Agent: *
User-Agent: Googlebot
Allow: /public
♻️ Proposed refactor
 function generateRuleLines(rule: RobotsRule): string[] {
   const lines: string[] = []
   const userAgents = normalizeUserAgents(rule.userAgent)
 
-  for (const userAgent of userAgents) {
+  for (const userAgent of userAgents)
     lines.push(`User-Agent: ${userAgent}`)
 
-    const allows = normalizeArray(rule.allow)
-    for (const allow of allows)
-      lines.push(`Allow: ${allow}`)
+  const allows = normalizeArray(rule.allow)
+  for (const allow of allows)
+    lines.push(`Allow: ${allow}`)
 
-    const disallows = normalizeArray(rule.disallow)
-    for (const disallow of disallows)
-      lines.push(`Disallow: ${disallow}`)
+  const disallows = normalizeArray(rule.disallow)
+  for (const disallow of disallows)
+    lines.push(`Disallow: ${disallow}`)
 
-    if (rule.crawlDelay !== undefined)
-      lines.push(`Crawl-delay: ${rule.crawlDelay}`)
+  if (rule.crawlDelay !== undefined)
+    lines.push(`Crawl-delay: ${rule.crawlDelay}`)
 
-    lines.push('')
-  }
+  lines.push('')
 
   return lines
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/router/robots-generator.ts` around lines 36 - 51, The
generator currently iterates userAgents and emits directives per-agent; instead
change the loop to emit all User-Agent lines first and then the shared
directives to group them: for each rule (the loop using userAgents and rule),
first push each `User-Agent: ${userAgent}` for all entries in `userAgents`, then
push the normalized `Allow` lines from `normalizeArray(rule.allow)` and
`Disallow` lines from `normalizeArray(rule.disallow)`, then conditionally push
`Crawl-delay: ${rule.crawlDelay}` if defined, and finally a blank line into
`lines`; keep using the same `normalizeArray`, `rule.crawlDelay`, and `lines`
identifiers so behavior remains the same but output is grouped.
packages/rari/src/runtime/shared/get-client-component.ts (1)

137-142: ⚠️ Potential issue | 🟠 Major

Generate ./ path aliases from one shared helper in both resolvers.

The sync branch still never tries ./${path} for bare ids, and the async branch's multiline template literal currently produces \n./${path} instead of ./${path}. As written, foo#Bar still misses whenever ~clientComponentPaths is keyed as ./foo.

Suggested fix
+function getPathCandidates(path: string): string[] {
+  return path.startsWith('./') ? [path, path.slice(2)] : [path, `./${path}`]
+}
+
 function resolveByPathWithExport(
   id: string,
   clientComponents: Record<string, ComponentInfo>,
   clientComponentPaths: Record<string, string>,
 ): any {
   const [path, exportName] = id.split('#')
-
-  const result = resolveByPath(path, exportName, clientComponents, clientComponentPaths)
-  if (result !== null)
-    return result
-
-  const normalizedPath = path.startsWith('./') ? path.slice(2) : path
-  return resolveByPath(normalizedPath, exportName, clientComponents, clientComponentPaths)
+  for (const candidate of getPathCandidates(path)) {
+    const result = resolveByPath(candidate, exportName, clientComponents, clientComponentPaths)
+    if (result !== null)
+      return result
+  }
+
+  return null
 }
-    const candidates = path.startsWith('./')
-      ? [path, path.slice(2)]
-      : [path, `
-./${path}`]
+    const candidates = getPathCandidates(path)

Run this read-only check to confirm the async candidate literal currently spans two lines:

#!/bin/bash
python - <<'PY'
from pathlib import Path
lines = Path("packages/rari/src/runtime/shared/get-client-component.ts").read_text().splitlines()
for lineno in range(196, 200):
    print(f"{lineno}: {lines[lineno - 1]!r}")
PY

Also applies to: 196-199

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/runtime/shared/get-client-component.ts` around lines 137 -
142, The sync resolver misses bare-id lookups like './foo' and the async
resolver builds a multiline template yielding '\n./${path}'; add a small shared
helper (e.g., getPathVariants(path)) that returns [path, `./${path}`] and use
its results in both resolution sites instead of the current normalizedPath
logic, and replace the async template literal with a single-line join/iteration
using those variants so resolveByPath is invoked for both the raw and './'
prefixed keys (refer to resolveByPath, normalizedPath, clientComponents,
clientComponentPaths).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/rari/src/server/middleware/request_context.rs`:
- Around line 132-136: Replace the O(n*m) dedup loop that checks
result.tags.contains(tag) with a HashSet-based approach: import
std::collections::HashSet, build a HashSet from result.tags, extend/insert each
entry from tags into that set, then replace result.tags with the set's contents
(or collect into a Vec if a Vec is required); apply this change to both the
block that currently iterates over tags (the shown loop over "for tag in &tags {
if !result.tags.contains(tag) { result.tags.push(tag.clone()); } }") and the
similar block at the other location (lines 176–180) to ensure O(1) lookups and
deduplication.
- Around line 131-140: The fetch_cache is merging request tags into cached
entries (see result.tags push loop and cache.put usage in fetch_cache) but
there's no tag-based invalidation (no tag_index or invalidate_by_tag for
fetch_cache) so accumulated tags are orphaned; either remove the tag
accumulation from fetch_cache (delete the loop that mutates result.tags and stop
returning merged tags to callers) or implement a tag-invalidation system
analogous to response_cache (add a tag_index for fetch_cache, record tags on
cache.put, and provide invalidate_by_tag logic) — pick one approach and update
the fetch_cache functions (and any callers expecting result.tags) to match the
chosen design.

In `@packages/deploy/src/utils.ts`:
- Around line 147-161: The function getRariVersion currently falls back to the
mutable string 'latest'; change it to return a reproducible pinned version
instead by introducing a constant fallback (e.g. RARI_FALLBACK_VERSION prefixed
with ^ or exact semver) and return that when the package.json cannot be read;
update the other occurrence noted (the similar fallback at lines 194-198) to use
the same RARI_FALLBACK_VERSION constant so both getRariVersion and the other
lookup return a deterministic pinned version rather than 'latest'.
- Around line 219-221: The current check using
gitignoreContent.includes(providerFolder) is too broad; instead read the
.gitignore via readFileSync, split into lines and check for an exact ignore
entry match (compare trimmed lines against the intended entry or each line in
providerGitignoreEntries) so substrings/comments don't falsely satisfy the
check, and only call writeFileSync to append providerGitignoreEntries when no
exact ignore-line exists; update the logic around gitignoreContent,
providerFolder, and providerGitignoreEntries to perform per-line/exact-match
validation before writing.

In `@packages/rari/src/image/Image.tsx`:
- Around line 223-227: The current img elements (e.g., the one using imgRef,
finalImgSrc, showAltText, alt, imgWidth, fill) conditionally set alt to an empty
string when showAltText is false, which makes images incorrectly decorative;
always render the alt attribute with the provided alt prop (or a guaranteed
non-empty fallback if the component exposes a prop indicating decorative intent)
instead of toggling it on load/error. Update the Image component instances
(including those using imgRef/finalImgSrc and the other occurrences referenced)
to always include alt={alt} (or alt={decorative ? '' : alt} if a dedicated
decorative prop exists), remove logic that clears alt based on showAltText, and
ensure any accessibility tests or props reflect the new behavior.
- Around line 103-108: The problem is that isVisible is only initialized from
shouldLoadImmediately and isn't updated if props later change to make
shouldLoadImmediately true; update the state when that computed value flips by
adding a useEffect that depends on shouldLoadImmediately and calls
setIsVisible(true) when shouldLoadImmediately becomes true (apply the same fix
for the other occurrence around lines referencing isVisible/setIsVisible at
134-135); reference the existing shouldLoadImmediately, isVisible and
setIsVisible symbols and ensure the effect returns nothing (no-op) when
shouldLoadImmediately is false.

In `@packages/rari/src/proxy/shared/utils.ts`:
- Around line 93-99: The empty catch block in the body extraction logic (around
result.text()/result.body that sets the local variable body) silently swallows
errors; update the catch to log the caught error at debug/trace level (including
context like result or a brief message) using the module's logger or a passed-in
logger so failures in result.text() are visible while still allowing execution
to continue, and ensure the catch does not rethrow so fault tolerance remains
intact.

In `@packages/rari/src/router/ClientRouter.tsx`:
- Around line 416-421: handleRedirect is a no-op that just returns finalPath;
either remove the function and use finalPath directly at call sites, or keep it
and add a clear comment explaining its intended future behavior and contract
(e.g., why _targetPath is accepted, expected return semantics, and when it will
mutate or validate finalPath). Update all callers of handleRedirect (references
to handleRedirect) accordingly if you remove it, or add the explanatory comment
above the handleRedirect declaration if you retain it.
- Around line 423-433: The empty catch in processMetadata hides decode/parse
errors; update it to catch the exception and log a warning instead of swallowing
it. In processMetadata (where you call response.headers.get('x-rari-metadata')
and then decode/JSON.parse to call updateDocumentMetadata), catch the error and
call a logger or console.warn with a clear message that includes the header
value and the error (e.g., "Failed to parse x-rari-metadata", metadataHeader,
error) so metadata parse failures are visible for debugging.

In `@packages/rari/src/router/navigation-error-handler.ts`:
- Around line 54-87: In handleHttpError, treat HTTP 408 (Request Timeout) and
429 (Too Many Requests) as retryable: update the logic in the generic return
(and/or introduce an isRetryable boolean used in the 404, 5xx, and fetch-error
branches) so that retryable is true when status >= 500 OR status === 408 OR
status === 429; leave 404 as non-retryable and keep the server-error branch
unchanged. Ensure originalError, statusCode, url, and timestamp remain populated
as before.

In `@packages/rari/src/router/sitemap-generator.ts`:
- Around line 259-313: The virtual module id string is duplicated as
virtualModuleId in both createSitemapPlugin and buildSitemapModule; extract it
to a single module-level constant (e.g., const VIRTUAL_SITEMAP_ID =
'\0virtual:sitemap') and replace the local virtualModuleId declarations inside
createSitemapPlugin and buildSitemapModule with references to that constant so
both resolveId, load, and the build input use the same identifier.
- Around line 284-295: The function extractChunkCode currently types its result
and callbacks as any; update it to use rolldown's exported RolldownBuild type
for the result parameter and use that type for any related callbacks to improve
type safety (import type { RolldownBuild } from 'rolldown' and change the
function signature to accept result: RolldownBuild), leaving the internal logic
(checks for result.output, finding entryChunk, and returning entryChunk.code)
unchanged and ensuring the entryChunk variable retains its narrowed type checks.

In `@packages/rari/src/runtime/shared/get-client-component.ts`:
- Around line 163-181: The async loader getClientComponentAsync currently lacks
the new name-fallback logic used by resolveClientComponent, causing
getClientComponentAsync('Button') to return null while the sync resolver
succeeds; update getClientComponentAsync to mirror resolveClientComponent's
resolution order: read globalAccessor['~clientComponents'],
['~clientComponentPaths'], and ['~clientComponentNames'], attempt
resolveById(id) first, if id contains '#' call resolveByPathWithExport(id,
clientComponents, clientComponentPaths), and finally fall back to
resolveByName(id, clientComponents, clientComponentNames); also ensure any
loading/enqueue behavior used when starting async loads is applied when
resolving via the name map so behavior matches the sync path.
- Around line 93-96: The sync prefetch path in tryLoadComponent currently calls
triggerComponentLoad(componentInfo) which invokes executeLoader that rethrows
errors, causing unhandled promise rejections; change tryLoadComponent to use the
same error-swallowing behavior as loadClientComponent by either calling
loadClientComponent(componentInfo) or invoking
triggerComponentLoad(componentInfo).catch(() => {/* swallow/log like
loadClientComponent */}); ensure the promise rejection is handled rather than
left unawaited so loader errors are logged/swallowed consistently.

In `@packages/rari/src/shared/http-utils.ts`:
- Around line 1-4: The helper throwIfNotOk currently throws a plain Error with
only a message, losing the HTTP status metadata; update throwIfNotOk to create
an Error object for the response (inside the if (!response.ok) block), set
properties on that Error such as status = response.status and statusText =
response.statusText (and optionally body or errorText) before throwing it, so
callers can inspect error.status (reference: function throwIfNotOk and the
Response object used there).

In `@packages/rari/src/vite/alias-resolver.ts`:
- Around line 8-13: The alias resolution currently iterates
Object.entries(aliases) in insertion order which causes prefix shadowing (e.g.,
"@" matching before "@/components"); update the logic in alias-resolver so
aliases are checked from most specific to least specific by sorting the alias
keys/entries by alias length descending before the for-loop that examines
source.startsWith(`${alias}/`) || source === alias; this ensures longer/more
specific aliases are matched first and then use the existing replacement,
path.join and path.resolve(projectRoot, ...) behavior unchanged.

In `@packages/rari/src/vite/file-resolver.ts`:
- Around line 8-25: The resolver appends extensions unconditionally causing
already-qualified imports like "./Foo.tsx" to become "./Foo.tsx.tsx"; update
both resolveFile (the loop that builds pathWithExt) and resolveIndexFile to
first detect if resolvedPath already ends with one of the provided extensions
and if fs.existsSync(resolvedPath) return it immediately; otherwise continue
with the existing extension-appending logic (use the resolvedPath and extensions
parameters and the existing fs.existsSync checks).

In `@packages/rari/src/vite/image-scanner.ts`:
- Around line 229-246: The regex built from variable identifiers in
processImageIdentifiers poses a ReDoS risk; validate each identifier against a
safe JS identifier pattern (e.g., /^[A-Za-z_$][A-Za-z0-9_$]*$/) before
constructing escapedIdentifier and createElementPattern, and skip or handle
identifiers that don't match (log or ignore) so only validated identifiers are
used to build the RegExp; keep the existing ESCAPE_REGEX usage for escaping
after validation and then proceed to extractBalancedBraces, parseImageProps, and
addImageToMap as before.
- Around line 91-128: The transformCode function's build call should mark the
"rari/image" import as external so Rollup/Rollup-based build won't try to
resolve it during the virtual module transform; update the build(...) invocation
inside transformCode (the call that uses virtualModuleId and plugins) to include
an external config that lists "rari/image" (matching the pattern used in
server-build.ts) so missing runtime-only imports won't trigger the fallback path
in extractImageUsages.

In `@web/src/components/Sidebar.tsx`:
- Line 132: Remove the redundant key prop on the inner <li> rendering nested
items in Sidebar.tsx: the key `${itemKey}-${nestedItem.href ||
nestedItem.label}` should be removed from the <li> inside the mapped component
(where nestedItem is used) because React keys must be applied at the map call
site (where the mapped component is created, using itemKey). Keep the existing
key at the map invocation that supplies itemKey and ensure no duplicate keys
exist inside the rendered element.
- Around line 16-21: Remove the duplicate NavItem type in Sidebar.tsx and
instead import the shared NavItem interface from the docs-navigation module:
export the interface (export interface NavItem { label: string; href?: string;
items?: NavItem[]; collapsible?: boolean }) from the docs-navigation module and
replace the local declaration in Sidebar.tsx with an import of NavItem; update
any references in Sidebar component to use the imported NavItem type.

---

Outside diff comments:
In `@packages/deploy/src/render.ts`:
- Around line 48-122: The Render README generation duplicates logic used for
other providers; extract the shared templating into a utility (e.g., a new
function like createProviderReadmeSection(providerName: string, body: string) or
formatProviderReadme(provider: 'render'|'railway', contentTemplate: string) and
move the provider-specific block (currently renderReadmeSection inside
updateReadmeForRender) into a provider-specific template passed to that helper;
then update updateReadmeForRender to call the utility (use the same helper from
the Railway implementation) to generate the final README content and reuse the
helper for create-or-update logic so file existence, append vs create, and
logSuccess calls are handled consistently.

In `@packages/rari/src/proxy/execute-proxy.ts`:
- Around line 29-31: The code is bypassing type-checking by using (executor as
any).proxyFn; update the executor's type instead: extend or create an
interface/type (e.g., ExecutorWithProxy or augment the existing Executor) that
includes proxyFn?: Function (or the specific function signature) and change the
function parameter or local variable type so you can access proxyFn without
casting; alternatively add a narrow type guard that asserts executor has proxyFn
before using it—replace the (executor as any).proxyFn access with a typed access
to the proxyFn property on the newly typed executor so TypeScript enforces the
shape and signature of proxyFn.

In `@packages/rari/src/vite/hmr-error-handler.ts`:
- Around line 24-25: recordError currently clears the timeout with an inline
check ("if (this.resetTimer) clearTimeout(this.resetTimer)"); replace that with
the shared utility clearTimer to match reset() and dispose(). Update the
recordError method to call clearTimer(this.resetTimer) (and ensure any resulting
value handling matches how reset() and dispose() manage this.resetTimer) so
timer clearing is consistent across the HMR error handler.

---

Duplicate comments:
In `@packages/rari/src/router/props-extractor.ts`:
- Around line 178-190: The mergeTitleField function collapses templates when
childTitle is a string by returning the interpolated string; instead, when
parentTitle is an object with a template and childTitle is a string, return an
object that preserves parentTitle.template and also contains the
interpolated/rendered title (so deeper segments can still inherit the template).
Update mergeTitleField (handling of parentTitle and childTitle) to return an
object with the original template plus a rendered/title field (using
parentTitle.template.replace('%s', childTitle)) rather than returning the plain
string.

In `@packages/rari/src/router/robots-generator.ts`:
- Around line 36-51: The generator currently iterates userAgents and emits
directives per-agent; instead change the loop to emit all User-Agent lines first
and then the shared directives to group them: for each rule (the loop using
userAgents and rule), first push each `User-Agent: ${userAgent}` for all entries
in `userAgents`, then push the normalized `Allow` lines from
`normalizeArray(rule.allow)` and `Disallow` lines from
`normalizeArray(rule.disallow)`, then conditionally push `Crawl-delay:
${rule.crawlDelay}` if defined, and finally a blank line into `lines`; keep
using the same `normalizeArray`, `rule.crawlDelay`, and `lines` identifiers so
behavior remains the same but output is grouped.

In `@packages/rari/src/runtime/shared/get-client-component.ts`:
- Around line 137-142: The sync resolver misses bare-id lookups like './foo' and
the async resolver builds a multiline template yielding '\n./${path}'; add a
small shared helper (e.g., getPathVariants(path)) that returns [path,
`./${path}`] and use its results in both resolution sites instead of the current
normalizedPath logic, and replace the async template literal with a single-line
join/iteration using those variants so resolveByPath is invoked for both the raw
and './' prefixed keys (refer to resolveByPath, normalizedPath,
clientComponents, clientComponentPaths).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 199660ef-e57f-4310-a764-860241b8b3bc

📥 Commits

Reviewing files that changed from the base of the PR and between 2a82dcc and e66b13e.

📒 Files selected for processing (29)
  • crates/rari/src/runtime/ops.rs
  • crates/rari/src/server/middleware/request_context.rs
  • packages/deploy/src/railway.ts
  • packages/deploy/src/render.ts
  • packages/deploy/src/utils.ts
  • packages/rari/src/image/Image.tsx
  • packages/rari/src/proxy/execute-proxy.ts
  • packages/rari/src/proxy/matcher.ts
  • packages/rari/src/proxy/runtime-executor.ts
  • packages/rari/src/proxy/shared/utils.ts
  • packages/rari/src/router/ClientRouter.tsx
  • packages/rari/src/router/navigation-error-handler.ts
  • packages/rari/src/router/props-extractor.ts
  • packages/rari/src/router/robots-generator.ts
  • packages/rari/src/router/sitemap-generator.ts
  • packages/rari/src/runtime-client.ts
  • packages/rari/src/runtime/ErrorBoundaryWrapper.tsx
  • packages/rari/src/runtime/shared/get-client-component.ts
  • packages/rari/src/shared/http-utils.ts
  • packages/rari/src/shared/timer-utils.ts
  • packages/rari/src/vite/alias-resolver.ts
  • packages/rari/src/vite/file-resolver.ts
  • packages/rari/src/vite/hmr-coordinator.ts
  • packages/rari/src/vite/hmr-error-handler.ts
  • packages/rari/src/vite/image-scanner.ts
  • packages/rari/src/vite/index.ts
  • packages/rari/src/vite/server-build.ts
  • web/src/components/Sidebar.tsx
  • web/src/lib/docs-navigation.ts

Comment thread crates/rari/src/server/middleware/request_context.rs Outdated
Comment thread crates/rari/src/server/middleware/request_context.rs Outdated
Comment thread packages/deploy/src/utils.ts Outdated
Comment thread packages/deploy/src/utils.ts
Comment thread packages/rari/src/image/Image.tsx Outdated
Comment thread packages/rari/src/vite/file-resolver.ts
Comment thread packages/rari/src/vite/image-scanner.ts
Comment thread packages/rari/src/vite/image-scanner.ts
Comment thread web/src/components/Sidebar.tsx Outdated
Comment thread web/src/components/Sidebar.tsx Outdated
skiniks added 2 commits March 18, 2026 00:49
- Remove separate react_globals_setup.js and consolidate React initialization into component_eval_setup.js
- Simplify React global object setup by removing redundant Component, Fragment, and Suspense assignments
- Add null checks in jsx/jsxs functions to handle missing React context gracefully
- Integrate spinner keyframes initialization directly into LoadingSpinner setup
- Remove module_reload extension and associated initialization logic
- Remove callbacks.rs and streaming module from runtime extensions
- Consolidate streaming React setup into init_react.js
- Update fetch initialization to use simplified web extension setup
- Streamline runtime extension module structure by removing unused modules
- Reduce boilerplate in RSC rendering core by eliminating duplicate React setup code
Copy link
Copy Markdown

@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: 19

Caution

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

⚠️ Outside diff range comments (2)
crates/rari/src/runtime/ext/rsc_renderer/mod.rs (1)

4-14: ⚠️ Potential issue | 🔴 Critical

Add init_react back to the deps list.

The ESM files in this extension (rsc_traversal.js and client_registry.js) extensively use React symbols (React.Fragment, React.Suspense, Symbol.for('react.transitional.element'), etc.). Without init_react in the dependency list, Deno's extension system offers no guarantee that React will be initialized before this extension runs, potentially causing runtime errors when those React symbols are accessed. Although react::extensions() is added to the extensions vector before rsc_renderer::extensions(), Deno respects explicit deps declarations for ordering, not Vec insertion order. Restore the dependency: deps = [rari, init_react, init_rsc_modules].

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/rari/src/runtime/ext/rsc_renderer/mod.rs` around lines 4 - 14, The
extension init_rsc_renderer's deps list is missing init_react which can cause
React symbols used in rsc_traversal.js and client_registry.js to be
uninitialized at runtime; update the extension!macro invocation named
init_rsc_renderer to include init_react in the deps array (i.e., change deps =
[rari, init_rsc_modules] to include init_react so it becomes deps = [rari,
init_react, init_rsc_modules]) so Deno guarantees React initialization before
this extension runs (the referenced files are rsc_traversal.js and
client_registry.js and React is provided by the init_react extension).
packages/rari/src/proxy/execute-proxy.ts (1)

22-27: ⚠️ Potential issue | 🟠 Major

Move waitUntil promise handling to a finally block to ensure async errors are reported even if proxyFn throws.

If proxyFn throws at line 33 after event.waitUntil(...) is called, execution jumps to the catch block and skips the Promise.allSettled block at lines 35-43. This drops error reporting for background tasks that were already scheduled.

Proposed fix
 export async function executeProxy(simpleRequest: SimpleRequest): Promise<SimpleProxyResult> {
+  const waitUntilPromises: Promise<unknown>[] = []
+  const flushWaitUntil = () => {
+    if (waitUntilPromises.length === 0)
+      return
+    void Promise.allSettled(waitUntilPromises).then((results) => {
+      results.forEach((result, index) => {
+        if (result.status === 'rejected') {
+          console.error(`[rari] Proxy: waitUntil promise ${index} failed:`, result.reason)
+        }
+      })
+    })
+  }
+
   try {
@@
-    const waitUntilPromises: Promise<unknown>[] = []
     const event = {
       waitUntil: (promise: Promise<unknown>) => {
         waitUntilPromises.push(promise)
       },
     }
@@
-    if (waitUntilPromises.length > 0) {
-      void Promise.allSettled(waitUntilPromises).then((results) => {
-        results.forEach((result, index) => {
-          if (result.status === 'rejected') {
-            console.error(`[rari] Proxy: waitUntil promise ${index} failed:`, result.reason)
-          }
-        })
-      })
-    }
-
     return await processProxyResult(result)
   }
   catch (error) {
     console.error('[rari] Proxy: executeProxy failed:', error)
     return { continue: true }
   }
+  finally {
+    flushWaitUntil()
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/proxy/execute-proxy.ts` around lines 22 - 27, The current
pattern collects background tasks into waitUntilPromises via event.waitUntil but
calls Promise.allSettled after proxyFn execution, so if proxyFn throws those
background errors aren’t reported; update executeProxy to run the
Promise.allSettled(waitUntilPromises) reporting logic inside a finally block
(after try/catch) so that regardless of proxyFn throwing the waitUntilPromises
array is awaited/settled and any rejections are logged; ensure you reference and
use the existing waitUntilPromises array and event.waitUntil behavior and keep
existing error reporting logic around Promise.allSettled unchanged.
♻️ Duplicate comments (3)
packages/rari/src/image/Image.tsx (1)

105-114: ⚠️ Potential issue | 🟠 Major

Move isVisible prop-sync out of render to avoid render-phase updates.

setIsVisible(true) at Line 109 (and mutating prevShouldLoadImmediatelyRef at Line 108) runs during render. This is fragile under Strict/Concurrent rendering and should be effect-driven.

💡 Proposed fix
 function useImageLazyLoad(
   imgRef: React.RefObject<HTMLImageElement | null>,
   shouldPreload: boolean,
   unoptimized: boolean,
   loading: 'lazy' | 'eager',
 ) {
   const shouldLoadImmediately = shouldPreload || unoptimized || loading === 'eager'
   const [isVisible, setIsVisible] = useState(shouldLoadImmediately)
-  const prevShouldLoadImmediatelyRef = useRef(shouldLoadImmediately)
-
-  if (shouldLoadImmediately && !prevShouldLoadImmediatelyRef.current) {
-    prevShouldLoadImmediatelyRef.current = true
-    setIsVisible(true)
-  }
-
-  useEffect(() => {
-    prevShouldLoadImmediatelyRef.current = shouldLoadImmediately
-  }, [shouldLoadImmediately])
+  useEffect(() => {
+    if (shouldLoadImmediately)
+      setIsVisible(true)
+  }, [shouldLoadImmediately])

   useEffect(() => {
     if (shouldLoadImmediately)
       return
In React 19, what is the recommended pattern for syncing state from props when a boolean flips (e.g., shouldLoadImmediately), and is calling setState during render discouraged?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/image/Image.tsx` around lines 105 - 114, The code is
calling setIsVisible(true) and mutating prevShouldLoadImmediatelyRef during
render when shouldLoadImmediately flips, which can cause render-phase updates;
move this sync into an effect: in a useEffect that depends on
shouldLoadImmediately, compare prevShouldLoadImmediatelyRef.current to
shouldLoadImmediately, if it changed and shouldLoadImmediately is true call
setIsVisible(true) and update prevShouldLoadImmediatelyRef.current accordingly,
and keep the existing effect that sets prevShouldLoadImmediatelyRef to
shouldLoadImmediately for other updates to avoid any setState during render.
packages/rari/src/router/ClientRouter.tsx (1)

90-96: ⚠️ Potential issue | 🟠 Major

Remove stale viewport metadata when the next route omits it.

Line 90 updates viewport only when present; when absent, the previous route’s viewport tag remains in <head>.

Proposed fix
   if (metadata.viewport) {
     updateOrCreateMetaTag('meta[name="viewport"]', {
       name: 'viewport',
       content: metadata.viewport,
     })
   }
+  else {
+    removeMetaTag('meta[name="viewport"]')
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/router/ClientRouter.tsx` around lines 90 - 96, The current
code only calls updateOrCreateMetaTag('meta[name="viewport"]', ...) when
metadata.viewport exists, leaving a stale <meta name="viewport"> from the
previous route; update the route-change logic in ClientRouter to explicitly
remove the existing viewport meta tag when metadata.viewport is absent by
querying for document.querySelector('meta[name="viewport"]') and removing it (or
calling the existing removeMetaTag helper if one exists) instead of doing
nothing; keep the update path using
updateOrCreateMetaTag('meta[name="viewport"]', { name: 'viewport', content:
metadata.viewport }) when metadata.viewport is present.
packages/rari/src/router/props-extractor.ts (1)

182-186: ⚠️ Potential issue | 🟠 Major

Preserve template metadata instead of collapsing to a string.

Line 183–186 still reduces { template: ... } to a plain string when childTitle is a string, which drops template inheritance for deeper segments.

Proposed fix
   if (typeof childTitle === 'string') {
     if (typeof parentTitle === 'object' && parentTitle?.template) {
-      const hasPlaceholder = parentTitle.template.includes('%s')
-      return hasPlaceholder ? parentTitle.template.replace('%s', childTitle) : childTitle
+      return {
+        ...parentTitle,
+        default: childTitle,
+      }
     }

     return childTitle
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/router/props-extractor.ts` around lines 182 - 186, When
childTitle is a string and parentTitle is an object with a template, don't
return a raw string (which drops the template metadata); instead return an
object that preserves the parent template and includes the resolved string (e.g.
keep parentTitle.template and add the computed text or value). Update the branch
handling parentTitle/template and childTitle to return an object (preserving
parentTitle.template) rather than collapsing to childTitle so deeper segments
can inherit the template; reference the variables parentTitle and childTitle in
props-extractor.ts to locate the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/rari/src/rsc/rendering/core/utils.rs`:
- Around line 41-43: The HMR bootstrap scripts (JSX_RUNTIME_SETUP_SCRIPT and
COMPONENT_EVAL_SETUP_SCRIPT) and transform_imports_for_hmr reference
globalThis.React but never initialize it; add an initialization of
globalThis.React before any transformed code runs (or, inside
transform_imports_for_hmr branches that inject "var React = globalThis.React;",
fall back to defining globalThis.React = globalThis.React || { createElement: /*
shim */, Fragment: /* shim */, Suspense: /* noop or shim */ }) so classic JSX
usages (React.createElement/Fragment/Suspense) and globals like
globalThis.LoadingSpinner will not throw; update the JSX_RUNTIME_SETUP_SCRIPT
and COMPONENT_EVAL_SETUP_SCRIPT to ensure globalThis.React is defined prior to
executing transformed imports.

In `@crates/rari/src/runtime/ext/rari/mod.rs`:
- Around line 10-14: The impl block for ExtensionTrait<()> for rari (fn init(():
()) -> Extension { rari::init() }) is not used because runtime initialization
calls extensions(is_snapshot) which delegates to rari::build(); either remove
this unused impl to avoid dead code, or explicitly document and suppress
warnings by adding a comment explaining it's intentionally unused and annotate
it with #[allow(dead_code)] (or #[allow(dead_code, unused)]), ensuring you
reference the rari::init(), rari::build(), and extensions(is_snapshot) code
paths so future maintainers understand why the trait impl was retained or
removed.

In `@crates/rari/src/runtime/ext/web/init_fetch.js`:
- Around line 116-122: The dedupe logic (requestDedupeMap) currently ignores
tags so concurrent requests with identical base keys but different
init.rari.tags result in followers cloning the leader's response without
propagating tags into fetchWithRustCache; update the keying so tags are
included: when building options.tags from init?.rari?.tags or init?.next?.tags
(the tags variable), normalize the tag array (filter strings, trim, sort) and
either append JSON.stringify(normalizedTags) to the dedupe key or use that
normalized string as part of the dedupeMap key; ensure fetch paths that set
options.tags and call fetchWithRustCache (the block using options.tags and the
code path referenced in the comment) use the same normalized-tag-aware key
(apply the same change to the other tagged-fetch block noted in the review).

In `@crates/rari/src/server/middleware/request_context.rs`:
- Around line 117-118: The fast-return path (guard.as_ref()) returns the
leader’s cached result without merging the follower’s tags parsed from
options.get("tags") (serde_json::from_str), so concurrent followers with
different tags lose their tags; before returning the shared in-flight result in
that branch, parse the follower tags into `tags: Vec<String>` (same parsing as
other branches), merge them into the result's tag list (append and deduplicate
or use the existing merge routine used in the leader/cache-hit code) and
persist/update the shared result so both the returned value and the stored
in-flight result include the combined tags; apply the same fix to the other
similar locations noted (the blocks around the other occurrences referenced in
the comment).

In `@packages/deploy/src/utils.ts`:
- Around line 189-195: The backup logic currently compares
packageJson.scripts.start to the literal 'rari start', which will mis-backup
generated scripts if config.startScript is different; compute the intended new
start value (e.g., const newStart = config.startScript || 'rari start') and only
create packageJson.scripts['start:original'] when packageJson.scripts.start
exists and packageJson.scripts.start !== newStart and
packageJson.scripts['start:original'] is not already set; then set
packageJson.scripts.start = newStart. This uses the symbols
packageJson.scripts.start, packageJson.scripts['start:original'], and
config.startScript to locate and fix the code.

In `@packages/rari/src/image/Image.tsx`:
- Around line 54-56: The prop extraction uses || which treats empty strings and
zero like missing values; change the fallbacks for imgWidth, imgHeight and
imgBlurDataURL to use nullish coalescing (??) so explicit values like
blurDataURL = "" or width = 0 are respected; update the expressions that
reference width, height, and blurDataURL (and the typeof src checks) to use ??
instead of || to prevent unintended fallback to src metadata.
- Around line 287-299: When imgWidth is provided the current logic sets
sizesArray = [imgWidth] and shouldUseSrcSet = false so the imgElement omits
srcSet and loses DPR candidates; update the rendering logic in Image.tsx to
always generate a DPR-based srcSet for the fixed-width path by calling
buildSrcSetString (or the loader) with density descriptors when imgWidth is
defined (i.e., treat the fixed-width branch specially: compute a srcSet with 1x,
2x, 3x candidates from finalSrc and quality and pass it into the imgElement's
srcSet), while keeping sizes undefined for the fixed-width case; modify the
conditions around shouldUseSrcSet / srcSet assignment so buildSrcSetString is
used for both responsive sizesArray and fixed imgWidth (refer to imgWidth,
sizesArray, shouldUseSrcSet, buildSrcSetString, and imgElement).

In `@packages/rari/src/proxy/matcher.ts`:
- Around line 70-75: checkHostCondition currently returns the input key when the
request hostname matches (returning key) which is inconsistent with other
checkers that return the actual matched value; update checkHostCondition
(function name) to return request.rariUrl.hostname when request.rariUrl.hostname
=== key and null otherwise so callers receive the actual hostname value instead
of the key.

In `@packages/rari/src/proxy/shared/utils.ts`:
- Around line 50-51: The current checks use truthiness on result.status (e.g.,
if (!result || !result.status)) which treats a valid status of 0 as absent;
update these checks to use explicit null/undefined comparisons (e.g.,
result.status == null / result.status != null) so a status of 0 is handled
correctly—replace occurrences that test result.status truthiness (the if
(!result || !result.status) check and the analogous check later in the file)
with nullish comparisons while keeping the existing early-return behavior when
result itself is falsy.
- Around line 16-19: The response.headers type and how headers are set in
handleDirectResponse currently overwrite duplicate header names (losing multiple
Set-Cookie), so change the header representation to allow multiple values (e.g.,
update response?: { headers: Record<string, string | string[]> } or a
string[]-capable type) and update the forEach in handleDirectResponse to append
instead of replace: when a header key already exists, convert the stored value
to an array if needed and push the new value (or merge into an array) so
duplicate header names are preserved; update any consumers of response.headers
accordingly (look for response.headers usage and the handleDirectResponse
function name to apply the changes).

In `@packages/rari/src/router/navigation-utils.ts`:
- Around line 24-31: The function matchDynamicSegment currently treats a missing
segmentParam as a successful match which allows malformed manifest entries to
pass; change the behavior to fail closed by returning false when segmentParam is
undefined so the route does not match and parameters aren't silently dropped.
Update matchDynamicSegment and any analogous checks for catch-all segments (the
same conditional branches around segmentParam in the dynamic/catch-all handling)
to return false instead of true, and ensure callers relying on params extraction
(the params Record<string,string|string[]>) only proceed when a valid
segmentParam exists. Verify all occurrences (the dynamic segment check and the
catch-all branch mentioned in the diff) are updated consistently.

In `@packages/rari/src/router/robots-generator.ts`:
- Around line 202-204: The current runtime check in robots-generator.ts only
tests module.default === undefined and therefore misses the case where
module.default is explicitly null; update the validation in the code that throws
the Error (the branch checking module and module.default) to treat nullish
values as invalid (e.g., use module.default == null or module.default ===
undefined || module.default === null) so that null defaults also trigger the
same error message, unless you intentionally want null to be allowed—in which
case add a comment explaining that null is treated as valid.

In `@packages/rari/src/router/sitemap-generator.ts`:
- Around line 308-319: The loop in sitemap-generator.ts that computes
sanitizedId (via SANITIZE_ID_REGEX) and then writes to disk can silently
overwrite files when two different raw ids normalize to the same sanitizedId; to
fix, track seen sanitized names (e.g., a Set seenSanitizedIds) inside the
sitemap generation function and after computing sanitizedId check
seenSanitizedIds.has(sanitizedId); if present, either throw/log a clear error
identifying both the conflicting raw ids (use the original id value) or generate
a deterministic unique filename (e.g., append an index or a short hash derived
from String(id)) before calling fs.writeFile; ensure you update the Set with the
final filename/sanitizedId and reference the variables sanitizedId, sitemapIds,
and the fs.writeFile call when making the change.
- Around line 253-259: The resolveId hook currently lacks the importer parameter
and always resolves relative paths against sitemapFile.path, breaking nested
relative imports and deviating from Rollup conventions; update the function
signature to accept (source: string, importer?: string) and when source
startsWith('.') resolve using path.resolve(path.dirname(importer ??
sitemapFile.path), source), keep the special-case return of VIRTUAL_SITEMAP_ID
when source === VIRTUAL_SITEMAP_ID, and return null for unresolved cases so it
behaves consistently with other resolveId implementations (e.g., the one using
the importer parameter).

In `@packages/rari/src/runtime/shared/get-client-component.ts`:
- Around line 96-97: The truthy checks on componentInfo.component in
getComponentFromInfo and resolveById incorrectly treat valid falsy values (e.g.,
0, '', false) as missing; replace those checks with nullish checks (test for
null or undefined) so only absent values are rejected — update the conditions
that currently read like "if (!componentInfo.component)" to use a null/undefined
check (e.g., componentInfo.component == null) in both getComponentFromInfo and
resolveById to match the check used later.
- Line 37: The code currently returns componentInfo.component || null which
incorrectly treats valid falsy exports (0, '', false) as null; update both
occurrences that return componentInfo.component (replace the `|| null` usage
with a nullish check such as `?? null` or an explicit undefined check) so that
falsy-but-valid values are preserved (apply this change where
componentInfo.component is returned and make it consistent with the existing
`(component !== null && component !== undefined)` checks).

In `@packages/rari/src/shared/http-utils.ts`:
- Line 10: The current thrown Error interpolates the full response body
(errorText) which may leak sensitive data and produce huge logs; change the
construction so the Error.message uses only bounded/safe info (e.g., `HTTP
${response.status}: ${response.statusText}`) and attach a capped preview of the
body (e.g., first N characters plus ellipsis) to the error as a separate
property (use the existing `error` variable and set
`error.status`/`error.statusText` as before, plus `error.bodyPreview`), ensuring
you do NOT include the full `errorText` in the message or any top-level logable
fields.

In `@packages/rari/src/vite/image-scanner.ts`:
- Around line 83-89: The scanDirectory function currently swallows
missing-directory errors (await fs.access(dir) catch { return }), which hides
typos in the required primary scan root; update the logic so missing primary
roots raise an error while still permitting silent returns for optional
additional dirs — e.g., add a boolean parameter like required (or throw from
scanDirectory when a new flag primary=true is passed) and change the caller that
invokes scanDirectory for the main srcDir to call scanDirectory(srcDir, images,
true) (keep additionalDirs calling with false), or alternatively perform an
explicit fs.access check before calling scanDirectory for the required srcDir
and throw a descriptive Error if absent; ensure you update references to
scanDirectory and any call sites around the current primary scan invocation (and
similarly address the same pattern referenced around lines 115-123 for
additionalDirs).
- Around line 33-39: Remove the brittle exact-string import prefilter in
processFile and make file reads resilient: call fs.readFile inside a try/catch
so a read error doesn't reject the whole scan, and after successfully reading
call extractImageUsages(content, fullPath, images) unconditionally (let
extractImageUsages handle alias/import detection); reference processFile and
extractImageUsages when updating the logic.

---

Outside diff comments:
In `@crates/rari/src/runtime/ext/rsc_renderer/mod.rs`:
- Around line 4-14: The extension init_rsc_renderer's deps list is missing
init_react which can cause React symbols used in rsc_traversal.js and
client_registry.js to be uninitialized at runtime; update the extension!macro
invocation named init_rsc_renderer to include init_react in the deps array
(i.e., change deps = [rari, init_rsc_modules] to include init_react so it
becomes deps = [rari, init_react, init_rsc_modules]) so Deno guarantees React
initialization before this extension runs (the referenced files are
rsc_traversal.js and client_registry.js and React is provided by the init_react
extension).

In `@packages/rari/src/proxy/execute-proxy.ts`:
- Around line 22-27: The current pattern collects background tasks into
waitUntilPromises via event.waitUntil but calls Promise.allSettled after proxyFn
execution, so if proxyFn throws those background errors aren’t reported; update
executeProxy to run the Promise.allSettled(waitUntilPromises) reporting logic
inside a finally block (after try/catch) so that regardless of proxyFn throwing
the waitUntilPromises array is awaited/settled and any rejections are logged;
ensure you reference and use the existing waitUntilPromises array and
event.waitUntil behavior and keep existing error reporting logic around
Promise.allSettled unchanged.

---

Duplicate comments:
In `@packages/rari/src/image/Image.tsx`:
- Around line 105-114: The code is calling setIsVisible(true) and mutating
prevShouldLoadImmediatelyRef during render when shouldLoadImmediately flips,
which can cause render-phase updates; move this sync into an effect: in a
useEffect that depends on shouldLoadImmediately, compare
prevShouldLoadImmediatelyRef.current to shouldLoadImmediately, if it changed and
shouldLoadImmediately is true call setIsVisible(true) and update
prevShouldLoadImmediatelyRef.current accordingly, and keep the existing effect
that sets prevShouldLoadImmediatelyRef to shouldLoadImmediately for other
updates to avoid any setState during render.

In `@packages/rari/src/router/ClientRouter.tsx`:
- Around line 90-96: The current code only calls
updateOrCreateMetaTag('meta[name="viewport"]', ...) when metadata.viewport
exists, leaving a stale <meta name="viewport"> from the previous route; update
the route-change logic in ClientRouter to explicitly remove the existing
viewport meta tag when metadata.viewport is absent by querying for
document.querySelector('meta[name="viewport"]') and removing it (or calling the
existing removeMetaTag helper if one exists) instead of doing nothing; keep the
update path using updateOrCreateMetaTag('meta[name="viewport"]', { name:
'viewport', content: metadata.viewport }) when metadata.viewport is present.

In `@packages/rari/src/router/props-extractor.ts`:
- Around line 182-186: When childTitle is a string and parentTitle is an object
with a template, don't return a raw string (which drops the template metadata);
instead return an object that preserves the parent template and includes the
resolved string (e.g. keep parentTitle.template and add the computed text or
value). Update the branch handling parentTitle/template and childTitle to return
an object (preserving parentTitle.template) rather than collapsing to childTitle
so deeper segments can inherit the template; reference the variables parentTitle
and childTitle in props-extractor.ts to locate the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6a8d2475-3c1f-4e93-a74f-2f52d59c550c

📥 Commits

Reviewing files that changed from the base of the PR and between e66b13e and 22a0e5d.

📒 Files selected for processing (41)
  • crates/rari/src/rsc/rendering/core/constants.rs
  • crates/rari/src/rsc/rendering/core/js/component_eval_setup.js
  • crates/rari/src/rsc/rendering/core/js/react_globals_setup.js
  • crates/rari/src/rsc/rendering/core/renderer.rs
  • crates/rari/src/rsc/rendering/core/utils.rs
  • crates/rari/src/rsc/rendering/layout/route_composer.rs
  • crates/rari/src/rsc/rendering/streaming/js/streaming_init.js
  • crates/rari/src/rsc/rendering/streaming/js/streaming_react_setup.js
  • crates/rari/src/runtime/ext/mod.rs
  • crates/rari/src/runtime/ext/module_reload/init_module_reload.js
  • crates/rari/src/runtime/ext/module_reload/mod.rs
  • crates/rari/src/runtime/ext/rari/callbacks.rs
  • crates/rari/src/runtime/ext/rari/mod.rs
  • crates/rari/src/runtime/ext/rari/rari.js
  • crates/rari/src/runtime/ext/react/init_react.js
  • crates/rari/src/runtime/ext/rsc_renderer/mod.rs
  • crates/rari/src/runtime/ext/streaming/mod.rs
  • crates/rari/src/runtime/ext/web/init_fetch.js
  • crates/rari/src/runtime/mod.rs
  • crates/rari/src/runtime/module_loader/core.rs
  • crates/rari/src/runtime/module_loader/node_stubs.rs
  • crates/rari/src/server/middleware/request_context.rs
  • packages/deploy/src/utils.ts
  • packages/rari/src/image/Image.tsx
  • packages/rari/src/proxy/execute-proxy.ts
  • packages/rari/src/proxy/matcher.ts
  • packages/rari/src/proxy/shared/utils.ts
  • packages/rari/src/router/ClientRouter.tsx
  • packages/rari/src/router/navigation-error-handler.ts
  • packages/rari/src/router/navigation-utils.ts
  • packages/rari/src/router/props-extractor.ts
  • packages/rari/src/router/robots-generator.ts
  • packages/rari/src/router/sitemap-generator.ts
  • packages/rari/src/runtime/shared/get-client-component.ts
  • packages/rari/src/shared/http-utils.ts
  • packages/rari/src/vite/alias-resolver.ts
  • packages/rari/src/vite/file-resolver.ts
  • packages/rari/src/vite/hmr-error-handler.ts
  • packages/rari/src/vite/image-scanner.ts
  • web/src/components/Sidebar.tsx
  • web/src/lib/docs-navigation.ts
💤 Files with no reviewable changes (13)
  • crates/rari/src/runtime/ext/streaming/mod.rs
  • crates/rari/src/runtime/mod.rs
  • crates/rari/src/rsc/rendering/core/constants.rs
  • crates/rari/src/rsc/rendering/layout/route_composer.rs
  • crates/rari/src/runtime/ext/rari/callbacks.rs
  • crates/rari/src/runtime/ext/mod.rs
  • crates/rari/src/runtime/ext/module_reload/init_module_reload.js
  • crates/rari/src/rsc/rendering/streaming/js/streaming_react_setup.js
  • crates/rari/src/rsc/rendering/core/js/react_globals_setup.js
  • crates/rari/src/runtime/module_loader/node_stubs.rs
  • crates/rari/src/rsc/rendering/streaming/js/streaming_init.js
  • crates/rari/src/runtime/ext/module_reload/mod.rs
  • crates/rari/src/runtime/ext/rari/rari.js

Comment thread crates/rari/src/rsc/rendering/core/utils.rs
Comment thread crates/rari/src/runtime/ext/rari/mod.rs
Comment thread crates/rari/src/server/middleware/request_context.rs
Comment thread packages/deploy/src/utils.ts Outdated
Comment thread packages/rari/src/image/Image.tsx
Comment thread packages/rari/src/runtime/shared/get-client-component.ts Outdated
Comment thread packages/rari/src/runtime/shared/get-client-component.ts Outdated
Comment thread packages/rari/src/shared/http-utils.ts Outdated
Comment thread packages/rari/src/vite/image-scanner.ts Outdated
Comment thread packages/rari/src/vite/image-scanner.ts
Copy link
Copy Markdown

@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.

Caution

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

⚠️ Outside diff range comments (3)
crates/rari/src/runtime/module_reload/manager.rs (3)

237-241: ⚠️ Potential issue | 🟠 Major

Reload failures are silently ignored in parallel mode.

handle.await returns Result<Result<(), RariError>, JoinError>. This code only handles the outer JoinError (task panic/cancel), but the inner Result<(), RariError> from reload_module is discarded. Actual reload failures go unreported, unlike the sequential path which propagates errors.

🐛 Proposed fix to handle inner result
         for handle in handles {
-            if let Err(e) = handle.await {
-                error!(error = %e, "Batch reload task failed");
+            match handle.await {
+                Ok(Ok(_)) => {}
+                Ok(Err(e)) => {
+                    error!(error = %e, "Module reload failed in batch");
+                }
+                Err(e) => {
+                    error!(error = %e, "Batch reload task panicked");
+                }
             }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/rari/src/runtime/module_reload/manager.rs` around lines 237 - 241, The
current loop over handles only inspects the outer JoinError from handle.await
and drops the inner Result from reload_module, so reload errors are ignored;
update the loop in manager.rs (the for handle in handles block) to match the
nested Result<Result<(), RariError>, JoinError> returned by handle.await, and
when the inner Result is Err(e) (the RariError from reload_module) log or
propagate that error (similar to the sequential path) instead of discarding it;
ensure you reference handle.await and use both match arms for the JoinError and
the inner Result to report the reload failure via error!(...) or return the
error as appropriate.

195-212: 🧹 Nitpick | 🔵 Trivial

Unused _file_path parameter is a code smell.

The _file_path parameter is passed through reload_modulereload_with_retryreload_module_internal but never used (note the underscore prefix). If file path information isn't needed for the reload logic, consider removing it from the internal method signature to avoid confusion. If it's intended for future use, a brief comment would clarify the intent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/rari/src/runtime/module_reload/manager.rs` around lines 195 - 212, The
_file_path parameter on reload_module_internal is unused; either remove it from
reload_module_internal's signature and from its callers (reload_module and
reload_with_retry) so the API reflects actual usage, or keep the parameter but
document its intended future purpose with a concise comment in
reload_module_internal and mark it as intentionally unused (e.g., keep the
leading underscore). Update all references to the function (reload_module →
reload_with_retry → reload_module_internal) to match the chosen approach to
avoid unused-parameter warnings and confusion.

125-136: ⚠️ Potential issue | 🔴 Critical

Fix race condition in reload_module_debounced: abort old task before registering new one.

When add_pending is called with a component_id that already has a pending task, the old JoinHandle is dropped without being aborted. In Rust, dropping a JoinHandle does not cancel the task—it continues running in the background. This causes both the old and new tasks to complete after their respective delays, resulting in duplicate reloads and defeating the debounce purpose.

Change line 136 to cancel any existing pending task before adding the new one:

if let Some(_) = self.debounce_manager.cancel_pending(component_id).await {
    // Old task was cancelled
}
self.debounce_manager.add_pending(component_id.clone(), request, handle).await;

Or alternatively, modify add_pending to abort the old handle internally before inserting:

pub async fn add_pending(
    &self,
    component_id: String,
    request: ModuleReloadRequest,
    handle: tokio::task::JoinHandle<()>,
) {
    let mut pending = self.pending.lock().await;
    if let Some((_, old_handle)) = pending.remove(&component_id) {
        old_handle.abort();
    }
    pending.insert(component_id, (request, handle));
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/rari/src/runtime/module_reload/manager.rs` around lines 125 - 136, The
debounce logic currently drops previous JoinHandles without cancelling them,
causing duplicate reloads; before calling debounce_manager.add_pending from
reload_module_debounced, ensure any existing pending task for component_id is
aborted—either call an existing
debounce_manager.cancel_pending(component_id).await and handle/abort its
JoinHandle, or update debounce_manager.add_pending to check
pending.contains_key(&component_id) and call old_handle.abort() (use the
ModuleReloadRequest and tokio::task::JoinHandle types from the signature) before
inserting the new (request, handle) pair so only the newest task runs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@crates/rari/src/runtime/module_reload/manager.rs`:
- Around line 237-241: The current loop over handles only inspects the outer
JoinError from handle.await and drops the inner Result from reload_module, so
reload errors are ignored; update the loop in manager.rs (the for handle in
handles block) to match the nested Result<Result<(), RariError>, JoinError>
returned by handle.await, and when the inner Result is Err(e) (the RariError
from reload_module) log or propagate that error (similar to the sequential path)
instead of discarding it; ensure you reference handle.await and use both match
arms for the JoinError and the inner Result to report the reload failure via
error!(...) or return the error as appropriate.
- Around line 195-212: The _file_path parameter on reload_module_internal is
unused; either remove it from reload_module_internal's signature and from its
callers (reload_module and reload_with_retry) so the API reflects actual usage,
or keep the parameter but document its intended future purpose with a concise
comment in reload_module_internal and mark it as intentionally unused (e.g.,
keep the leading underscore). Update all references to the function
(reload_module → reload_with_retry → reload_module_internal) to match the chosen
approach to avoid unused-parameter warnings and confusion.
- Around line 125-136: The debounce logic currently drops previous JoinHandles
without cancelling them, causing duplicate reloads; before calling
debounce_manager.add_pending from reload_module_debounced, ensure any existing
pending task for component_id is aborted—either call an existing
debounce_manager.cancel_pending(component_id).await and handle/abort its
JoinHandle, or update debounce_manager.add_pending to check
pending.contains_key(&component_id) and call old_handle.abort() (use the
ModuleReloadRequest and tokio::task::JoinHandle types from the signature) before
inserting the new (request, handle) pair so only the newest task runs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 481a4919-bafd-4de0-84b8-d990f5fe599e

📥 Commits

Reviewing files that changed from the base of the PR and between 22a0e5d and 7ffde77.

📒 Files selected for processing (6)
  • crates/rari/src/runtime/module_reload/config.rs
  • crates/rari/src/runtime/module_reload/manager.rs
  • crates/rari/src/runtime/module_reload/mod.rs
  • crates/rari/src/runtime/module_reload/stats.rs
  • crates/rari/src/server/config/mod.rs
  • crates/rari/src/server/core/mod.rs
💤 Files with no reviewable changes (5)
  • crates/rari/src/runtime/module_reload/config.rs
  • crates/rari/src/runtime/module_reload/mod.rs
  • crates/rari/src/server/config/mod.rs
  • crates/rari/src/server/core/mod.rs
  • crates/rari/src/runtime/module_reload/stats.rs

- Add React global initialization with createElement, Fragment, and Suspense in component eval setup
- Extract tag validation logic into reusable extractValidTags function for fetch operations
- Include normalized tags in fetch cache key generation for proper cache differentiation
- Update RSC renderer extension to depend on init_react for proper React setup
- Improve module reload error handling with better match patterns for task results
- Remove unused file_path parameter from reload_module_internal function
- Enhance fetch cache merging to combine tags from multiple requests and maintain sorted order
- Update deploy utils to support configurable start scripts with backup preservation
- Improve navigation utils and props extractor with better type handling and edge cases
Comment thread packages/rari/src/router/props-extractor.ts Outdated
Copy link
Copy Markdown

@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: 8

Caution

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

⚠️ Outside diff range comments (2)
crates/rari/src/runtime/module_reload/manager.rs (1)

221-249: ⚠️ Potential issue | 🟡 Minor

Inconsistent error handling between sequential and parallel modes.

In sequential mode (line 223), the first error is propagated via ? and stops processing. In parallel mode (lines 237-244), errors are only logged and the function always returns Ok(()), even if all reloads fail.

This asymmetry means callers cannot rely on the return value to detect batch failures when parallel_reloads is enabled. If this is intentional (fire-and-forget semantics for parallel), consider documenting it. Otherwise, consider collecting errors and returning a summary error:

💡 Optional: Collect and report parallel failures
         for handle in handles {
             match handle.await {
                 Ok(Ok(())) => {}
                 Ok(Err(e)) => {
                     error!(error = %e, "Module reload failed in batch");
+                    // Optionally collect: errors.push(e);
                 }
                 Err(e) => {
                     error!(error = %e, "Batch reload task failed");
                 }
             }
         }
+        // If collecting errors, return an aggregate error here
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/rari/src/runtime/module_reload/manager.rs` around lines 221 - 249,
Sequential mode currently propagates the first error via ? but the parallel
branch just logs task errors and always returns Ok(()); change the parallel
branch in the method to collect results from the spawned handles (the
tokio::spawn handles created with manager.clone_for_task() calling
manager.reload_module(&request.component_id, &request.file_path).await),
aggregate any Err(e) results (both JoinError and inner Err from reload_module)
into a single error (or a Vec of errors) and return Err(...) if any task failed
so the function's return semantics match the sequential path; ensure you still
log individual errors (using error!(error=%e,...)) while accumulating them and
return Ok(()) only when all handles succeeded.
packages/rari/src/proxy/execute-proxy.ts (1)

12-16: ⚠️ Potential issue | 🟠 Major

Serialize proxy bootstrap across concurrent requests.

This isInitialized() / initializeProxyFromManifest() pair is non-atomic. In packages/rari/src/proxy/executor.ts, initialization is only guarded by a boolean flag, so two cold-start requests can both call loadProxy() on the singleton at the same time.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/proxy/execute-proxy.ts` around lines 12 - 16, The
initialization race happens because executor.isInitialized() /
initializeProxyFromManifest() are non-atomic; update the bootstrap to serialize
concurrent initialization by adding an in-flight initialization promise or mutex
in the executor singleton (e.g., in packages/rari/src/proxy/executor.ts) so
multiple callers share the same
initializeProxyFromManifest('./dist/proxy-manifest.json') promise instead of
calling loadProxy() twice; change initializeProxyFromManifest to set and return
a single Promise (and have isInitialized reflect completion) so callers in
execute-proxy.ts that call initializeProxyFromManifest(...) will await the same
promise and only one loadProxy() runs.
♻️ Duplicate comments (4)
packages/rari/src/image/Image.tsx (2)

103-119: ⚠️ Potential issue | 🟠 Major

isVisible can still stay false after props switch to immediate load.

Line [115] initializes visibility once, but Lines [117-123] never set isVisible to true when shouldLoadImmediately flips from false to true, so src can remain unset.

💡 Proposed fix
 function useImageLazyLoad(
   imgRef: React.RefObject<HTMLImageElement | null>,
   shouldPreload: boolean,
   unoptimized: boolean,
   loading: 'lazy' | 'eager',
 ) {
   const shouldLoadImmediately = shouldPreload || unoptimized || loading === 'eager'
-  const prevShouldLoadImmediatelyRef = useRef(shouldLoadImmediately)
-
-  const getInitialVisibility = () => {
-    if (shouldLoadImmediately && !prevShouldLoadImmediatelyRef.current) {
-      prevShouldLoadImmediatelyRef.current = true
-      return true
-    }
-
-    return shouldLoadImmediately
-  }
-
-  const [isVisible, setIsVisible] = useState(getInitialVisibility)
-
-  useEffect(() => {
-    prevShouldLoadImmediatelyRef.current = shouldLoadImmediately
-  }, [shouldLoadImmediately])
+  const [isVisible, setIsVisible] = useState(shouldLoadImmediately)
+
+  useEffect(() => {
+    if (shouldLoadImmediately)
+      setIsVisible(true)
+  }, [shouldLoadImmediately])

   useEffect(() => {
     if (shouldLoadImmediately)
       return

Also applies to: 121-123

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/image/Image.tsx` around lines 103 - 119, The isVisible
state may remain false when shouldLoadImmediately flips true because
getInitialVisibility only runs once; update the useEffect that watches
shouldLoadImmediately to detect the transition
(prevShouldLoadImmediatelyRef.current === false && shouldLoadImmediately ===
true) and call setIsVisible(true) before updating
prevShouldLoadImmediatelyRef.current; this preserves the initial
getInitialVisibility logic while ensuring isVisible is set when
shouldLoadImmediately changes to immediate load (refer to shouldLoadImmediately,
prevShouldLoadImmediatelyRef, getInitialVisibility, isVisible, setIsVisible, and
the existing useEffect).

292-304: ⚠️ Potential issue | 🟠 Major

Fixed-width rendering still drops DPR candidates (srcSet).

Line [297] makes shouldUseSrcSet false for fixed width, so Line [303] omits srcSet; high-density screens lose sharper variants.

💡 Proposed fix
   const defaultWidth = imgWidth || 1920
   const sizesArray = imgWidth ? [imgWidth] : DEFAULT_DEVICE_SIZES
@@
   const shouldUseSrcSet = !imgWidth || sizesArray.length > 1 || sizesArray[0] !== defaultWidth
+  const densitySrcSet = imgWidth
+    ? [
+        `${loader ? loader({ src: finalSrc, width: imgWidth, quality }) : buildImageUrl(finalSrc, imgWidth, quality)} 1x`,
+        `${loader ? loader({ src: finalSrc, width: imgWidth * 2, quality }) : buildImageUrl(finalSrc, imgWidth * 2, quality)} 2x`,
+      ].join(', ')
+    : undefined

   const imgElement = (
     <img
       ref={imgRef}
       src={isVisible ? mainSrc : undefined}
-      srcSet={isVisible && shouldUseSrcSet ? buildSrcSetString(sizesArray, finalSrc, quality, undefined, loader) : undefined}
-      sizes={shouldUseSrcSet ? sizes : undefined}
+      srcSet={isVisible ? (densitySrcSet || (shouldUseSrcSet ? buildSrcSetString(sizesArray, finalSrc, quality, undefined, loader) : undefined)) : undefined}
+      sizes={densitySrcSet ? undefined : (shouldUseSrcSet ? sizes : undefined)}

Also applies to: 318-319

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/image/Image.tsx` around lines 292 - 304, The component
currently sets shouldUseSrcSet = false when a fixed imgWidth is provided, which
prevents rendering DPR (density) candidates and yields low-res images on
high-density screens; update the logic around defaultWidth, sizesArray and
shouldUseSrcSet so that even for a single fixed width you still build a
density-based srcSet via buildSrcSetString (i.e., allow srcSet when imgWidth is
present by detecting the single-size case and generating DPR descriptors), and
mirror the same change where srcSet/sizes are set in the imgElement and the
corresponding alternate branch later (the references are defaultWidth,
sizesArray, shouldUseSrcSet, buildSrcSetString, and imgElement).
packages/rari/src/router/sitemap-generator.ts (1)

310-318: ⚠️ Potential issue | 🟡 Minor

Validate that sanitizedId is not empty after sanitization.

If the original id consists entirely of characters that match SANITIZE_ID_REGEX (e.g., "@#$%"), the sanitizedId will be an empty string, resulting in a file named .xml. This could cause unexpected behavior or overwrite issues.

🛡️ Proposed fix
       const sanitizedId = String(id).replace(SANITIZE_ID_REGEX, '_')

+      if (!sanitizedId) {
+        throw new Error(`Invalid sitemap id "${id}": results in empty string after sanitization`)
+      }
+
       const existingId = seenSanitizedIds.get(sanitizedId)
       if (existingId !== undefined) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/router/sitemap-generator.ts` around lines 310 - 318, After
creating sanitizedId from String(id).replace(SANITIZE_ID_REGEX, '_'), validate
that sanitizedId is not the empty string; if it is, either assign a safe default
(e.g., '_') or throw a descriptive error to avoid generating a ".xml" filename.
Update the sitemap generation logic that uses sanitizedId (and the duplicate
check via seenSanitizedIds) to operate on the validated/normalized value so
collisions and file writes are safe; reference the variables sanitizedId,
SANITIZE_ID_REGEX, sitemapIds, and seenSanitizedIds when making the change.
packages/rari/src/proxy/shared/utils.ts (1)

15-15: ⚠️ Potential issue | 🟠 Major

Preserve duplicate responseHeaders values in continue flows.

handleDirectResponse() now keeps repeated header names, but extractProxyHeaders() still flattens responseHeaders to a single string. In the x-rari-proxy-continue path, multiple Set-Cookie values will still collapse to the last one.

💡 Suggested fix
 export interface SimpleProxyResult {
   continue: boolean
   redirect?: {
@@
   }
   rewrite?: string
   requestHeaders?: Record<string, string>
-  responseHeaders?: Record<string, string>
+  responseHeaders?: Record<string, string | string[]>
   response?: {
     status: number
     headers: Record<string, string | string[]>
@@
-export function extractProxyHeaders(headers: ResponseLike['headers']): { requestHeaders?: Record<string, string>, responseHeaders?: Record<string, string> } {
+export function extractProxyHeaders(headers: ResponseLike['headers']): { requestHeaders?: Record<string, string>, responseHeaders?: Record<string, string | string[]> } {
   const requestHeaders: Record<string, string> = {}
-  const responseHeaders: Record<string, string> = {}
+  const responseHeaders: Record<string, string | string[]> = {}
@@
       if (key.startsWith('x-rari-proxy-request-')) {
         const headerName = key.replace('x-rari-proxy-request-', '')
         requestHeaders[headerName] = value
       }
       else if (!key.startsWith('x-rari-proxy-')) {
-        responseHeaders[key] = value
+        const existing = responseHeaders[key]
+        responseHeaders[key] = existing === undefined
+          ? value
+          : Array.isArray(existing) ? [...existing, value] : [existing, value]
       }
     })
   }

Also applies to: 68-80, 90-95

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/proxy/shared/utils.ts` at line 15, extractProxyHeaders
currently flattens responseHeaders into a Record<string,string>, which loses
duplicate header values (e.g. multiple Set-Cookie) used by the
x-rari-proxy-continue path; update extractProxyHeaders (and any callers that
build responseHeaders in the x-rari-proxy-continue flow) to preserve duplicate
headers by using Record<string,string[]> or an array of header tuples, mirror
the behavior of handleDirectResponse, and ensure the downstream serialization
for the continue flow emits repeated header entries instead of joining them into
one string; reference extractProxyHeaders, handleDirectResponse,
responseHeaders, and x-rari-proxy-continue when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/rari/src/rsc/rendering/core/js/component_eval_setup.js`:
- Around line 45-50: The code assumes document.head exists before calling
document.head.appendChild, which can throw in partial DOM environments; update
the block that creates the style with id 'spinner-keyframes' to first ensure a
head element is available (check document.head and fall back to
document.getElementsByTagName('head')[0] or skip appending if none), and only
call appendChild on that validated head reference so the creation of the style
node and setting style.textContent remains safe in non-browser contexts.
- Around line 5-12: The element creation currently collapses falsy keys (e.g., 0
or '') to null and leaves the original key inside props; update the logic in the
element construction (the block that builds element with $typeof, type, props
and key) to: 1) compute key using a hasOwnProperty check (so valid falsy values
are preserved) rather than `props?.key || null`, and 2) create a shallow copy of
props with the `key` property removed (do not mutate the incoming props) and use
that copy for element.props before you merge children (the code that currently
spreads props into element.props and later merges children should use the
props-without-key copy).
- Around line 21-24: The current null no-op fallbacks for _jsx and _jsxs will
drop output; update their fallback factories so they delegate to the existing
runtime renderers instead of returning null: when setting _jsx and _jsxs, use
globalThis['~react']?.jsxRuntime?.jsx || globalThis.jsx ||
((...args)=>globalThis.React.createElement(...args)) and similarly for jsxs
(referencing _jsx, _jsxs, globalThis.jsx, globalThis.jsxs, and
globalThis.React.createElement) so the variables delegate to the working
implementations rather than returning null.

In `@crates/rari/src/runtime/ext/web/init_fetch.js`:
- Around line 134-140: Remove the redundant outer check that inspects `tags`
(the lines referencing `const tags = init?.rari?.tags ?? init?.next?.tags` and
`if (tags && Array.isArray(tags))`) because `extractValidTags(init)` already
returns an empty array for missing/invalid tags; instead call
`extractValidTags(init)`, check its length, and only then set `options.tags =
JSON.stringify(validTags)` — update references in this block around
`extractValidTags`, `validTags`, and `options.tags` so the code relies solely on
`extractValidTags(init)` for validation.

In `@packages/deploy/src/utils.ts`:
- Around line 147-168: getRariVersion is declared as getRariVersion(cwd: string)
but railway.ts and render.ts call it without arguments, causing a TS error;
change the function signature in utils.ts (getRariVersion) to accept an optional
cwd with a default (e.g., cwd = process.cwd()) so callers can omit it, and keep
existing behavior when a cwd is provided; update only the signature in
getRariVersion and leave all callers (railway.ts, render.ts, and existing
getRariVersion(cwd) usage) unchanged.

In `@packages/rari/src/proxy/execute-proxy.ts`:
- Around line 24-27: The event.waitUntil handler currently pushes raw promises
into waitUntilPromises which can trigger unhandled-rejection before the final
Promise.allSettled; update the waitUntil implementation (the event object and
any similar block at lines 42-50) to attach a rejection handler when registering
the promise (e.g., add .catch(err => store/log the error or noop) or wrap it so
rejections are observed) while still pushing the original promise (or its
wrapped variant) into waitUntilPromises so the existing Promise.allSettled logic
remains valid.

In `@packages/rari/src/proxy/shared/utils.ts`:
- Around line 103-107: The merge logic in result.headers.forEach incorrectly
treats falsy values (like an empty string) as absent because it uses if
(headers[lowerKey]); change that presence check to an explicit existence
test—e.g. use Object.prototype.hasOwnProperty.call(headers, lowerKey) or typeof
headers[lowerKey] !== 'undefined'—so when merging into headers (lowerKey,
existing, value) you correctly accumulate multiple header values instead of
overwriting when the first value is an empty string.

In `@test/unit/router/props-extractor.test.ts`:
- Around line 25-28: MetadataResult.title can be an object but
PageMetadata.title (used in ClientRouter.tsx) is typed/treated as string; update
the code that assigns metadata to document.title and that serializes the
x-rari-metadata header to coerce title objects to a string by using the object's
default property (or a sensible fallback like template applied or empty string)
before assignment/serialization. Locate the mergeMetadata output
(MetadataResult.title) and the places in ClientRouter.tsx where
PageMetadata.title is read/assigned (document.title assignment and metadata
header serialization) and add a small conversion helper that accepts string | {
default?: string; template?: string } and returns the proper string to use.

---

Outside diff comments:
In `@crates/rari/src/runtime/module_reload/manager.rs`:
- Around line 221-249: Sequential mode currently propagates the first error via
? but the parallel branch just logs task errors and always returns Ok(());
change the parallel branch in the method to collect results from the spawned
handles (the tokio::spawn handles created with manager.clone_for_task() calling
manager.reload_module(&request.component_id, &request.file_path).await),
aggregate any Err(e) results (both JoinError and inner Err from reload_module)
into a single error (or a Vec of errors) and return Err(...) if any task failed
so the function's return semantics match the sequential path; ensure you still
log individual errors (using error!(error=%e,...)) while accumulating them and
return Ok(()) only when all handles succeeded.

In `@packages/rari/src/proxy/execute-proxy.ts`:
- Around line 12-16: The initialization race happens because
executor.isInitialized() / initializeProxyFromManifest() are non-atomic; update
the bootstrap to serialize concurrent initialization by adding an in-flight
initialization promise or mutex in the executor singleton (e.g., in
packages/rari/src/proxy/executor.ts) so multiple callers share the same
initializeProxyFromManifest('./dist/proxy-manifest.json') promise instead of
calling loadProxy() twice; change initializeProxyFromManifest to set and return
a single Promise (and have isInitialized reflect completion) so callers in
execute-proxy.ts that call initializeProxyFromManifest(...) will await the same
promise and only one loadProxy() runs.

---

Duplicate comments:
In `@packages/rari/src/image/Image.tsx`:
- Around line 103-119: The isVisible state may remain false when
shouldLoadImmediately flips true because getInitialVisibility only runs once;
update the useEffect that watches shouldLoadImmediately to detect the transition
(prevShouldLoadImmediatelyRef.current === false && shouldLoadImmediately ===
true) and call setIsVisible(true) before updating
prevShouldLoadImmediatelyRef.current; this preserves the initial
getInitialVisibility logic while ensuring isVisible is set when
shouldLoadImmediately changes to immediate load (refer to shouldLoadImmediately,
prevShouldLoadImmediatelyRef, getInitialVisibility, isVisible, setIsVisible, and
the existing useEffect).
- Around line 292-304: The component currently sets shouldUseSrcSet = false when
a fixed imgWidth is provided, which prevents rendering DPR (density) candidates
and yields low-res images on high-density screens; update the logic around
defaultWidth, sizesArray and shouldUseSrcSet so that even for a single fixed
width you still build a density-based srcSet via buildSrcSetString (i.e., allow
srcSet when imgWidth is present by detecting the single-size case and generating
DPR descriptors), and mirror the same change where srcSet/sizes are set in the
imgElement and the corresponding alternate branch later (the references are
defaultWidth, sizesArray, shouldUseSrcSet, buildSrcSetString, and imgElement).

In `@packages/rari/src/proxy/shared/utils.ts`:
- Line 15: extractProxyHeaders currently flattens responseHeaders into a
Record<string,string>, which loses duplicate header values (e.g. multiple
Set-Cookie) used by the x-rari-proxy-continue path; update extractProxyHeaders
(and any callers that build responseHeaders in the x-rari-proxy-continue flow)
to preserve duplicate headers by using Record<string,string[]> or an array of
header tuples, mirror the behavior of handleDirectResponse, and ensure the
downstream serialization for the continue flow emits repeated header entries
instead of joining them into one string; reference extractProxyHeaders,
handleDirectResponse, responseHeaders, and x-rari-proxy-continue when making the
change.

In `@packages/rari/src/router/sitemap-generator.ts`:
- Around line 310-318: After creating sanitizedId from
String(id).replace(SANITIZE_ID_REGEX, '_'), validate that sanitizedId is not the
empty string; if it is, either assign a safe default (e.g., '_') or throw a
descriptive error to avoid generating a ".xml" filename. Update the sitemap
generation logic that uses sanitizedId (and the duplicate check via
seenSanitizedIds) to operate on the validated/normalized value so collisions and
file writes are safe; reference the variables sanitizedId, SANITIZE_ID_REGEX,
sitemapIds, and seenSanitizedIds when making the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 75adc588-a5e2-492c-bd6d-13f79be47fb0

📥 Commits

Reviewing files that changed from the base of the PR and between 7ffde77 and 616fe8d.

📒 Files selected for processing (18)
  • crates/rari/src/rsc/rendering/core/js/component_eval_setup.js
  • crates/rari/src/runtime/ext/web/init_fetch.js
  • crates/rari/src/runtime/module_reload/manager.rs
  • crates/rari/src/server/middleware/request_context.rs
  • packages/deploy/src/utils.ts
  • packages/rari/src/image/Image.tsx
  • packages/rari/src/proxy/execute-proxy.ts
  • packages/rari/src/proxy/matcher.ts
  • packages/rari/src/proxy/shared/utils.ts
  • packages/rari/src/router/navigation-utils.ts
  • packages/rari/src/router/props-extractor.ts
  • packages/rari/src/router/robots-generator.ts
  • packages/rari/src/router/sitemap-generator.ts
  • packages/rari/src/runtime/shared/get-client-component.ts
  • packages/rari/src/shared/http-utils.ts
  • packages/rari/src/vite/image-scanner.ts
  • test/unit/router/navigation-utils.test.ts
  • test/unit/router/props-extractor.test.ts

Comment thread crates/rari/src/rsc/rendering/core/js/component_eval_setup.js
Comment thread crates/rari/src/rsc/rendering/core/js/component_eval_setup.js Outdated
Comment thread crates/rari/src/rsc/rendering/core/js/component_eval_setup.js Outdated
Comment thread crates/rari/src/runtime/ext/web/init_fetch.js Outdated
Comment thread packages/deploy/src/utils.ts Outdated
Comment thread packages/rari/src/proxy/execute-proxy.ts
Comment thread packages/rari/src/proxy/shared/utils.ts Outdated
Comment thread test/unit/router/props-extractor.test.ts Outdated
- Refactor React.createElement to explicitly handle key prop separation from props object
- Update _jsx and _jsxs fallbacks to use globalThis.jsx/jsxs or createElement as final fallback
- Add defensive head element check before appending spinner keyframes style
- Simplify fetch cache tags extraction logic and remove redundant array check
- Fix timeout option handling to properly coalesce undefined values with OR operator
- Enhance module reload error collection to capture and return first error from batch
- Add default parameter to getRariVersion function for process.cwd() fallback
- Simplify image lazy load state management by removing ref-based visibility tracking
- Add useDprDescriptors parameter to buildSrcSetString for device pixel ratio support
- Update image srcset generation to support both width and DPR descriptor formats
- Improve props extractor and sitemap generator type safety and error handling
Copy link
Copy Markdown

@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: 8

Caution

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

⚠️ Outside diff range comments (4)
crates/rari/src/rsc/rendering/core/js/component_eval_setup.js (2)

52-63: 🧹 Nitpick | 🔵 Trivial

Consider guarding globalThis.React inside LoadingSpinner.

If a consumer somehow unsets or replaces globalThis.React after initial setup, this function would throw. A defensive check would improve resilience:

Suggested fix
   globalThis.LoadingSpinner = function () {
+    if (!globalThis.React?.createElement)
+      return null
     return globalThis.React.createElement('div', {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/rari/src/rsc/rendering/core/js/component_eval_setup.js` around lines
52 - 63, The LoadingSpinner function currently assumes globalThis.React exists
and will throw if it's unset; update the globalThis.LoadingSpinner
implementation to first check that globalThis.React and React.createElement are
defined (e.g., if (!globalThis.React || typeof globalThis.React.createElement
!== 'function') return null) and only call React.createElement when safe;
reference the LoadingSpinner function to add this defensive guard and return a
safe fallback (null or a no-op) when React is unavailable.

66-78: 🧹 Nitpick | 🔵 Trivial

Consider guarding globalThis.React inside DefaultLoading for consistency.

Same defensive consideration as LoadingSpinner — if globalThis.React is unset after setup, this throws.

Suggested fix
   globalThis.DefaultLoading = function () {
+    if (!globalThis.React?.createElement)
+      return null
     return globalThis.React.createElement('div', {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/rari/src/rsc/rendering/core/js/component_eval_setup.js` around lines
66 - 78, DefaultLoading currently assumes globalThis.React exists and will throw
if React is unset; update the DefaultLoading function to guard access to
globalThis.React (same pattern used for LoadingSpinner) — e.g., check if
globalThis.React is defined before calling createElement and return a safe
fallback (null or a plain DOM string/div) when React is missing; ensure the
function still returns the React element when globalThis.React and
globalThis.LoadingSpinner are present, and reference DefaultLoading,
globalThis.React, and globalThis.LoadingSpinner when making the change.
crates/rari/src/server/middleware/request_context.rs (1)

78-108: ⚠️ Potential issue | 🟠 Major

Cache key inconsistency enables unintended tag accumulation.

The Rust cache excludes tags from the cache key (line 81), but the JavaScript deduplication layer in init_fetch.js includes tags in the deduplication key via :tags:<json> suffix. This inconsistency creates problematic behavior:

  • JS deduplication: requests with different tags generate different dedup keys
  • Rust cache: requests with different tags reuse the same cache entry

Consequence: when multiple requests access the same cached URL with different tags, the cached entry accumulates all tags from every request that hits it (lines 137, 162, 191 perform tag merging and write back to cache). This means:

  1. A cached response originally fetched with tag [A] will be mutated to include tag [B] when accessed by a different request
  2. Later requests for the same URL may receive tags they never requested
  3. Tag values become unpredictable across requests

Either align cache key generation to include tags (matching JS behavior) or avoid mutating cached entries with request-specific tags.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/rari/src/server/middleware/request_context.rs` around lines 78 - 108,
The cache key generation in generate_cache_key currently excludes "tags"
(cache_relevant_options filter) causing Rust cache entries to be shared across
different tag sets and enabling tag accumulation; fix by including tags in the
cache-relevant options (remove "tags" from the exclusion) and ensure the tags
value is serialized deterministically to match the JS deduplication suffix
(e.g., use a stable JSON/string representation or sorted list) so that
generate_cache_key produces distinct keys for different tag sets and prevents
mutation/merging of tags into a shared cache entry.
crates/rari/src/runtime/module_reload/manager.rs (1)

125-139: ⚠️ Potential issue | 🟠 Major

Debounce scheduling still has a race window before pending registration.

Because the task is spawned at Line 125 before Line 137 cancellation and Line 139 registration, execution can interleave in a way that allows stale/untracked reloads.

Suggested ordering fix
-        let handle = tokio::spawn(async move {
-            tokio::time::sleep(debounce_delay).await;
-
-            manager.debounce_manager.remove_pending(&component_id_clone).await;
-
-            if let Err(e) = manager.reload_module(&component_id_clone, &file_path_clone).await {
-                error!("Failed to reload module {}: {}", component_id_clone, e);
-            }
-        });
-
         let request = ModuleReloadRequest::new(component_id.clone(), file_path.clone());
-
-        self.debounce_manager.cancel_pending(&component_id).await;
-
-        self.debounce_manager.add_pending(component_id.clone(), request, handle).await;
+        self.debounce_manager.cancel_pending(&component_id).await;
+
+        let handle = tokio::spawn(async move {
+            tokio::time::sleep(debounce_delay).await;
+            manager.debounce_manager.remove_pending(&component_id_clone).await;
+            if let Err(e) = manager.reload_module(&component_id_clone, &file_path_clone).await {
+                error!("Failed to reload module {}: {}", component_id_clone, e);
+            }
+        });
+
+        self.debounce_manager
+            .add_pending(component_id.clone(), request, handle)
+            .await;

Run this read-only check to confirm whether remove_pending/cancel_pending are keyed only by component id and whether debounce_delay_ms can be zero:

#!/bin/bash
set -euo pipefail

fd '.*\.rs$' crates/rari/src/runtime/module_reload

rg -n -C3 'struct DebounceManager|fn add_pending|fn cancel_pending|fn remove_pending' crates/rari/src/runtime/module_reload -g '*.rs'

rg -n -C3 'struct ReloadConfig|debounce_delay_ms' crates/rari/src/runtime/module_reload -g '*.rs'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/rari/src/runtime/module_reload/manager.rs` around lines 125 - 139, The
background task is spawned before the pending request is cancelled/registered,
creating a race between the spawned task's remove_pending call and the
subsequent cancel_pending/add_pending; reorder operations so you first construct
the ModuleReloadRequest, call
debounce_manager.cancel_pending(&component_id).await, then spawn the tokio task
(the code that sleeps and calls remove_pending/reload_module) and finally call
debounce_manager.add_pending(component_id.clone(), request, handle).await;
ensure you reference debounce_manager, cancel_pending, add_pending,
remove_pending, ModuleReloadRequest, debounce_delay (or debounce_delay_ms) and
the spawned task's handle when applying this change.
♻️ Duplicate comments (6)
packages/rari/src/image/Image.tsx (2)

292-304: ⚠️ Potential issue | 🟠 Major

Fixed-width rendering still drops DPR srcSet candidates.

When imgWidth is provided, shouldUseSrcSet becomes false and the <img> is rendered without srcSet, which degrades sharpness on high-density displays.

💡 Proposed fix (add density srcSet for fixed-width path)
   const defaultWidth = imgWidth || 1920
   const sizesArray = imgWidth ? [imgWidth] : DEFAULT_DEVICE_SIZES
@@
-  const shouldUseSrcSet = !imgWidth || sizesArray.length > 1 || sizesArray[0] !== defaultWidth
+  const shouldUseSrcSet = !imgWidth || sizesArray.length > 1 || sizesArray[0] !== defaultWidth
+  const densitySrcSet = imgWidth
+    ? [
+        `${loader ? loader({ src: finalSrc, width: imgWidth, quality }) : buildImageUrl(finalSrc, imgWidth, quality)} 1x`,
+        `${loader ? loader({ src: finalSrc, width: imgWidth * 2, quality }) : buildImageUrl(finalSrc, imgWidth * 2, quality)} 2x`,
+      ].join(', ')
+    : undefined
@@
-      srcSet={isVisible && shouldUseSrcSet ? buildSrcSetString(sizesArray, finalSrc, quality, undefined, loader) : undefined}
-      sizes={shouldUseSrcSet ? sizes : undefined}
+      srcSet={isVisible ? (densitySrcSet || (shouldUseSrcSet ? buildSrcSetString(sizesArray, finalSrc, quality, undefined, loader) : undefined)) : undefined}
+      sizes={densitySrcSet ? undefined : (shouldUseSrcSet ? sizes : undefined)}
@@
-  if (!shouldUseSrcSet)
+  if (!shouldUseSrcSet && !densitySrcSet)
     return imgElement

Also applies to: 318-319

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/image/Image.tsx` around lines 292 - 304, The image
component currently disables srcSet when a fixed imgWidth is passed (because
shouldUseSrcSet becomes false), which drops DPR candidates and reduces sharpness
on high-density screens; update the logic around shouldUseSrcSet and the img
rendering so that when imgWidth is provided you still generate a density-based
srcSet (using buildSrcSetString) for DPR variants (e.g., 1x/2x/3x) and pass that
into the imgElement srcSet prop while keeping sizes appropriate; specifically
adjust calculation of shouldUseSrcSet (or add a new flag) and ensure
buildSrcSetString is invoked for the fixed-width path (referencing imgWidth,
sizesArray, shouldUseSrcSet, buildSrcSetString, mainSrc, and imgElement) so
fixed-width images include DPR srcSet candidates.

103-123: ⚠️ Potential issue | 🟠 Major

isVisible is still not synchronized when immediate-load conditions change.

isVisible is initialized once (Line 115). If shouldLoadImmediately flips from false to true later, state is never updated, so src can remain unset.

💡 Proposed fix
-  const prevShouldLoadImmediatelyRef = useRef(shouldLoadImmediately)
-
-  const getInitialVisibility = () => {
-    if (shouldLoadImmediately && !prevShouldLoadImmediatelyRef.current) {
-      prevShouldLoadImmediatelyRef.current = true
-      return true
-    }
-
-    return shouldLoadImmediately
-  }
-
-  const [isVisible, setIsVisible] = useState(getInitialVisibility)
-
-  useEffect(() => {
-    prevShouldLoadImmediatelyRef.current = shouldLoadImmediately
-  }, [shouldLoadImmediately])
+  const [isVisible, setIsVisible] = useState(shouldLoadImmediately)
+
+  useEffect(() => {
+    if (shouldLoadImmediately)
+      setIsVisible(true)
+  }, [shouldLoadImmediately])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/image/Image.tsx` around lines 103 - 123, isVisible is only
initialized once via getInitialVisibility so when shouldLoadImmediately flips
true later the state isn't updated; add an effect that watches
shouldLoadImmediately (and optionally prevShouldLoadImmediatelyRef.current) and
calls setIsVisible(true) when shouldLoadImmediately transitions to true,
updating prevShouldLoadImmediatelyRef.current accordingly; reference the
existing symbols getInitialVisibility, shouldLoadImmediately,
prevShouldLoadImmediatelyRef, isVisible, and setIsVisible to locate where to add
this synchronization.
packages/rari/src/router/navigation-utils.ts (1)

79-82: ⚠️ Potential issue | 🟠 Major

Optional catch-all still matches malformed segments at end-of-path.

At Line 79-82, optional-catch-all returns success even when segment.param is missing. This still allows malformed manifest entries to match and silently drop params; it should fail closed like the dynamic/catch-all paths.

Proposed fix
   if (actualIndex >= actualSegments.length) {
     if (segment.type === 'optional-catch-all') {
+      if (!segment.param)
+        return { success: false, newIndex: actualIndex }
       handleOptionalCatchAll(actualIndex, actualSegments.length, segment.param, params)
       return { success: true, newIndex: actualIndex }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/router/navigation-utils.ts` around lines 79 - 82, The
optional-catch-all branch currently returns success without validating that
segment.param exists, allowing malformed manifests to match; update the branch
around handleOptionalCatchAll(actualIndex, actualSegments.length, segment.param,
params) to first check that segment.param is present (non-empty) and, if
missing, return { success: false } (same failure behavior as dynamic/catch-all
handling); otherwise call handleOptionalCatchAll and return the success result
as before so missing param cases fail closed.
packages/rari/src/proxy/shared/utils.ts (1)

103-107: ⚠️ Potential issue | 🟡 Minor

Use explicit key-existence checks when merging response headers.

Line 105 still uses truthiness (if (headers[lowerKey])), so an existing empty-string value is treated as absent and can be overwritten instead of merged.

Proposed fix
     result.headers.forEach((value: string, key: string) => {
       const lowerKey = key.toLowerCase()
-      if (headers[lowerKey]) {
+      if (Object.prototype.hasOwnProperty.call(headers, lowerKey)) {
         const existing = headers[lowerKey]
         headers[lowerKey] = Array.isArray(existing) ? [...existing, value] : [existing, value]
       }
       else {
         headers[lowerKey] = value
       }
     })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/proxy/shared/utils.ts` around lines 103 - 107, The merge
logic uses a truthiness check (if (headers[lowerKey])) which treats empty-string
or other falsy values as missing; change that to an explicit existence check
such as Object.prototype.hasOwnProperty.call(headers, lowerKey) or (lowerKey in
headers) so keys with falsy values are correctly merged; update the block in the
result.headers.forEach loop referencing headers, lowerKey and existing to use
that explicit check before deciding whether to append to existing or create a
new array.
packages/rari/src/proxy/execute-proxy.ts (1)

24-27: ⚠️ Potential issue | 🟠 Major

Observe waitUntil rejections when registered, not only in finally.

Line 26 stores the raw promise. If it rejects before finally runs, it can still hit unhandled-rejection paths.

Proposed fix
     const event = {
       waitUntil: (promise: Promise<unknown>) => {
-        waitUntilPromises.push(promise)
+        const observedPromise = promise.catch((error) => {
+          throw error
+        })
+        waitUntilPromises.push(observedPromise)
       },
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/proxy/execute-proxy.ts` around lines 24 - 27, The
event.waitUntil implementation currently pushes the raw promise
(waitUntilPromises.push(promise)) which can reject before finally handling and
cause unhandled rejections; wrap the incoming promise before storing so its
rejection is observed immediately — e.g., create a wrapped = promise.catch(err
=> { /* record/log the error or push to a waitUntilErrors array */ throw err;
}); and push that wrapped promise into waitUntilPromises inside the waitUntil
handler (referencing event.waitUntil and waitUntilPromises) so rejections are
captured when registered rather than only in the later finally.
packages/rari/src/router/sitemap-generator.ts (1)

253-259: ⚠️ Potential issue | 🟠 Major

Resolve relative imports from the real sitemap file, not the virtual module ID.

On Line 257, imports coming from the virtual entry use VIRTUAL_SITEMAP_ID as importer, so ./foo resolves from the virtual module instead of sitemapFile.path. Returning path.resolve(...) here also bypasses normal extension resolution, so common extensionless imports can still fail.

🔧 Proposed fix
 function createSitemapPlugin(sitemapFile: SitemapFile, sourceCode: string) {
   return {
     name: 'virtual-sitemap',
-    resolveId(id: string, importer?: string) {
+    async resolveId(id: string, importer?: string) {
       if (id === VIRTUAL_SITEMAP_ID)
         return id
-      if (id.startsWith('.'))
-        return path.resolve(path.dirname(importer ?? sitemapFile.path), id)
-
-      return null
+      if (!id.startsWith('.'))
+        return null
+
+      const baseImporter = !importer || importer === VIRTUAL_SITEMAP_ID
+        ? sitemapFile.path
+        : importer
+
+      return this.resolve(id, baseImporter, { skipSelf: true })
     },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/router/sitemap-generator.ts` around lines 253 - 259, The
resolveId implementation currently resolves relative imports with path.resolve
which treats the virtual module as the importer and bypasses normal extension
resolution; update resolveId (the function named resolveId and references to
VIRTUAL_SITEMAP_ID and sitemapFile.path) so that when id.startsWith('.') you
call this.resolve(id, correctedImporter) instead of path.resolve: if importer
=== VIRTUAL_SITEMAP_ID use sitemapFile.path as the importer, otherwise use the
provided importer, then return the resolved result (e.g., result?.id) or null so
Vite/Rollup can perform normal extension resolution and plugin chaining.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/rari/src/runtime/module_reload/manager.rs`:
- Around line 237-245: The parallel batch branch currently logs errors from each
task (matching on handle.await) but still returns Ok(()) so callers can't detect
failures; modify the join loop in manager.rs that processes each handle.await
(the code matching Ok(Ok(())), Ok(Err(e)), Err(e)) to collect errors (e.g., the
first encountered error or aggregate them) into a local variable and after the
loop return Err(...) when any error was seen instead of always returning Ok(());
ensure you propagate an appropriate Error type consistent with the function's
signature so the caller receives a failure result.

In `@packages/deploy/src/utils.ts`:
- Around line 170-175: The exported function updatePackageJsonForProvider has a
parameter typed as ProviderConfig but the ProviderConfig interface is not
exported; fix by either exporting the interface (add export before interface
ProviderConfig) so the function's public signature is valid, or if
updatePackageJsonForProvider is meant to be internal, remove the export keyword
from updatePackageJsonForProvider (and optionally keep it out of the barrel
index.ts); update whichever choice you make so signatures and re-exports are
consistent.

In `@packages/rari/src/proxy/shared/utils.ts`:
- Around line 120-124: The current truthy check drops empty-string response
bodies; in the response handling in packages/rari/src/proxy/shared/utils.ts
adjust the guards so empty strings are preserved by removing the `result.body
&&` from the first condition and ensuring the other branch uses a null/undefined
check (`result.body != null`) instead of a truthy check; locate the logic that
assigns `body` from `result.body` and update those conditions accordingly to
allow `''` through while still treating null/undefined as absent.

In `@packages/rari/src/router/sitemap-generator.ts`:
- Around line 228-246: determineModuleType advertises support for .mjs, .cjs and
.json but findSitemapFiles() only defaults to ['.ts','.tsx','.js','.jsx'], so
those formats will be skipped; update findSitemapFiles() to include '.mjs',
'.cjs', and '.json' in its default extensions array (or alternatively remove
those extensions from determineModuleType) so both functions agree, and ensure
any callers relying on the default behavior pick up the new extensions; refer to
the determineModuleType and findSitemapFiles identifiers when making the change.
- Around line 320-322: The shard invocation currently coerces id to a string
before calling module.default (see generateSitemaps() loop where sitemapData is
created), which can change semantics for numeric IDs; change the call to pass
the original id value (do not use String(id)) when calling module.default so the
shard receives the original type, and only apply String(id) where you build
filenames or diagnostic messages (e.g., sitemap filename construction or
logging) to avoid changing lookup/pagination behavior.

In `@packages/rari/src/runtime/shared/get-client-component.ts`:
- Around line 146-161: resolveByPathWithExport currently only tries the original
path and a stripped './' variant, which is asymmetric to
getClientComponentAsync's use of getPathVariants; update resolveByPathWithExport
to mirror getClientComponentAsync by trying both adding and removing the './'
prefix (i.e., produce the same path variants as getPathVariants) and call
resolveByPath for each variant until one returns non-null, ensuring lookup works
whether components are registered as 'foo' or './foo'; reference
resolveByPathWithExport, resolveByPath, and getPathVariants when making the
change.

In `@packages/rari/src/vite/image-scanner.ts`:
- Around line 244-245: The cache-key normalization currently uses logical OR
which treats explicit 0 as falsy; in the const key construction (using
imageUsage.src, imageUsage.width, imageUsage.quality and DEFAULT_QUALITY) and
the other occurrence around the same logic near the image scanning logic,
replace the `||` fallbacks with nullish coalescing `??` so only null/undefined
get the defaults (e.g., use imageUsage.width ?? 'auto' and imageUsage.quality ??
DEFAULT_QUALITY); update both places referencing those symbols to preserve
explicit zero values.
- Around line 293-306: The brace tracking for template expressions in
extractBalancedBraces is sharing braceCount with the outer props-object, which
can desynchronize extraction when nested braces appear inside `${...}`;
introduce a separate templateBraceCount (or templateDepth) to track braces
specifically inside template expressions and update that variable when you
detect the `${` start (where currently you do `braceCount++`) and when you see
matching `}` inside template mode (where you currently do `braceCount--`), keep
the outer braceCount untouched except for object-level `{`/`}` handling, and
ensure string/template start/end logic (stringChar, inString) references the new
template-specific counter so balanced extraction returns correct ranges.

---

Outside diff comments:
In `@crates/rari/src/rsc/rendering/core/js/component_eval_setup.js`:
- Around line 52-63: The LoadingSpinner function currently assumes
globalThis.React exists and will throw if it's unset; update the
globalThis.LoadingSpinner implementation to first check that globalThis.React
and React.createElement are defined (e.g., if (!globalThis.React || typeof
globalThis.React.createElement !== 'function') return null) and only call
React.createElement when safe; reference the LoadingSpinner function to add this
defensive guard and return a safe fallback (null or a no-op) when React is
unavailable.
- Around line 66-78: DefaultLoading currently assumes globalThis.React exists
and will throw if React is unset; update the DefaultLoading function to guard
access to globalThis.React (same pattern used for LoadingSpinner) — e.g., check
if globalThis.React is defined before calling createElement and return a safe
fallback (null or a plain DOM string/div) when React is missing; ensure the
function still returns the React element when globalThis.React and
globalThis.LoadingSpinner are present, and reference DefaultLoading,
globalThis.React, and globalThis.LoadingSpinner when making the change.

In `@crates/rari/src/runtime/module_reload/manager.rs`:
- Around line 125-139: The background task is spawned before the pending request
is cancelled/registered, creating a race between the spawned task's
remove_pending call and the subsequent cancel_pending/add_pending; reorder
operations so you first construct the ModuleReloadRequest, call
debounce_manager.cancel_pending(&component_id).await, then spawn the tokio task
(the code that sleeps and calls remove_pending/reload_module) and finally call
debounce_manager.add_pending(component_id.clone(), request, handle).await;
ensure you reference debounce_manager, cancel_pending, add_pending,
remove_pending, ModuleReloadRequest, debounce_delay (or debounce_delay_ms) and
the spawned task's handle when applying this change.

In `@crates/rari/src/server/middleware/request_context.rs`:
- Around line 78-108: The cache key generation in generate_cache_key currently
excludes "tags" (cache_relevant_options filter) causing Rust cache entries to be
shared across different tag sets and enabling tag accumulation; fix by including
tags in the cache-relevant options (remove "tags" from the exclusion) and ensure
the tags value is serialized deterministically to match the JS deduplication
suffix (e.g., use a stable JSON/string representation or sorted list) so that
generate_cache_key produces distinct keys for different tag sets and prevents
mutation/merging of tags into a shared cache entry.

---

Duplicate comments:
In `@packages/rari/src/image/Image.tsx`:
- Around line 292-304: The image component currently disables srcSet when a
fixed imgWidth is passed (because shouldUseSrcSet becomes false), which drops
DPR candidates and reduces sharpness on high-density screens; update the logic
around shouldUseSrcSet and the img rendering so that when imgWidth is provided
you still generate a density-based srcSet (using buildSrcSetString) for DPR
variants (e.g., 1x/2x/3x) and pass that into the imgElement srcSet prop while
keeping sizes appropriate; specifically adjust calculation of shouldUseSrcSet
(or add a new flag) and ensure buildSrcSetString is invoked for the fixed-width
path (referencing imgWidth, sizesArray, shouldUseSrcSet, buildSrcSetString,
mainSrc, and imgElement) so fixed-width images include DPR srcSet candidates.
- Around line 103-123: isVisible is only initialized once via
getInitialVisibility so when shouldLoadImmediately flips true later the state
isn't updated; add an effect that watches shouldLoadImmediately (and optionally
prevShouldLoadImmediatelyRef.current) and calls setIsVisible(true) when
shouldLoadImmediately transitions to true, updating
prevShouldLoadImmediatelyRef.current accordingly; reference the existing symbols
getInitialVisibility, shouldLoadImmediately, prevShouldLoadImmediatelyRef,
isVisible, and setIsVisible to locate where to add this synchronization.

In `@packages/rari/src/proxy/execute-proxy.ts`:
- Around line 24-27: The event.waitUntil implementation currently pushes the raw
promise (waitUntilPromises.push(promise)) which can reject before finally
handling and cause unhandled rejections; wrap the incoming promise before
storing so its rejection is observed immediately — e.g., create a wrapped =
promise.catch(err => { /* record/log the error or push to a waitUntilErrors
array */ throw err; }); and push that wrapped promise into waitUntilPromises
inside the waitUntil handler (referencing event.waitUntil and waitUntilPromises)
so rejections are captured when registered rather than only in the later
finally.

In `@packages/rari/src/proxy/shared/utils.ts`:
- Around line 103-107: The merge logic uses a truthiness check (if
(headers[lowerKey])) which treats empty-string or other falsy values as missing;
change that to an explicit existence check such as
Object.prototype.hasOwnProperty.call(headers, lowerKey) or (lowerKey in headers)
so keys with falsy values are correctly merged; update the block in the
result.headers.forEach loop referencing headers, lowerKey and existing to use
that explicit check before deciding whether to append to existing or create a
new array.

In `@packages/rari/src/router/navigation-utils.ts`:
- Around line 79-82: The optional-catch-all branch currently returns success
without validating that segment.param exists, allowing malformed manifests to
match; update the branch around handleOptionalCatchAll(actualIndex,
actualSegments.length, segment.param, params) to first check that segment.param
is present (non-empty) and, if missing, return { success: false } (same failure
behavior as dynamic/catch-all handling); otherwise call handleOptionalCatchAll
and return the success result as before so missing param cases fail closed.

In `@packages/rari/src/router/sitemap-generator.ts`:
- Around line 253-259: The resolveId implementation currently resolves relative
imports with path.resolve which treats the virtual module as the importer and
bypasses normal extension resolution; update resolveId (the function named
resolveId and references to VIRTUAL_SITEMAP_ID and sitemapFile.path) so that
when id.startsWith('.') you call this.resolve(id, correctedImporter) instead of
path.resolve: if importer === VIRTUAL_SITEMAP_ID use sitemapFile.path as the
importer, otherwise use the provided importer, then return the resolved result
(e.g., result?.id) or null so Vite/Rollup can perform normal extension
resolution and plugin chaining.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4ee91ef8-f338-4bc3-96cc-b7e9b8fd34f2

📥 Commits

Reviewing files that changed from the base of the PR and between 7ffde77 and 616fe8d.

📒 Files selected for processing (18)
  • crates/rari/src/rsc/rendering/core/js/component_eval_setup.js
  • crates/rari/src/runtime/ext/web/init_fetch.js
  • crates/rari/src/runtime/module_reload/manager.rs
  • crates/rari/src/server/middleware/request_context.rs
  • packages/deploy/src/utils.ts
  • packages/rari/src/image/Image.tsx
  • packages/rari/src/proxy/execute-proxy.ts
  • packages/rari/src/proxy/matcher.ts
  • packages/rari/src/proxy/shared/utils.ts
  • packages/rari/src/router/navigation-utils.ts
  • packages/rari/src/router/props-extractor.ts
  • packages/rari/src/router/robots-generator.ts
  • packages/rari/src/router/sitemap-generator.ts
  • packages/rari/src/runtime/shared/get-client-component.ts
  • packages/rari/src/shared/http-utils.ts
  • packages/rari/src/vite/image-scanner.ts
  • test/unit/router/navigation-utils.test.ts
  • test/unit/router/props-extractor.test.ts

Comment thread crates/rari/src/runtime/module_reload/manager.rs
Comment thread packages/deploy/src/utils.ts
Comment thread packages/rari/src/proxy/shared/utils.ts Outdated
Comment thread packages/rari/src/router/sitemap-generator.ts
Comment thread packages/rari/src/router/sitemap-generator.ts
Comment thread packages/rari/src/runtime/shared/get-client-component.ts
Comment thread packages/rari/src/vite/image-scanner.ts Outdated
Comment thread packages/rari/src/vite/image-scanner.ts
- Add null checks for React globals in LoadingSpinner and DefaultLoading components
- Remove "tags" from cache key generation filter in request context
- Export ProviderConfig type from deploy utils
- Improve null coalescing checks in proxy response body handling
- Add validation for optional-catch-all segment param before processing
- Expand sitemap file extensions to include .mjs, .cjs, and .json
- Fix sitemap module invocation to pass id directly instead of wrapped object
- Refactor client component path resolution to check both relative and absolute variants
- Replace logical OR operators with nullish coalescing operators in image scanner
- Update test description to reflect actual behavior for missing param names
Comment thread packages/rari/src/router/ClientRouter.tsx Outdated
Copy link
Copy Markdown

@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

Caution

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

⚠️ Outside diff range comments (5)
packages/rari/src/image/Image.tsx (3)

146-153: ⚠️ Potential issue | 🟠 Major

Fill mode currently overrides consumer objectFit.

On Line 152, hardcoding objectFit: 'cover' ignores a caller-provided style.objectFit, which makes fill images harder to customize.

💡 Proposed fix
     ...(fill && {
       position: 'absolute',
       inset: 0,
       width: '100%',
       height: '100%',
-      objectFit: 'cover',
+      objectFit: style?.objectFit ?? 'cover',
     }),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/image/Image.tsx` around lines 146 - 153, The fill branch in
Image.tsx is forcing objectFit: 'cover' which overrides any consumer-provided
style.objectFit; update the spread so the consumer style wins by merging style
after the fill defaults (or only set objectFit if not provided) within the
component that builds the element's style (the code using the fill && {
position, inset, width, height, objectFit } object) so that style.objectFit
passed in props.style is preserved; adjust the merge order or conditional to
check for style?.objectFit before assigning objectFit in the fill block.

293-315: 🧹 Nitpick | 🔵 Trivial

Remove constant-true branch to reduce noise.

shouldUseSrcSet is always true, so the if (!shouldUseSrcSet) branch is unreachable and adds unnecessary complexity.

💡 Proposed cleanup
-  const shouldUseSrcSet = true
-
   const imgElement = (
     <img
       ref={imgRef}
       src={isVisible ? mainSrc : undefined}
-      srcSet={isVisible && shouldUseSrcSet ? buildSrcSetString(sizesArray, finalSrc, quality, undefined, loader, hasFixedWidth) : undefined}
+      srcSet={isVisible ? buildSrcSetString(sizesArray, finalSrc, quality, undefined, loader, hasFixedWidth) : undefined}
       sizes={hasFixedWidth ? undefined : sizes}
       alt={alt}
@@
-  if (!shouldUseSrcSet)
-    return imgElement
-
   return (
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/image/Image.tsx` around lines 293 - 315, The file defines a
redundant constant shouldUseSrcSet = true causing the unreachable if
(!shouldUseSrcSet) return imgElement branch; remove the constant and the dead
conditional and simplify the img JSX to always compute srcSet via
buildSrcSetString(sizesArray, finalSrc, quality, undefined, loader,
hasFixedWidth) (or guard with isVisible only as currently done), ensuring
references to imgElement, shouldUseSrcSet, and buildSrcSetString are
updated/removed so no dead code remains.

104-136: ⚠️ Potential issue | 🟠 Major

Visibility can regress when immediate-load props flip off.

On Line 104, hasIntersected always starts false. If shouldLoadImmediately is initially true and later becomes false, isVisible can drop to false and clear src until intersection occurs again.

💡 Proposed fix
 function useImageLazyLoad(
   imgRef: React.RefObject<HTMLImageElement | null>,
   shouldPreload: boolean,
   unoptimized: boolean,
   loading: 'lazy' | 'eager',
 ) {
   const shouldLoadImmediately = shouldPreload || unoptimized || loading === 'eager'
-  const [hasIntersected, setHasIntersected] = useState(false)
+  const [hasIntersected, setHasIntersected] = useState(shouldLoadImmediately)
+
+  useEffect(() => {
+    if (shouldLoadImmediately)
+      setHasIntersected(true)
+  }, [shouldLoadImmediately])

   useEffect(() => {
     if (shouldLoadImmediately)
       return
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/image/Image.tsx` around lines 104 - 136, The visibility
flag resets incorrectly when shouldLoadImmediately goes from true to false;
initialize and preserve intersection state instead of always starting false.
Change the useState for hasIntersected to derive its initial value from
shouldLoadImmediately (or setHasIntersected(true) when shouldLoadImmediately is
true) and ensure nothing in useEffect or elsewhere ever sets hasIntersected back
to false so the returned expression (shouldLoadImmediately || hasIntersected)
won't drop visibility when shouldLoadImmediately flips off; key symbols:
hasIntersected, setHasIntersected, shouldLoadImmediately, imgRef, useEffect,
observer.
packages/deploy/src/utils.ts (1)

123-133: ⚠️ Potential issue | 🟠 Major

ensureMinimumNodeEngine does not enforce the declared >=22.12.0 floor.

Line 123 declares MIN_NODE_VERSION as >=22.12.0, but Lines 128-129 reduce validation to major-only (22). This lets ranges like >=22.0.0 pass unchanged even though they are below the stated minimum.

Please align validation with the full minimum version, or change the declared minimum to major-only if that was the actual intent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/deploy/src/utils.ts` around lines 123 - 133, The current
ensureMinimumNodeEngine uses EXTRACT_MAJOR_REGEX and isNodeVersionSufficient to
check only the major version against MIN_NODE_VERSION (>=22.12.0), letting
ranges like >=22.0.0 slip through; update ensureMinimumNodeEngine to compare
full semver ranges instead of extracting the major: use the semver library
(e.g., semver.minVersion or semver.satisfies/semver.intersects) to derive the
concrete minimum from MIN_NODE_VERSION and verify packageJson.engines.node
satisfies that full minimum (reference symbols: MIN_NODE_VERSION,
ensureMinimumNodeEngine, isNodeVersionSufficient, EXTRACT_MAJOR_REGEX,
MIN_SUPPORTED_NODE_MAJOR); alternatively, if the intent was to enforce a
major-only floor, change MIN_NODE_VERSION to a major-only value (e.g.,
">=22.0.0") and keep the existing major-only logic.
crates/rari/src/runtime/module_reload/manager.rs (1)

125-139: ⚠️ Potential issue | 🟠 Major

Move task cancellation before task spawn to prevent race condition with debounce state.

The task spawned at line 125 can execute before the debounce state is registered via add_pending (line 139). If debounce_delay_ms is 0 or very low, the spawned task's remove_pending call executes before add_pending, leaving a stale orphan entry in the pending map that corrupts subsequent operations on that component_id.

Additionally, ReloadConfig.validate() exists but is never called in the codebase, meaning the debounce delay has no minimum validation and can be set to 0.

Reorder operations to register the pending state before the task can run:

Suggested fix
-        let manager = self.clone_for_task();
-        let component_id_clone = component_id.clone();
-        let file_path_clone = file_path.clone();
-
-        let handle = tokio::spawn(async move {
+        self.debounce_manager.cancel_pending(&component_id).await;
+
+        let manager = self.clone_for_task();
+        let component_id_clone = component_id.clone();
+        let file_path_clone = file_path.clone();
+
+        let handle = tokio::spawn(async move {
             tokio::time::sleep(debounce_delay).await;
 
             manager.debounce_manager.remove_pending(&component_id_clone).await;
 
             if let Err(e) = manager.reload_module(&component_id_clone, &file_path_clone).await {
                 error!("Failed to reload module {}: {}", component_id_clone, e);
             }
         });
 
         let request = ModuleReloadRequest::new(component_id.clone(), file_path.clone());
-
-        self.debounce_manager.cancel_pending(&component_id).await;
-
         self.debounce_manager.add_pending(component_id.clone(), request, handle).await;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/rari/src/runtime/module_reload/manager.rs` around lines 125 - 139,
Move the debounce cancellation before you spawn the background task and ensure
the debounce config is validated: call
self.debounce_manager.cancel_pending(&component_id).await before creating the
tokio::spawn handle so you remove any prior pending state before the new task
can run, then create ModuleReloadRequest::new(...) and call
debounce_manager.add_pending(..., handle).await after that; additionally, invoke
ReloadConfig::validate() where the debounce_delay is derived so debounce_delay
cannot be 0 (use the validated value when constructing the spawned task and
calling remove_pending in manager.debounce_manager.remove_pending and
manager.reload_module).
♻️ Duplicate comments (6)
packages/rari/src/router/props-extractor.ts (1)

182-186: ⚠️ Potential issue | 🟠 Major

Template is still collapsed to a string, which breaks deeper title inheritance.

At Line 185, a parent templated title is converted into a plain string, so downstream merges can no longer apply the inherited template. This is the same concern previously raised for this area and appears reintroduced.

💡 Proposed fix
   if (typeof childTitle === 'string') {
     if (typeof parentTitle === 'object' && parentTitle?.template) {
       const hasPlaceholder = parentTitle.template.includes('%s')
-      return hasPlaceholder ? parentTitle.template.replace('%s', childTitle) : childTitle
+      return {
+        ...parentTitle,
+        default: hasPlaceholder
+          ? parentTitle.template.replace('%s', childTitle)
+          : childTitle,
+      }
     }

     return childTitle
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/router/props-extractor.ts` around lines 182 - 186, The
current branch returns a plain string when childTitle is a string and
parentTitle has a template, which collapses parentTitle.template and prevents
deeper inheritance; instead, keep parentTitle as an object and return a merged
object that preserves parentTitle.template and stores the child string in a
separate field (e.g., { template: parentTitle.template, value: childTitle }) so
downstream merges can still apply the template; update the code path referencing
childTitle/parentTitle in props-extractor.ts to return this merged object rather
than collapsing to a string.
packages/rari/src/proxy/shared/utils.ts (1)

137-142: ⚠️ Potential issue | 🟡 Minor

Preserve empty-string response bodies.

The truthy guard result.body && at line 137 drops empty-string bodies (result.body === ''). An empty body is semantically valid (e.g., 204 No Content responses that still have body set to '').

Proposed fix
-    else if (result.body && typeof result.body === 'string') {
+    else if (typeof result.body === 'string') {
       body = result.body
     }
-    else if (result.body) {
+    else if (result.body != null) {
       console.warn('[rari] Proxy: Response body is not extractable as text')
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/proxy/shared/utils.ts` around lines 137 - 142, The current
truthy guard drops empty-string bodies; update the checks around result.body so
empty strings are preserved: use typeof result.body === 'string' to set body
when result.body is a string (so '' is kept), and change the subsequent
existence check to a null/undefined check (e.g., result.body != null) before
logging the warning, keeping the variable name body and the console.warn('[rari]
Proxy: Response body is not extractable as text') behavior intact.
packages/deploy/src/utils.ts (1)

170-177: ⚠️ Potential issue | 🟡 Minor

Exported function uses a non-exported type in its public signature.

Line 177 exports updatePackageJsonForProvider, while ProviderConfig at Line 170 is not exported. This can cause API/declaration friction and makes the public contract less explicit.

#!/bin/bash
set -euo pipefail

echo "=== Symbol visibility in utils.ts ==="
rg -nP '^(export\s+)?interface\s+ProviderConfig|^export function updatePackageJsonForProvider' packages/deploy/src/utils.ts

echo
echo "=== Declaration-related compiler options under packages/deploy ==="
python - <<'PY'
import json
from pathlib import Path

for path in Path("packages/deploy").rglob("tsconfig*.json"):
    try:
        data = json.loads(path.read_text())
    except Exception as e:
        print(f"--- {path} ---")
        print(f"Failed to parse JSON: {e}")
        continue
    co = data.get("compilerOptions", {})
    print(f"--- {path} ---")
    print({
        "declaration": co.get("declaration"),
        "emitDeclarationOnly": co.get("emitDeclarationOnly"),
        "composite": co.get("composite")
    })
PY
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/deploy/src/utils.ts` around lines 170 - 177, The exported function
updatePackageJsonForProvider currently references a non-exported type
ProviderConfig in its public signature; export the interface ProviderConfig (add
export to the ProviderConfig declaration) so the function's public API surface
is explicit and type declarations can be generated and consumed correctly,
ensuring symbols ProviderConfig and updatePackageJsonForProvider remain
consistent in the module's public types.
packages/rari/src/router/sitemap-generator.ts (3)

228-246: ⚠️ Potential issue | 🟡 Minor

Keep extension discovery aligned with the advertised module types.

This helper now treats .mjs, .cjs, and .json as supported, but findSitemapFiles() still defaults to ['.ts', '.tsx', '.js', '.jsx'], and the current caller in packages/rari/src/vite/server-build.ts:1851-1865 passes that same list. If these formats are meant to work end-to-end, they need to be added to discovery as well; otherwise this branch is unreachable unless every caller opts in manually.

♻️ Minimal fix
 export async function findSitemapFiles(
   appDir: string,
-  extensions: string[] = ['.ts', '.tsx', '.js', '.jsx'],
+  extensions: string[] = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.json'],
 ): Promise<SitemapFile[]> {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/router/sitemap-generator.ts` around lines 228 - 246, The
determineModuleType function lists .mjs, .cjs, and .json as supported module
types but discovery still uses a narrower set; update sitemap discovery so those
extensions are actually found by adding '.mjs', '.cjs', and '.json' to the
default extensions array used by findSitemapFiles (and ensure the caller in
server-build.ts uses that updated list rather than the hardcoded
['.ts','.tsx','.js','.jsx']), or centralize the allowedExtensions constant and
reference it from determineModuleType, findSitemapFiles, and the caller to keep
discovery and type handling in sync.

323-325: ⚠️ Potential issue | 🟠 Major

Pass the original shard ID into the module function.

Stringifying here changes semantics for any non-string ID returned by generateSitemaps(). The filename still needs a sanitized string, but the shard function should receive the original id value.

🔧 Proposed fix
       const sitemapData = typeof module.default === 'function'
-        ? await module.default({ id: String(id) })
+        ? await module.default({ id })
         : module.default
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/router/sitemap-generator.ts` around lines 323 - 325, The
code currently stringifies the shard id when calling the module function (const
sitemapData = typeof module.default === 'function' ? await module.default({ id:
String(id) }) : module.default), which alters semantics for non-string ids;
change the call to pass the original id value (await module.default({ id })) and
only stringify/sanitize when constructing filenames for the sitemap file output
so the shard function receives the original id type while filenames still use a
sanitized string.

253-259: ⚠️ Potential issue | 🟠 Major

Delegate relative imports back to Rolldown's resolver.

resolveId() receives the fully resolved importer, returning null defers to the rest of the resolver chain, and this.resolve(...) exists specifically to re-use that chain. This branch short-circuits all relative imports through path.resolve(...), so imports from the virtual entry skip the VIRTUAL_SITEMAP_ID base case and, in practice, can miss normal extension resolution for common ./helper imports. (rolldown.rs)

🔧 Proposed fix
-    resolveId(id: string, importer?: string) {
+    async resolveId(id: string, importer?: string) {
       if (id === VIRTUAL_SITEMAP_ID)
         return id
-      if (id.startsWith('.'))
-        return path.resolve(path.dirname(importer ?? sitemapFile.path), id)
-
-      return null
+      if (!id.startsWith('.'))
+        return null
+
+      const baseImporter = importer === VIRTUAL_SITEMAP_ID || !importer
+        ? sitemapFile.path
+        : importer
+
+      return this.resolve(id, baseImporter, { skipSelf: true })
     },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/router/sitemap-generator.ts` around lines 253 - 259, The
resolveId branch that handles relative imports currently short-circuits
resolution by returning path.resolve(...), which bypasses Rollup/Rolldown's
resolver chain; change the id.startsWith('.') branch in resolveId to delegate to
the resolver chain by returning this.resolve(id, importer ?? sitemapFile.path)
instead of calling path.resolve, so relative imports from the virtual sitemap
entry (VIRTUAL_SITEMAP_ID) go through normal extension and plugin resolution.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/rari/src/rsc/rendering/core/js/component_eval_setup.js`:
- Line 10: The element factory currently sets the property named $typeof but
downstream checks expect $$typeof; update the created element object so the key
is $$typeof (with value Symbol.for('react.transitional.element')) instead of
$typeof so component identity and error handling in component_render.js and
streaming_react_setup.js will match; ensure any place constructing transitional
elements uses the $$typeof symbol key consistently.
- Around line 25-28: The current fallbacks for _jsx and _jsxs use a variadic
arrow that forwards all args into globalThis.React.createElement, which treats a
third arg (key) as a child and corrupts element identity; replace the fallback
with a small wrapper that accepts (type, props, key) (or checks args.length) and
calls globalThis.React.createElement(type, props, key) so the key is passed as
the third parameter; apply this change to both _jsx and _jsxs references in the
file.

In `@crates/rari/src/runtime/module_reload/manager.rs`:
- Around line 246-249: The task join failures are being mislabeled as
ModuleReloadError::RuntimeNotAvailable; add a new enum variant (e.g.,
ModuleReloadError::TaskJoinFailure or BatchTaskFailure) to the ModuleReloadError
type and use it when wrapping tokio join errors instead of RuntimeNotAvailable;
update the error construction call (currently using
RariError::module_reload(ModuleReloadError::RuntimeNotAvailable { message: ...
})) to RariError::module_reload(ModuleReloadError::TaskJoinFailure { message:
format!("Task join error: {}", e) }) and adjust any Display/From/serialization
implementations and tests that generate or match module-reload errors so the new
variant is handled consistently.

In `@packages/rari/src/proxy/execute-proxy.ts`:
- Around line 33-35: The code reads the private field proxyFn via a cast
((executor as any).proxyFn), which is fragile; add a public accessor on
ProxyExecutor (e.g., getProxyFunction(): ProxyFunction | null) in executor.ts to
return the underlying proxy function, then update execute-proxy.ts to call
executor.getProxyFunction() and validate its type instead of using the any cast;
reference ProxyExecutor, getProxyFunction, and the local proxyFn usage in
execute-proxy.ts when making the change.
- Around line 24-31: The current waitUntil implementation swallows rejections by
using .catch(err => return err) so handledPromise always resolves and
Promise.allSettled cannot detect rejections; change the handler in
event.waitUntil (around the event/waitUntil/waitUntilPromises code) to attach a
catch that rethrows the error (or use .then(undefined, err => { throw err })) so
the promise remains rejected while still having a handler to avoid
unhandledRejection warnings, allowing the later Promise.allSettled/status ===
'rejected' logic to work correctly.

---

Outside diff comments:
In `@crates/rari/src/runtime/module_reload/manager.rs`:
- Around line 125-139: Move the debounce cancellation before you spawn the
background task and ensure the debounce config is validated: call
self.debounce_manager.cancel_pending(&component_id).await before creating the
tokio::spawn handle so you remove any prior pending state before the new task
can run, then create ModuleReloadRequest::new(...) and call
debounce_manager.add_pending(..., handle).await after that; additionally, invoke
ReloadConfig::validate() where the debounce_delay is derived so debounce_delay
cannot be 0 (use the validated value when constructing the spawned task and
calling remove_pending in manager.debounce_manager.remove_pending and
manager.reload_module).

In `@packages/deploy/src/utils.ts`:
- Around line 123-133: The current ensureMinimumNodeEngine uses
EXTRACT_MAJOR_REGEX and isNodeVersionSufficient to check only the major version
against MIN_NODE_VERSION (>=22.12.0), letting ranges like >=22.0.0 slip through;
update ensureMinimumNodeEngine to compare full semver ranges instead of
extracting the major: use the semver library (e.g., semver.minVersion or
semver.satisfies/semver.intersects) to derive the concrete minimum from
MIN_NODE_VERSION and verify packageJson.engines.node satisfies that full minimum
(reference symbols: MIN_NODE_VERSION, ensureMinimumNodeEngine,
isNodeVersionSufficient, EXTRACT_MAJOR_REGEX, MIN_SUPPORTED_NODE_MAJOR);
alternatively, if the intent was to enforce a major-only floor, change
MIN_NODE_VERSION to a major-only value (e.g., ">=22.0.0") and keep the existing
major-only logic.

In `@packages/rari/src/image/Image.tsx`:
- Around line 146-153: The fill branch in Image.tsx is forcing objectFit:
'cover' which overrides any consumer-provided style.objectFit; update the spread
so the consumer style wins by merging style after the fill defaults (or only set
objectFit if not provided) within the component that builds the element's style
(the code using the fill && { position, inset, width, height, objectFit }
object) so that style.objectFit passed in props.style is preserved; adjust the
merge order or conditional to check for style?.objectFit before assigning
objectFit in the fill block.
- Around line 293-315: The file defines a redundant constant shouldUseSrcSet =
true causing the unreachable if (!shouldUseSrcSet) return imgElement branch;
remove the constant and the dead conditional and simplify the img JSX to always
compute srcSet via buildSrcSetString(sizesArray, finalSrc, quality, undefined,
loader, hasFixedWidth) (or guard with isVisible only as currently done),
ensuring references to imgElement, shouldUseSrcSet, and buildSrcSetString are
updated/removed so no dead code remains.
- Around line 104-136: The visibility flag resets incorrectly when
shouldLoadImmediately goes from true to false; initialize and preserve
intersection state instead of always starting false. Change the useState for
hasIntersected to derive its initial value from shouldLoadImmediately (or
setHasIntersected(true) when shouldLoadImmediately is true) and ensure nothing
in useEffect or elsewhere ever sets hasIntersected back to false so the returned
expression (shouldLoadImmediately || hasIntersected) won't drop visibility when
shouldLoadImmediately flips off; key symbols: hasIntersected, setHasIntersected,
shouldLoadImmediately, imgRef, useEffect, observer.

---

Duplicate comments:
In `@packages/deploy/src/utils.ts`:
- Around line 170-177: The exported function updatePackageJsonForProvider
currently references a non-exported type ProviderConfig in its public signature;
export the interface ProviderConfig (add export to the ProviderConfig
declaration) so the function's public API surface is explicit and type
declarations can be generated and consumed correctly, ensuring symbols
ProviderConfig and updatePackageJsonForProvider remain consistent in the
module's public types.

In `@packages/rari/src/proxy/shared/utils.ts`:
- Around line 137-142: The current truthy guard drops empty-string bodies;
update the checks around result.body so empty strings are preserved: use typeof
result.body === 'string' to set body when result.body is a string (so '' is
kept), and change the subsequent existence check to a null/undefined check
(e.g., result.body != null) before logging the warning, keeping the variable
name body and the console.warn('[rari] Proxy: Response body is not extractable
as text') behavior intact.

In `@packages/rari/src/router/props-extractor.ts`:
- Around line 182-186: The current branch returns a plain string when childTitle
is a string and parentTitle has a template, which collapses parentTitle.template
and prevents deeper inheritance; instead, keep parentTitle as an object and
return a merged object that preserves parentTitle.template and stores the child
string in a separate field (e.g., { template: parentTitle.template, value:
childTitle }) so downstream merges can still apply the template; update the code
path referencing childTitle/parentTitle in props-extractor.ts to return this
merged object rather than collapsing to a string.

In `@packages/rari/src/router/sitemap-generator.ts`:
- Around line 228-246: The determineModuleType function lists .mjs, .cjs, and
.json as supported module types but discovery still uses a narrower set; update
sitemap discovery so those extensions are actually found by adding '.mjs',
'.cjs', and '.json' to the default extensions array used by findSitemapFiles
(and ensure the caller in server-build.ts uses that updated list rather than the
hardcoded ['.ts','.tsx','.js','.jsx']), or centralize the allowedExtensions
constant and reference it from determineModuleType, findSitemapFiles, and the
caller to keep discovery and type handling in sync.
- Around line 323-325: The code currently stringifies the shard id when calling
the module function (const sitemapData = typeof module.default === 'function' ?
await module.default({ id: String(id) }) : module.default), which alters
semantics for non-string ids; change the call to pass the original id value
(await module.default({ id })) and only stringify/sanitize when constructing
filenames for the sitemap file output so the shard function receives the
original id type while filenames still use a sanitized string.
- Around line 253-259: The resolveId branch that handles relative imports
currently short-circuits resolution by returning path.resolve(...), which
bypasses Rollup/Rolldown's resolver chain; change the id.startsWith('.') branch
in resolveId to delegate to the resolver chain by returning this.resolve(id,
importer ?? sitemapFile.path) instead of calling path.resolve, so relative
imports from the virtual sitemap entry (VIRTUAL_SITEMAP_ID) go through normal
extension and plugin resolution.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1bba13d9-616f-4801-b885-12a7035e302b

📥 Commits

Reviewing files that changed from the base of the PR and between 616fe8d and f9c5fab.

📒 Files selected for processing (10)
  • crates/rari/src/rsc/rendering/core/js/component_eval_setup.js
  • crates/rari/src/runtime/ext/web/init_fetch.js
  • crates/rari/src/runtime/module_reload/manager.rs
  • packages/deploy/src/utils.ts
  • packages/rari/src/image/Image.tsx
  • packages/rari/src/proxy/execute-proxy.ts
  • packages/rari/src/proxy/executor.ts
  • packages/rari/src/proxy/shared/utils.ts
  • packages/rari/src/router/props-extractor.ts
  • packages/rari/src/router/sitemap-generator.ts

Comment thread crates/rari/src/rsc/rendering/core/js/component_eval_setup.js Outdated
Comment thread crates/rari/src/rsc/rendering/core/js/component_eval_setup.js Outdated
Comment thread crates/rari/src/runtime/module_reload/manager.rs
Comment thread packages/rari/src/proxy/execute-proxy.ts
Comment thread packages/rari/src/proxy/execute-proxy.ts Outdated
- Fix React element symbol from $typeof to $$typeof for proper element detection
- Replace spread operator with explicit parameters in _jsx and _jsxs fallbacks
- Optimize image lazy loading with immediate state initialization based on shouldLoadImmediately
- Add ref tracking to handle shouldLoadImmediately state changes without extra renders
- Reorder style application in buildImageStyle to allow custom styles to override fill styles
- Remove unnecessary shouldUseSrcSet variable and conditional logic in OptimizedImage
- Improve error handling in executeProxy by throwing caught errors instead of returning them
- Add getProxyFunction() method to ProxyExecutor for cleaner proxy function access
- Simplify history state management in ClientRouter by removing redundant path comparison
Comment thread packages/rari/src/router/ClientRouter.tsx Outdated
Copy link
Copy Markdown

@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: 4

♻️ Duplicate comments (6)
crates/rari/src/rsc/rendering/core/js/component_eval_setup.js (2)

10-10: ⚠️ Potential issue | 🔴 Critical

Use $$typeof for element identity (Line 10).

Downstream render paths match $$typeof, not $typeof; this mismatch can break element recognition.

Suggested patch
       const element = {
-        $typeof: Symbol.for('react.transitional.element'),
+        $$typeof: Symbol.for('react.transitional.element'),
         type,
         props: propsWithoutKey,
         key,
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/rari/src/rsc/rendering/core/js/component_eval_setup.js` at line 10,
The object currently uses the property "$typeof" which downstream renderers
expect as "$$typeof"; update the key from "$typeof" to "$$typeof" so it sets
Symbol.for('react.transitional.element') on the expected identity property
(i.e., replace the "$typeof" property assignment with "$$typeof:
Symbol.for('react.transitional.element')").

25-28: ⚠️ Potential issue | 🟠 Major

Fix _jsx/_jsxs fallback signature mismatch with React.createElement (Lines 25-28).

The current variadic fallback can pass JSX runtime key as a child argument. Wrap fallbacks as (type, props, key) and merge key into props only when defined.

Suggested patch
 if (typeof _jsx === 'undefined')
-  var _jsx = globalThis['~react']?.jsxRuntime?.jsx || globalThis.jsx || ((...args) => globalThis.React.createElement(...args))
+  var _jsx = globalThis['~react']?.jsxRuntime?.jsx
+    || globalThis.jsx
+    || ((type, props, key) => globalThis.React.createElement(type, key === undefined ? props : { ...(props || {}), key }))
 if (typeof _jsxs === 'undefined')
-  var _jsxs = globalThis['~react']?.jsxRuntime?.jsxs || globalThis.jsxs || ((...args) => globalThis.React.createElement(...args))
+  var _jsxs = globalThis['~react']?.jsxRuntime?.jsxs
+    || globalThis.jsxs
+    || ((type, props, key) => globalThis.React.createElement(type, key === undefined ? props : { ...(props || {}), key }))
#!/bin/bash
set -euo pipefail

echo "1) Inspect current fallback implementations:"
rg -nP "var _jsx|var _jsxs|createElement\\(\\.\\.\\.args\\)" crates/rari/src/rsc/rendering/core/js/component_eval_setup.js -C2

echo
echo "2) Find transpiled callsites passing a 3rd argument (potential key):"
rg -nP '\b_jsxs?\s*\([^,]+,[^,]+,[^)]+\)' crates/rari/src/rsc/rendering/core/js -g '!**/node_modules/**'

echo
echo "Expected:"
echo "- If step (2) returns matches, current variadic fallback risks treating key as child."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/rari/src/rsc/rendering/core/js/component_eval_setup.js` around lines
25 - 28, The fallback implementations for _jsx and _jsxs currently use a
variadic signature that may pass the JSX-runtime key as a child to
React.createElement; change the fallbacks for _jsx and _jsxs to accept (type,
props, key) and, when key is defined, copy or set props.key = key (or create a
shallow props copy with key) before calling globalThis.React.createElement(type,
props) so the third argument is merged into props only when present; update the
fallback expressions that reference globalThis['~react']?.jsxRuntime?.jsx or
globalThis.jsx to follow this (type, props, key) -> merge key -> call
React.createElement to match the JSX runtime signature for both _jsx and _jsxs.
packages/rari/src/router/sitemap-generator.ts (1)

273-284: 🧹 Nitpick | 🔵 Trivial

Consider using rolldown's exported types for better type safety.

The result parameter and array callbacks still use any. As noted in a previous review, rolldown exports the RolldownBuild type which could improve type safety here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/router/sitemap-generator.ts` around lines 273 - 284,
Replace the use of any in extractChunkCode by importing and using rolldown's
exported build/output types: change the parameter type of result to
RolldownBuild (or the exact exported build type) and type the output items as
the Rolldown output chunk type (e.g., RolldownOutputChunk or the exact exported
output item type) so the find callbacks use properly typed items instead of any;
keep the same runtime checks for entryChunk and return type string from
extractChunkCode but update signatures and local variables (result, entryChunk,
and the callback parameter) to use the rolldown types for full type safety.
crates/rari/src/server/middleware/request_context.rs (1)

153-167: ⚠️ Potential issue | 🟠 Major

Persist merged tags back into the in-flight shared result.

Line 153 returns a merged follower result, but only updates fetch_cache (Line 164). guard is left unchanged, so subsequent followers still merge from stale leader tags and can lose previously merged follower tags.

🔧 Proposed fix
         if let Some(result) = guard.as_ref() {
             let mut cloned_result = result.clone()?;

             let mut tag_set: FxHashSet<String> = cloned_result.tags.iter().cloned().collect();
             tag_set.extend(tags.iter().cloned());
             let mut merged_tags: Vec<String> = tag_set.into_iter().collect();
             merged_tags.sort();
             cloned_result.tags = merged_tags;

             {
                 let mut cache = self.fetch_cache.lock();
                 cache.put(cache_key.clone(), cloned_result.clone());
             }
+            *guard = Some(Ok(cloned_result.clone()));

             return Ok(cloned_result);
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/rari/src/server/middleware/request_context.rs` around lines 153 - 167,
The merged tags are only written to fetch_cache (cloned_result) but not
persisted back into the in-flight shared `guard`, so subsequent followers will
read stale tags; update the in-flight shared result referenced by `guard` (the
same object behind `guard.as_ref()` / `result`) to store `merged_tags` (or
replace its tags with `cloned_result.tags`) before releasing the lock and
returning, ensuring both `self.fetch_cache` (cache_key) and the in-flight
`guard` reflect the merged tag set.
packages/rari/src/runtime/shared/get-client-component.ts (1)

155-155: 🧹 Nitpick | 🔵 Trivial

Consider using getPathVariants to reduce duplication.

Line 155 duplicates the exact logic defined in getPathVariants (lines 209-211). Consider calling the helper function for better maintainability.

♻️ Suggested refactor
-  const variants = path.startsWith('./') ? [path, path.slice(2)] : [path, `./${path}`]
+  const variants = getPathVariants(path)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/runtime/shared/get-client-component.ts` at line 155, The
line creating `variants` duplicates logic found in `getPathVariants`; replace
the inline ternary with a call to that helper by setting `variants =
getPathVariants(path)` in `get-client-component.ts` (where `variants` and `path`
are used), ensuring the helper `getPathVariants` is in scope so you remove the
duplicated logic and centralize path-variant generation.
packages/rari/src/vite/image-scanner.ts (1)

250-322: ⚠️ Potential issue | 🟠 Major

Template expression brace tracking still conflates counters.

The fix for nested objects introduced templateDepth but the issue persists: braceCount is used for both ${...} template expression delimiters (lines 294, 305) and object literal braces (lines 311, 314). Objects inside template expressions cause extraction to fail.

Example that breaks: { src: \/images/${getPath({type: 'foo'})}.jpg` }`

  • The inner object's } decrements braceCount via line 305, causing the outer object's closing } to drive braceCount negative.
🐛 Proposed fix: separate template expression depth from object brace count
 function extractBalancedBraces(code: string, startIndex: number): string | null {
   let braceCount = 0
   let inString = false
   let stringChar = ''
   let escaped = false
   let templateDepth = 0
+  let templateExprDepth = 0
 
   for (let i = startIndex; i < code.length; i++) {
     // ... escape and string start handling unchanged ...
 
-    if (inString && stringChar === '`' && char === '$' && i + 1 < code.length && code[i + 1] === '{') {
-      braceCount++
+    if (inString && stringChar === '`' && templateExprDepth === 0 && char === '$' && i + 1 < code.length && code[i + 1] === '{') {
+      templateExprDepth++
       i++
       continue
     }
 
-    if (inString && stringChar === '`' && braceCount > 0 && char === '`') {
-      templateDepth++
-      continue
+    if (inString && stringChar === '`' && templateExprDepth > 0) {
+      if (char === '{')
+        templateExprDepth++
+      else if (char === '}') {
+        templateExprDepth--
+        continue
+      }
     }
-
-    if (inString && stringChar === '`' && char === '}' && braceCount > 0) {
-      braceCount--
-      continue
-    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/vite/image-scanner.ts` around lines 250 - 322, The
extractBalancedBraces function conflates template-expression braces with
object-literal braces using a single braceCount; split these concerns by
introducing two counters (e.g., objBraceCount and templateExprCount) and only
decrement/increment the appropriate counter depending on context: use
templateExprCount for tracking ${...} expressions inside backtick strings
(increment when seeing ${ and decrement on matching } inside that expression)
and use objBraceCount for top-level { } object literals (increment/decrement
only when not inside a template expression or string). Update the logic around
inString/stringChar/templateDepth to (1) increment templateExprCount when you
encounter '${' while in a backtick string, (2) route '}' handling to decrement
templateExprCount when templateExprCount>0, otherwise decrement objBraceCount,
and (3) return the substring when objBraceCount reaches zero; ensure escaped
chars and nested backtick/template backtracking still respect these separate
counters (symbols: extractBalancedBraces, braceCount -> replace uses with
objBraceCount/templateExprCount, inString, stringChar, templateDepth).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/rari/src/server/middleware/request_context.rs`:
- Around line 132-136: The repeated dedup+sort logic that builds tag_set,
extends with tags, collects to merged_tags, sorts and assigns to result.tags
should be extracted into a helper (e.g., fn merge_and_sort_tags(existing: impl
IntoIterator<Item=String>, extra: impl IntoIterator<Item=String>) ->
Vec<String>) which returns a deduplicated, sorted Vec<String>; replace the three
inline blocks (the ones manipulating tag_set, merged_tags and assigning
result.tags) with calls to this helper, and update call sites to pass
result.tags and tags (or other tag sources) into merge_and_sort_tags and assign
the return back to result.tags to centralize behavior and reduce duplication.

In `@packages/rari/src/proxy/shared/utils.ts`:
- Around line 78-95: Duplicate header append/arrayify logic in
packages/rari/src/proxy/shared/utils.ts (the blocks that mutate requestHeaders
and responseHeaders) should be extracted into a single helper (e.g., mergeHeader
or appendOrArrayifyHeader) that accepts the target headers object, the
lower-cased key and the value and performs: if Object.hasOwn(headers, key) then
set headers[key] = Array.isArray(existing) ? [...existing, value] : [existing,
value] else headers[key] = value; replace the two duplicated blocks (the
requestHeaders and responseHeaders branches and the similar code at lines
~122-127) with calls to this helper so behavior is consistent and duplication
removed.
- Around line 73-87: The proxy header detection uses the raw header key casing
so headers like X-Rari-Proxy-Request-... can be missed; in extractProxyHeaders
normalize the incoming header key to lower-case first (e.g., const lowerKey =
key.toLowerCase()) and then perform the prefix checks against lowerKey (use
lowerKey.startsWith('x-rari-proxy-request-') and
lowerKey.startsWith('x-rari-proxy-')), compute headerName from lowerKey (strip
the 'x-rari-proxy-request-' prefix) and continue updating
requestHeaders/response path using the normalized key to ensure consistent
behavior regardless of original header casing.

In `@packages/rari/src/router/navigation-utils.ts`:
- Around line 20-22: matchStaticSegment currently compares the raw path token to
segment.value which fails for encoded characters (e.g., "/caf%C3%A9"); update
matchStaticSegment to decode the incoming token (and decode segment.value for
safety) using decodeURIComponent before doing the equality check so static
routes with percent-encoded characters match correctly — locate the
matchStaticSegment function and replace the direct === comparison with a
comparison of decoded values.

---

Duplicate comments:
In `@crates/rari/src/rsc/rendering/core/js/component_eval_setup.js`:
- Line 10: The object currently uses the property "$typeof" which downstream
renderers expect as "$$typeof"; update the key from "$typeof" to "$$typeof" so
it sets Symbol.for('react.transitional.element') on the expected identity
property (i.e., replace the "$typeof" property assignment with "$$typeof:
Symbol.for('react.transitional.element')").
- Around line 25-28: The fallback implementations for _jsx and _jsxs currently
use a variadic signature that may pass the JSX-runtime key as a child to
React.createElement; change the fallbacks for _jsx and _jsxs to accept (type,
props, key) and, when key is defined, copy or set props.key = key (or create a
shallow props copy with key) before calling globalThis.React.createElement(type,
props) so the third argument is merged into props only when present; update the
fallback expressions that reference globalThis['~react']?.jsxRuntime?.jsx or
globalThis.jsx to follow this (type, props, key) -> merge key -> call
React.createElement to match the JSX runtime signature for both _jsx and _jsxs.

In `@crates/rari/src/server/middleware/request_context.rs`:
- Around line 153-167: The merged tags are only written to fetch_cache
(cloned_result) but not persisted back into the in-flight shared `guard`, so
subsequent followers will read stale tags; update the in-flight shared result
referenced by `guard` (the same object behind `guard.as_ref()` / `result`) to
store `merged_tags` (or replace its tags with `cloned_result.tags`) before
releasing the lock and returning, ensuring both `self.fetch_cache` (cache_key)
and the in-flight `guard` reflect the merged tag set.

In `@packages/rari/src/router/sitemap-generator.ts`:
- Around line 273-284: Replace the use of any in extractChunkCode by importing
and using rolldown's exported build/output types: change the parameter type of
result to RolldownBuild (or the exact exported build type) and type the output
items as the Rolldown output chunk type (e.g., RolldownOutputChunk or the exact
exported output item type) so the find callbacks use properly typed items
instead of any; keep the same runtime checks for entryChunk and return type
string from extractChunkCode but update signatures and local variables (result,
entryChunk, and the callback parameter) to use the rolldown types for full type
safety.

In `@packages/rari/src/runtime/shared/get-client-component.ts`:
- Line 155: The line creating `variants` duplicates logic found in
`getPathVariants`; replace the inline ternary with a call to that helper by
setting `variants = getPathVariants(path)` in `get-client-component.ts` (where
`variants` and `path` are used), ensuring the helper `getPathVariants` is in
scope so you remove the duplicated logic and centralize path-variant generation.

In `@packages/rari/src/vite/image-scanner.ts`:
- Around line 250-322: The extractBalancedBraces function conflates
template-expression braces with object-literal braces using a single braceCount;
split these concerns by introducing two counters (e.g., objBraceCount and
templateExprCount) and only decrement/increment the appropriate counter
depending on context: use templateExprCount for tracking ${...} expressions
inside backtick strings (increment when seeing ${ and decrement on matching }
inside that expression) and use objBraceCount for top-level { } object literals
(increment/decrement only when not inside a template expression or string).
Update the logic around inString/stringChar/templateDepth to (1) increment
templateExprCount when you encounter '${' while in a backtick string, (2) route
'}' handling to decrement templateExprCount when templateExprCount>0, otherwise
decrement objBraceCount, and (3) return the substring when objBraceCount reaches
zero; ensure escaped chars and nested backtick/template backtracking still
respect these separate counters (symbols: extractBalancedBraces, braceCount ->
replace uses with objBraceCount/templateExprCount, inString, stringChar,
templateDepth).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2c95941b-2241-47c1-8bff-646c0bd18ca3

📥 Commits

Reviewing files that changed from the base of the PR and between f9c5fab and d87730c.

📒 Files selected for processing (9)
  • crates/rari/src/rsc/rendering/core/js/component_eval_setup.js
  • crates/rari/src/server/middleware/request_context.rs
  • packages/deploy/src/utils.ts
  • packages/rari/src/proxy/shared/utils.ts
  • packages/rari/src/router/navigation-utils.ts
  • packages/rari/src/router/sitemap-generator.ts
  • packages/rari/src/runtime/shared/get-client-component.ts
  • packages/rari/src/vite/image-scanner.ts
  • test/unit/router/navigation-utils.test.ts

Comment thread crates/rari/src/server/middleware/request_context.rs Outdated
Comment thread packages/rari/src/proxy/shared/utils.ts Outdated
Comment thread packages/rari/src/proxy/shared/utils.ts Outdated
Comment thread packages/rari/src/router/navigation-utils.ts
Copy link
Copy Markdown

@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

Caution

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

⚠️ Outside diff range comments (1)
packages/rari/src/router/ClientRouter.tsx (1)

382-384: ⚠️ Potential issue | 🔴 Critical

Use requested-path key for abort cleanup to avoid stale pending navigations.

pendingNavigationsRef.current is stored under targetPath, but abort cleanup sometimes deletes with actualTargetPath. On redirects these differ, leaving stale map entries and causing later navigations to short-circuit to an old promise.

🔧 Proposed fix
-  const cleanupAbortedNavigation = (path: string, navigationId: number) => {
-    pendingNavigationsRef.current.delete(path)
+  const cleanupAbortedNavigation = (pendingPath: string, navigationId: number) => {
+    pendingNavigationsRef.current.delete(pendingPath)
@@
-  const handleStreamingResponse = async (
+  const handleStreamingResponse = async (
     response: Response,
     abortController: AbortController,
+    pendingPath: string,
     actualTargetPath: string,
     navigationId: number,
@@
         if (abortController.signal.aborted) {
           await reader.cancel()
-          cleanupAbortedNavigation(actualTargetPath, navigationId)
+          cleanupAbortedNavigation(pendingPath, navigationId)
           return
         }
@@
         if (abortController.signal.aborted) {
-          cleanupAbortedNavigation(actualTargetPath, navigationId)
+          cleanupAbortedNavigation(targetPath, navigationId)
           return
         }
@@
           await handleStreamingResponse(
             response,
             abortController,
+            targetPath,
             actualTargetPath,
             navigationId,
@@
         if (abortController.signal.aborted) {
-          cleanupAbortedNavigation(actualTargetPath, navigationId)
+          cleanupAbortedNavigation(targetPath, navigationId)
           return
         }

Also applies to: 449-450, 671-672, 710-711, 724-729

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/router/ClientRouter.tsx` around lines 382 - 384, The
cleanup function cleanupAbortedNavigation is deleting
pendingNavigationsRef.current entries using the resolved/actual path (variable
named path) but the map is keyed by the originally requested targetPath, causing
stale entries on redirects; update cleanupAbortedNavigation (and the other
cleanup spots that call pendingNavigationsRef.current.delete) to delete using
the requested/requestPath/requestedTargetPath key that was used when the entry
was created (use the same identifier stored when creating the pending navigation
promise instead of actualTargetPath) so redirects remove the correct map entry.
♻️ Duplicate comments (3)
crates/rari/src/rsc/rendering/core/js/component_eval_setup.js (1)

25-28: ⚠️ Potential issue | 🔴 Critical

_jsx/_jsxs fallbacks incorrectly pass key as a child instead of merging into props.

The JSX runtime signature is jsx(type, props, key), but createElement at line 4 uses (type, props, ...children). Passing key as the third argument treats it as a child, corrupting the element. Note that globalThis.jsx at line 35 correctly merges key into props—apply the same pattern here.

Suggested fix
 if (typeof _jsx === 'undefined')
-  var _jsx = globalThis['~react']?.jsxRuntime?.jsx || globalThis.jsx || ((type, props, key) => globalThis.React.createElement(type, props, key))
+  var _jsx = globalThis['~react']?.jsxRuntime?.jsx || globalThis.jsx || ((type, props, key) => globalThis.React.createElement(type, key === undefined ? props : { ...(props || {}), key }))
 if (typeof _jsxs === 'undefined')
-  var _jsxs = globalThis['~react']?.jsxRuntime?.jsxs || globalThis.jsxs || ((type, props, key) => globalThis.React.createElement(type, props, key))
+  var _jsxs = globalThis['~react']?.jsxRuntime?.jsxs || globalThis.jsxs || ((type, props, key) => globalThis.React.createElement(type, key === undefined ? props : { ...(props || {}), key }))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/rari/src/rsc/rendering/core/js/component_eval_setup.js` around lines
25 - 28, The fallback definitions for _jsx and _jsxs pass the third param `key`
directly to globalThis.React.createElement as a child, which corrupts the
element; change the fallback to merge the key into the props object (e.g., if
key is not undefined, create a new propsWithKey = { ...props, key } and pass
that as the second argument) so that _jsx and _jsxs behave like the real
runtime; update the _jsx and _jsxs fallback expressions that reference
globalThis.React.createElement to merge key into props before calling
createElement.
packages/rari/src/router/ClientRouter.tsx (1)

66-69: ⚠️ Potential issue | 🟠 Major

Clear stale <title> and viewport metadata when omitted.

updateBasicMetadata now removes some absent fields, but missing title/viewport values still leave previous-route values in <head>, so metadata leakage is only partially fixed.

💡 Proposed fix
 function updateBasicMetadata(metadata: PageMetadata): void {
-  if (metadata.title)
-    document.title = metadata.title
+  if (metadata.title !== undefined)
+    document.title = metadata.title
+  else
+    document.title = ''

@@
   if (metadata.viewport) {
     updateOrCreateMetaTag('meta[name="viewport"]', {
       name: 'viewport',
       content: metadata.viewport,
     })
   }
+  else {
+    removeMetaTag('meta[name="viewport"]')
+  }
 }

Also applies to: 90-96

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/router/ClientRouter.tsx` around lines 66 - 69,
updateBasicMetadata currently only sets document.title/viewport when provided
but doesn't clear stale values; modify updateBasicMetadata so that if
metadata.title is absent it clears document.title (e.g., set to empty string)
and if metadata.viewport is absent it removes any existing <meta
name="viewport"> or clears its content; similarly apply the same clear/remove
behavior in the other metadata updater that handles title/viewport (the
analogous metadata update function around lines 90-96) so omitted fields no
longer leak previous-route values.
packages/rari/src/proxy/execute-proxy.ts (1)

24-30: 🧹 Nitpick | 🔵 Trivial

Simpler pattern avoids potential timing edge case.

The current catch((err) => { throw err }) creates a new promise that rejects, which allSettled will observe. However, there's a window between when handledPromise rejects and when finally calls allSettled where Node could emit an unhandledRejection warning.

The simpler pattern attaches a no-op handler to suppress the warning while pushing the original promise to preserve its rejection state:

Suggested simplification
     const event = {
       waitUntil: (promise: Promise<unknown>) => {
-        const handledPromise = promise.catch((err) => {
-          throw err
-        })
-        waitUntilPromises.push(handledPromise)
+        promise.catch(() => {}) // suppress unhandled rejection warning
+        waitUntilPromises.push(promise)
       },
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/proxy/execute-proxy.ts` around lines 24 - 30, The waitUntil
implementation currently wraps the incoming promise into handledPromise via
.catch(err => { throw err }), which can cause an unhandledRejection race;
instead attach a noop rejection handler to suppress warnings and push the
original promise to waitUntilPromises so its rejection state is preserved.
Locate the event.waitUntil function and replace the creation of handledPromise
with adding a .catch(() => {}) (or equivalent no-op) on the original promise for
warning suppression, then push the original promise (not the wrapped one) onto
waitUntilPromises while preserving behavior when later awaited with
Promise.allSettled.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/rari/src/image/Image.tsx`:
- Around line 85-86: The preload link only sets imagesizes but not imagesrcset,
so responsive preloading won't work; update the logic in Image.tsx where the
preload <link> is created (the block calling link.setAttribute('imagesizes',
sizes)) to also set the imagesrcset attribute when sizes is provided by
computing or passing the same srcSet value used for the <img> (e.g., the
component's srcSet/computeSrcSet function or prop) and calling
link.setAttribute('imagesrcset', srcSet) so the browser can choose the correct
image candidate.

---

Outside diff comments:
In `@packages/rari/src/router/ClientRouter.tsx`:
- Around line 382-384: The cleanup function cleanupAbortedNavigation is deleting
pendingNavigationsRef.current entries using the resolved/actual path (variable
named path) but the map is keyed by the originally requested targetPath, causing
stale entries on redirects; update cleanupAbortedNavigation (and the other
cleanup spots that call pendingNavigationsRef.current.delete) to delete using
the requested/requestPath/requestedTargetPath key that was used when the entry
was created (use the same identifier stored when creating the pending navigation
promise instead of actualTargetPath) so redirects remove the correct map entry.

---

Duplicate comments:
In `@crates/rari/src/rsc/rendering/core/js/component_eval_setup.js`:
- Around line 25-28: The fallback definitions for _jsx and _jsxs pass the third
param `key` directly to globalThis.React.createElement as a child, which
corrupts the element; change the fallback to merge the key into the props object
(e.g., if key is not undefined, create a new propsWithKey = { ...props, key }
and pass that as the second argument) so that _jsx and _jsxs behave like the
real runtime; update the _jsx and _jsxs fallback expressions that reference
globalThis.React.createElement to merge key into props before calling
createElement.

In `@packages/rari/src/proxy/execute-proxy.ts`:
- Around line 24-30: The waitUntil implementation currently wraps the incoming
promise into handledPromise via .catch(err => { throw err }), which can cause an
unhandledRejection race; instead attach a noop rejection handler to suppress
warnings and push the original promise to waitUntilPromises so its rejection
state is preserved. Locate the event.waitUntil function and replace the creation
of handledPromise with adding a .catch(() => {}) (or equivalent no-op) on the
original promise for warning suppression, then push the original promise (not
the wrapped one) onto waitUntilPromises while preserving behavior when later
awaited with Promise.allSettled.

In `@packages/rari/src/router/ClientRouter.tsx`:
- Around line 66-69: updateBasicMetadata currently only sets
document.title/viewport when provided but doesn't clear stale values; modify
updateBasicMetadata so that if metadata.title is absent it clears document.title
(e.g., set to empty string) and if metadata.viewport is absent it removes any
existing <meta name="viewport"> or clears its content; similarly apply the same
clear/remove behavior in the other metadata updater that handles title/viewport
(the analogous metadata update function around lines 90-96) so omitted fields no
longer leak previous-route values.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 76b5c7b8-e5ab-47cc-b0a0-dd596ed3bdd4

📥 Commits

Reviewing files that changed from the base of the PR and between d87730c and c07ba98.

📒 Files selected for processing (5)
  • crates/rari/src/rsc/rendering/core/js/component_eval_setup.js
  • packages/rari/src/image/Image.tsx
  • packages/rari/src/proxy/execute-proxy.ts
  • packages/rari/src/proxy/executor.ts
  • packages/rari/src/router/ClientRouter.tsx

Comment thread packages/rari/src/image/Image.tsx Outdated
- Fix JSX runtime to properly handle key prop by merging with props object
- Extract merge_and_sort_tags helper to reduce duplication in fetch cache tag handling
- Update fetch cache to persist guard state after tag merging
- Add imagesrcset attribute generation for image preloading optimization
- Simplify waitUntil promise handling by removing unnecessary error wrapping
- Extract mergeHeader helper to consolidate header merging logic in proxy utilities
- Normalize header key comparison to lowercase for consistent proxy header extraction
- Improve code maintainability by reducing repeated tag merging and header processing logic
Comment thread packages/rari/src/router/ClientRouter.tsx Outdated
Comment thread packages/rari/src/image/Image.tsx Outdated
Copy link
Copy Markdown

@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: 4

Caution

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

⚠️ Outside diff range comments (3)
crates/rari/src/server/middleware/request_context.rs (1)

89-93: ⚠️ Potential issue | 🟠 Major

Exclude tags from cache key generation to prevent cache fragmentation.

At Line 92, tags are still treated as cache-relevant options. Requests that differ only by tags will miss the same cache entry/in-flight lock, which defeats cross-request tag accumulation and causes unnecessary upstream fetches.

💡 Proposed fix
-            .filter(|(k, _)| !matches!(k.as_str(), "cacheTTLMs" | "timeout"))
+            .filter(|(k, _)| !matches!(k.as_str(), "cacheTTLMs" | "timeout" | "tags"))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/rari/src/server/middleware/request_context.rs` around lines 89 - 93,
The cache key generator generate_cache_key currently treats "tags" as
cache-relevant and thus includes it in cache_relevant_options; update the filter
inside generate_cache_key (the closure building cache_relevant_options) to also
exclude the "tags" key (i.e., add "tags" to the matches! pattern alongside
"cacheTTLMs" and "timeout") so that requests differing only by tags map to the
same cache/in-flight lock and avoid cache fragmentation.
packages/rari/src/proxy/execute-proxy.ts (1)

12-17: 🧹 Nitpick | 🔵 Trivial

Race condition during concurrent initialization is mitigated by internal synchronization.

Multiple requests may simultaneously see isInitialized() return false and call initializeProxyFromManifest(). This causes the manifest file to be read redundantly (minor overhead). However, the proxy loading itself is already synchronized: ProxyExecutor.loadProxy() caches its initialization promise, so only the first caller executes doLoadProxy() while others await the same promise.

The suggested module-level caching won't prevent this race condition in practice—concurrent requests will still see isInitialized() as false—and introduces issues in serverless environments where module state resets between invocations. No immediate action needed; redundant manifest reads are acceptable given the internal synchronization already in place.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/proxy/execute-proxy.ts` around lines 12 - 17, Do not add
module-level caching; the race where multiple callers see
executor.isInitialized() === false is harmless because ProxyExecutor.loadProxy()
caches the initialization promise so only the first caller runs doLoadProxy()
while others await it; leave the existing
initializeProxyFromManifest('./dist/proxy-manifest.json') call as-is and add a
brief inline comment near isInitialized/initializeProxyFromManifest explaining
that ProxyExecutor.loadProxy() handles synchronization (references:
isInitialized, initializeProxyFromManifest, ProxyExecutor.loadProxy,
doLoadProxy).
packages/rari/src/router/ClientRouter.tsx (1)

382-390: ⚠️ Potential issue | 🟡 Minor

State update is a no-op that triggers unnecessary re-render.

The setNavigationState call at lines 386-388 spreads prev without changing any values, creating a new object that's functionally identical. This triggers an unnecessary React re-render.

Either remove the state update if no changes are needed, or add the intended state changes.

Proposed fix: remove no-op state update or clarify intent
   const cleanupAbortedNavigation = (pendingPath: string, navigationId: number) => {
     pendingNavigationsRef.current.delete(pendingPath)
-
-    if (isMountedRef.current && navigationState.navigationId === navigationId) {
-      setNavigationState(prev => ({
-        ...prev,
-      }))
-    }
   }

If a re-render is intentionally needed to sync state, add a comment explaining why:

   const cleanupAbortedNavigation = (pendingPath: string, navigationId: number) => {
     pendingNavigationsRef.current.delete(pendingPath)
 
     if (isMountedRef.current && navigationState.navigationId === navigationId) {
+      // Force re-render to ensure UI reflects aborted navigation state
       setNavigationState(prev => ({
         ...prev,
       }))
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/router/ClientRouter.tsx` around lines 382 - 390, The
setNavigationState call inside cleanupAbortedNavigation is a no-op (it spreads
prev without changes) and causes an unnecessary re-render; either remove the
setNavigationState invocation entirely or replace it with the actual state
mutation you intended (e.g., updating navigationState properties) and ensure
pendingNavigationsRef.current.delete(pendingPath) is preserved; if you
intentionally relied on forcing a re-render, replace the call with an explicit
intent (e.g., toggle a dedicated renderKey in navigationState) or add a clear
comment explaining why the no-op update is required. Ensure changes are made in
the cleanupAbortedNavigation function and reference navigationState,
pendingNavigationsRef, isMountedRef, and setNavigationState accordingly.
♻️ Duplicate comments (5)
packages/rari/src/runtime/shared/get-client-component.ts (1)

50-53: ⚠️ Potential issue | 🟠 Major

Use nullish checks for loaded component guards.

Line 52 and Line 67 still use truthy checks, so valid falsy exports (0, '', false) are treated as missing. This can return null after a successful load and can also re-enter the loader path unnecessarily.

Suggested fix
   if (componentInfo.loader) {
     const loadedComponent = await executeLoader(componentInfo)
-    if (!loadedComponent)
+    if (loadedComponent == null)
       return null
@@
 function triggerComponentLoad(componentInfo: LazyComponentInfo): Promise<any> {
-  if (!componentInfo.loader || componentInfo.loading || componentInfo.component || componentInfo.loadPromise)
+  if (!componentInfo.loader || componentInfo.loading || componentInfo.component != null || componentInfo.loadPromise)
     return Promise.resolve(null)

Also applies to: 66-68

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/runtime/shared/get-client-component.ts` around lines 50 -
53, The current truthy guards around the loader result treat valid falsy exports
(0, '', false) as missing; in the loader flow (when componentInfo.loader is
present) and the subsequent guard use nullish checks instead of truthy checks:
replace checks like `if (!loadedComponent)` and any other `!` truthy guard for
the result with a nullish check (e.g., `if (loadedComponent == null)` or `if
(loadedComponent === null || loadedComponent === undefined)`) so executeLoader's
return values (see executeLoader and the loadedComponent variable in
getClientComponent) are preserved when they are valid falsy values and to avoid
re-entering the loader path unnecessarily.
packages/rari/src/router/ClientRouter.tsx (2)

66-68: 🧹 Nitpick | 🔵 Trivial

Consider clearing or resetting document.title when metadata.title is absent.

The function sets document.title when provided but doesn't handle the case when it's absent. Unlike meta tags that can be removed, document.title always exists. If a page doesn't specify a title, the previous page's title persists.

Consider setting a default fallback title when metadata.title is undefined.

Proposed fix
 function updateBasicMetadata(metadata: PageMetadata): void {
   if (metadata.title)
     document.title = metadata.title
+  // Optionally: else document.title = 'Default Site Title'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/router/ClientRouter.tsx` around lines 66 - 68, The
updateBasicMetadata function only sets document.title when metadata.title
exists, leaving the previous title if absent; modify updateBasicMetadata (and
reference PageMetadata) to set document.title to a sensible default (e.g., an
app-level fallback string or empty string) when metadata.title is undefined or
empty so the title is cleared/reset for pages that don't provide one. Ensure the
fallback is a constant or config value used elsewhere so it's easy to change.

659-671: ⚠️ Potential issue | 🟠 Major

History entry is committed before response body is fully processed.

The history state is pushed/replaced (lines 668-671) after the fetch response headers are received but before the response body is fully processed. If streaming or response parsing fails after this point, the address bar will show the new URL while the content remains on the previous page.

Consider deferring the history update until after handleStreamingResponse or handleNonStreamingResponse completes successfully, or use a two-phase approach: update history optimistically but restore it on failure.

Proposed fix: defer history update to completeNavigation
-        const urlWithHash = hash ? `${actualTargetPath}#${hash}` : actualTargetPath
-        const historyState: HistoryState = {
-          route: actualTargetPath,
-          navigationId,
-          scrollPosition: { x: window.scrollX, y: window.scrollY },
-          timestamp: Date.now(),
-          key: historyKey,
-        }
-
-        if (options.replace)
-          window.history.replaceState(historyState, '', urlWithHash)
-        else
-          window.history.pushState(historyState, '', urlWithHash)
-
         processMetadata(response)
         // ... streaming/non-streaming handling ...

-        completeNavigation(actualTargetPath, hash, options, navigationId)
+        completeNavigation(actualTargetPath, hash, options, navigationId, historyKey)

// Then in completeNavigation, add history update:
   const completeNavigation = (
     actualTargetPath: string,
     hash: string,
     options: NavigationOptions,
     navigationId: number,
+    historyKey: string,
   ) => {
     // ... existing code ...
+    const urlWithHash = hash ? `${actualTargetPath}#${hash}` : actualTargetPath
+    const historyState: HistoryState = {
+      route: actualTargetPath,
+      navigationId,
+      scrollPosition: { x: window.scrollX, y: window.scrollY },
+      timestamp: Date.now(),
+      key: historyKey,
+    }
+    if (options.replace)
+      window.history.replaceState(historyState, '', urlWithHash)
+    else
+      window.history.pushState(historyState, '', urlWithHash)
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/router/ClientRouter.tsx` around lines 659 - 671, The
history update (window.history.pushState/replaceState using urlWithHash and
historyState) is happening before the response body is fully processed; move
that logic into the navigation completion path so the URL is only committed
after handleStreamingResponse or handleNonStreamingResponse finishes
successfully. Specifically, remove or defer the push/replace calls near
urlWithHash/historyState and instead call them from completeNavigation (or the
function that runs after the response handlers complete), using the same
historyState and honoring options.replace; if processing fails, restore the
previous history or avoid mutating it (implement an optimistic two‑phase
approach only if you also roll back on failure).
packages/rari/src/image/Image.tsx (2)

113-115: 🧹 Nitpick | 🔵 Trivial

Avoid render-phase state updates in useImageLazyLoad.

Line 113 updates state during render. This works conditionally, but it makes render impure and is harder to reason about in Strict Mode. Prefer effect-based synchronization.

💡 Proposed refactor
-  const prevShouldLoadImmediatelyRef = useRef(shouldLoadImmediately)
-
-  if (shouldLoadImmediately && !prevShouldLoadImmediatelyRef.current && !hasIntersected)
-    setHasIntersected(true)
-  prevShouldLoadImmediatelyRef.current = shouldLoadImmediately
+  useEffect(() => {
+    if (shouldLoadImmediately)
+      setHasIntersected(true)
+  }, [shouldLoadImmediately])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/image/Image.tsx` around lines 113 - 115, The render
currently mutates state in useImageLazyLoad by calling setHasIntersected(true)
when shouldLoadImmediately changes, which is impure; move that logic into an
effect: create a useEffect that depends on shouldLoadImmediately and
hasIntersected, and inside it check if shouldLoadImmediately &&
!prevShouldLoadImmediatelyRef.current && !hasIntersected then call
setHasIntersected(true); then update prevShouldLoadImmediatelyRef.current =
shouldLoadImmediately inside the same effect (or a separate effect) to keep
render pure; reference the useImageLazyLoad hook and the variables
prevShouldLoadImmediatelyRef, shouldLoadImmediately, hasIntersected, and
setHasIntersected when making the change.

85-92: ⚠️ Potential issue | 🟠 Major

Preload candidate generation diverges from rendered image candidates.

At Line 85-Line 89, imagesrcset is skipped when loader is provided and is always generated with w descriptors. But fixed-width rendering at Line 308 uses DPR descriptors (1x/2x/3x), so preload can select a different resource than the eventual <img>, reducing cache hit probability and wasting bandwidth.

💡 Proposed fix
-    if (!unoptimized && !loader) {
-      const sizesArray = imgWidth ? [imgWidth, imgWidth * 2, imgWidth * 3] : DEFAULT_DEVICE_SIZES
-      const srcSet = sizesArray.map(w => `${buildImageUrl(finalSrc, w, quality)} ${w}w`).join(', ')
-      link.setAttribute('imagesrcset', srcSet)
-    }
-
-    if (sizes)
-      link.setAttribute('imagesizes', sizes)
+    if (!unoptimized) {
+      const hasFixedWidth = !!imgWidth
+      const sizesArray = imgWidth ? [imgWidth, imgWidth * 2, imgWidth * 3] : DEFAULT_DEVICE_SIZES
+      const srcSet = buildSrcSetString(sizesArray, finalSrc, quality, undefined, loader, hasFixedWidth)
+      link.setAttribute('imagesrcset', srcSet)
+
+      if (!hasFixedWidth && sizes)
+        link.setAttribute('imagesizes', sizes)
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/image/Image.tsx` around lines 85 - 92, The preload
imagesrcset generation currently skips when loader exists and always uses width
(w) descriptors, which mismatches the rendered image's DPR descriptors; update
the block that sets srcSet so when the rendered image will use DPR descriptors
(i.e., fixed-width rendering path), produce imagesrcset with DPR descriptors
(1x/2x/3x) by mapping desired DPRs to buildImageUrl(finalSrc,
Math.round(imgWidth * dpr), quality) and joining as `${url} ${dpr}x`; ensure
this logic lives alongside the existing unoptimized/loader checks and updates
the same variables (sizesArray/srcSet/imagesrcset) so preload candidates match
the eventual <img> candidates.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/rari/src/rsc/rendering/core/js/component_eval_setup.js`:
- Around line 31-35: The wrappers globalThis.jsx and globalThis.jsxs currently
always add a key property (creating key: undefined) when building props; change
them to only materialize key when it's provided by checking if key is !==
undefined and, if so, return React.createElement(type, { ...props, key });
otherwise return React.createElement(type, props) so unkeyed elements do not
gain an own-property key; update both globalThis.jsx and globalThis.jsxs
accordingly.

In `@packages/rari/src/router/ClientRouter.tsx`:
- Around line 90-96: The viewport meta handling should mirror
description/keywords removal: if metadata.viewport is present call
updateOrCreateMetaTag('meta[name="viewport"]', { name: 'viewport', content:
metadata.viewport }), otherwise remove the tag so a previous page's viewport
doesn't persist — add an else branch that calls the same removal helper used for
description/keywords (e.g., removeMetaTag('meta[name="viewport"]')) next to the
existing updateOrCreateMetaTag usage and reference metadata.viewport and the
removeMetaTag/updateOrCreateMetaTag helpers.
- Around line 571-603: handleNavigationError currently results in two different
custom events for the same failure—errorHandlerRef.current.handleError(...)
emits 'rari:navigation-error' with only navError while handleNavigationError
emits 'rari:navigate-error' with full {from,to,error,navigationId}; consolidate
them by emitting a single event (preferably 'rari:navigate-error') containing
the full shape from handleNavigationError and remove or disable the event
emission inside errorHandlerRef.current.handleError, or alternatively document
both usages and make their detail shapes consistent; update the code paths
around handleNavigationError and errorHandlerRef.current.handleError to ensure
only one event name/shape is dispatched (or clearly documented) and adjust any
consumers to the unified format.

In `@packages/rari/src/runtime/shared/get-client-component.ts`:
- Around line 220-221: The early return when componentInfo is truthy calls
ensureComponentLoaded(componentInfo) but doesn't forward the exportName, so
direct-id hits containing a "#" can return the whole module instead of the named
export; update the call in get-client-component to forward the exportName (e.g.,
ensureComponentLoaded(componentInfo, exportName) or otherwise include exportName
on componentInfo) so ensureComponentLoaded receives the exportName and returns
the correct exported member.

---

Outside diff comments:
In `@crates/rari/src/server/middleware/request_context.rs`:
- Around line 89-93: The cache key generator generate_cache_key currently treats
"tags" as cache-relevant and thus includes it in cache_relevant_options; update
the filter inside generate_cache_key (the closure building
cache_relevant_options) to also exclude the "tags" key (i.e., add "tags" to the
matches! pattern alongside "cacheTTLMs" and "timeout") so that requests
differing only by tags map to the same cache/in-flight lock and avoid cache
fragmentation.

In `@packages/rari/src/proxy/execute-proxy.ts`:
- Around line 12-17: Do not add module-level caching; the race where multiple
callers see executor.isInitialized() === false is harmless because
ProxyExecutor.loadProxy() caches the initialization promise so only the first
caller runs doLoadProxy() while others await it; leave the existing
initializeProxyFromManifest('./dist/proxy-manifest.json') call as-is and add a
brief inline comment near isInitialized/initializeProxyFromManifest explaining
that ProxyExecutor.loadProxy() handles synchronization (references:
isInitialized, initializeProxyFromManifest, ProxyExecutor.loadProxy,
doLoadProxy).

In `@packages/rari/src/router/ClientRouter.tsx`:
- Around line 382-390: The setNavigationState call inside
cleanupAbortedNavigation is a no-op (it spreads prev without changes) and causes
an unnecessary re-render; either remove the setNavigationState invocation
entirely or replace it with the actual state mutation you intended (e.g.,
updating navigationState properties) and ensure
pendingNavigationsRef.current.delete(pendingPath) is preserved; if you
intentionally relied on forcing a re-render, replace the call with an explicit
intent (e.g., toggle a dedicated renderKey in navigationState) or add a clear
comment explaining why the no-op update is required. Ensure changes are made in
the cleanupAbortedNavigation function and reference navigationState,
pendingNavigationsRef, isMountedRef, and setNavigationState accordingly.

---

Duplicate comments:
In `@packages/rari/src/image/Image.tsx`:
- Around line 113-115: The render currently mutates state in useImageLazyLoad by
calling setHasIntersected(true) when shouldLoadImmediately changes, which is
impure; move that logic into an effect: create a useEffect that depends on
shouldLoadImmediately and hasIntersected, and inside it check if
shouldLoadImmediately && !prevShouldLoadImmediatelyRef.current &&
!hasIntersected then call setHasIntersected(true); then update
prevShouldLoadImmediatelyRef.current = shouldLoadImmediately inside the same
effect (or a separate effect) to keep render pure; reference the
useImageLazyLoad hook and the variables prevShouldLoadImmediatelyRef,
shouldLoadImmediately, hasIntersected, and setHasIntersected when making the
change.
- Around line 85-92: The preload imagesrcset generation currently skips when
loader exists and always uses width (w) descriptors, which mismatches the
rendered image's DPR descriptors; update the block that sets srcSet so when the
rendered image will use DPR descriptors (i.e., fixed-width rendering path),
produce imagesrcset with DPR descriptors (1x/2x/3x) by mapping desired DPRs to
buildImageUrl(finalSrc, Math.round(imgWidth * dpr), quality) and joining as
`${url} ${dpr}x`; ensure this logic lives alongside the existing
unoptimized/loader checks and updates the same variables
(sizesArray/srcSet/imagesrcset) so preload candidates match the eventual <img>
candidates.

In `@packages/rari/src/router/ClientRouter.tsx`:
- Around line 66-68: The updateBasicMetadata function only sets document.title
when metadata.title exists, leaving the previous title if absent; modify
updateBasicMetadata (and reference PageMetadata) to set document.title to a
sensible default (e.g., an app-level fallback string or empty string) when
metadata.title is undefined or empty so the title is cleared/reset for pages
that don't provide one. Ensure the fallback is a constant or config value used
elsewhere so it's easy to change.
- Around line 659-671: The history update (window.history.pushState/replaceState
using urlWithHash and historyState) is happening before the response body is
fully processed; move that logic into the navigation completion path so the URL
is only committed after handleStreamingResponse or handleNonStreamingResponse
finishes successfully. Specifically, remove or defer the push/replace calls near
urlWithHash/historyState and instead call them from completeNavigation (or the
function that runs after the response handlers complete), using the same
historyState and honoring options.replace; if processing fails, restore the
previous history or avoid mutating it (implement an optimistic two‑phase
approach only if you also roll back on failure).

In `@packages/rari/src/runtime/shared/get-client-component.ts`:
- Around line 50-53: The current truthy guards around the loader result treat
valid falsy exports (0, '', false) as missing; in the loader flow (when
componentInfo.loader is present) and the subsequent guard use nullish checks
instead of truthy checks: replace checks like `if (!loadedComponent)` and any
other `!` truthy guard for the result with a nullish check (e.g., `if
(loadedComponent == null)` or `if (loadedComponent === null || loadedComponent
=== undefined)`) so executeLoader's return values (see executeLoader and the
loadedComponent variable in getClientComponent) are preserved when they are
valid falsy values and to avoid re-entering the loader path unnecessarily.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 73d45c38-a92d-48df-a69c-400d43a53892

📥 Commits

Reviewing files that changed from the base of the PR and between c07ba98 and 915b642.

📒 Files selected for processing (8)
  • crates/rari/src/rsc/rendering/core/js/component_eval_setup.js
  • crates/rari/src/server/middleware/request_context.rs
  • packages/rari/src/image/Image.tsx
  • packages/rari/src/proxy/execute-proxy.ts
  • packages/rari/src/proxy/shared/utils.ts
  • packages/rari/src/router/ClientRouter.tsx
  • packages/rari/src/router/navigation-utils.ts
  • packages/rari/src/runtime/shared/get-client-component.ts

Comment thread crates/rari/src/rsc/rendering/core/js/component_eval_setup.js Outdated
Comment thread packages/rari/src/router/ClientRouter.tsx
Comment thread packages/rari/src/router/ClientRouter.tsx
Comment thread packages/rari/src/runtime/shared/get-client-component.ts Outdated
Copy link
Copy Markdown

@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

♻️ Duplicate comments (1)
packages/rari/src/runtime/shared/get-client-component.ts (1)

66-68: ⚠️ Potential issue | 🟡 Minor

Use a nullish guard when checking loaded state in triggerComponentLoad.

Line 67 still uses a truthy check (componentInfo.component), which can treat valid falsy values as “not loaded” and re-trigger loading.

Suggested fix
 function triggerComponentLoad(componentInfo: LazyComponentInfo): Promise<any> {
-  if (!componentInfo.loader || componentInfo.loading || componentInfo.component || componentInfo.loadPromise)
+  if (!componentInfo.loader || componentInfo.loading || componentInfo.component != null || componentInfo.loadPromise)
     return Promise.resolve(null)

   return executeLoader(componentInfo)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/runtime/shared/get-client-component.ts` around lines 66 -
68, The current guard in triggerComponentLoad uses a truthy check for
componentInfo.component which will re-trigger loading for valid falsy
components; update the conditional in triggerComponentLoad to use a nullish
check (e.g., check componentInfo.component != null or componentInfo.component
!== null && componentInfo.component !== undefined) alongside the existing checks
for componentInfo.loader, componentInfo.loading, and componentInfo.loadPromise
so that already-loaded falsy components are treated as loaded and the function
returns Promise.resolve(null) only when truly not loaded.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/rari/src/rsc/rendering/core/js/component_eval_setup.js`:
- Around line 2-3: The React shim should guard against partial globals by
checking for required APIs instead of truthiness; update the initialization
checks (currently using if (!globalThis.React) and similar blocks) to also
verify required members like createElement (e.g., if (!globalThis.React ||
typeof globalThis.React.createElement !== 'function')) so the shim runs when a
partial React global exists and prevents crashes in calls that rely on
React.createElement (referenced around the wrapper calls near lines where
createElement is used). Apply the same pattern to the other shim checks
mentioned (the blocks at the other two occurrences) to ensure required methods
exist before skipping initialization.

In `@crates/rari/src/server/middleware/request_context.rs`:
- Around line 159-171: The code unnecessarily clones tags when calling
Self::merge_and_sort_tags inside the guard branch; since this branch returns
immediately, move tags instead of cloning to avoid the allocation—replace the
tags.clone() argument with tags (ensuring ownership is moved) when calling
Self::merge_and_sort_tags for cloned_result.tags and keep the rest of the logic
(cache put, setting *guard, and returning cloned_result) unchanged.

In `@packages/rari/src/router/navigation-error-handler.ts`:
- Around line 136-139: The code assumes (error as any).status is numeric;
validate and coerce it before calling handleHttpError to avoid silent failures.
In navigation-error-handler.ts, where you extract status from error, create a
statusNumber: if typeof status === 'number' use it; else if typeof status ===
'string' and it matches a numeric pattern (e.g., /^\d+$/) parseInt to a number;
otherwise set statusNumber to undefined (or null) and pass that to
handleHttpError(error, statusNumber, url). This ensures handleHttpError always
receives a proper number or a clear empty value for non-numeric statuses.

---

Duplicate comments:
In `@packages/rari/src/runtime/shared/get-client-component.ts`:
- Around line 66-68: The current guard in triggerComponentLoad uses a truthy
check for componentInfo.component which will re-trigger loading for valid falsy
components; update the conditional in triggerComponentLoad to use a nullish
check (e.g., check componentInfo.component != null or componentInfo.component
!== null && componentInfo.component !== undefined) alongside the existing checks
for componentInfo.loader, componentInfo.loading, and componentInfo.loadPromise
so that already-loaded falsy components are treated as loaded and the function
returns Promise.resolve(null) only when truly not loaded.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2f93077c-5609-4cad-bd96-2c7a039ef110

📥 Commits

Reviewing files that changed from the base of the PR and between 915b642 and 9a512d5.

📒 Files selected for processing (5)
  • crates/rari/src/rsc/rendering/core/js/component_eval_setup.js
  • crates/rari/src/server/middleware/request_context.rs
  • packages/rari/src/router/navigation-error-handler.ts
  • packages/rari/src/runtime/shared/get-client-component.ts
  • test/unit/router/navigation-error-handler.test.ts
💤 Files with no reviewable changes (1)
  • test/unit/router/navigation-error-handler.test.ts

Comment thread crates/rari/src/rsc/rendering/core/js/component_eval_setup.js Outdated
Comment thread crates/rari/src/server/middleware/request_context.rs
Comment thread packages/rari/src/router/navigation-error-handler.ts
Comment thread packages/rari/src/proxy/runtime-executor.ts
Copy link
Copy Markdown

@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

♻️ Duplicate comments (1)
packages/rari/src/runtime/shared/get-client-component.ts (1)

114-123: ⚠️ Potential issue | 🟠 Major

Direct #export IDs still bypass export resolution in sync path.

On Line 119, resolveById() returns componentInfo.component directly. For direct IDs that include #namedExport, this can return the wrong value instead of the requested export.

Suggested fix
 function resolveById(id: string, clientComponents: Record<string, ComponentInfo>): any {
   const componentInfo = clientComponents[id] as LazyComponentInfo
   if (!componentInfo)
     return null

-  if (componentInfo.component != null)
-    return componentInfo.component
+  const hashIndex = id.indexOf('#')
+  const exportName = hashIndex === -1 ? '' : id.slice(hashIndex + 1)
+  const component = getComponentFromInfo(componentInfo, exportName)
+  if (component !== null && component !== undefined)
+    return component

   tryLoadComponent(componentInfo)
   return null
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/runtime/shared/get-client-component.ts` around lines 114 -
123, resolveById currently returns componentInfo.component directly which
bypasses resolving named exports for direct IDs that include a "#exportName";
update resolveById to detect if id contains a "#" (parse the export name), and
when componentInfo.component is already loaded return the requested export
(e.g., componentInfo.component[exportName]) instead of the whole module; if no
"#" return componentInfo.component as before; keep the existing lazy load
behavior (tryLoadComponent(componentInfo)) when component isn't loaded. Ensure
you reference resolveById, componentInfo.component, and tryLoadComponent in your
changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/rari/src/rsc/rendering/core/js/component_eval_setup.js`:
- Around line 25-28: The duplication between _jsx/_jsxs and
globalThis.jsx/globalThis.jsxs should be consolidated: add a single helper
(e.g., createJsxDelegate or getJsxFactory) that encapsulates the
delegation/search order and key-merging logic, then assign _jsx =
createJsxDelegate('jsx', globalThis['~react']?.jsxRuntime?.jsx, globalThis.jsx,
globalThis.React) and similarly _jsxs using the same helper; update the existing
_jsx/_jsxs initialization and the globalThis aliasing (also the similar block
around lines 31-50) to call this helper so both factories share identical
key-handling and fallback behavior.

In `@crates/rari/src/server/middleware/request_context.rs`:
- Around line 189-193: The code currently calls result.tags.clone() when merging
tags after perform_fetch, causing an unnecessary allocation; replace that clone
with std::mem::take(&mut result.tags) so you move out the existing Vec (leaving
an empty Vec in its place) and pass ownership into Self::merge_and_sort_tags,
then assign the merged Vec back to result.tags; update the block around
fetch_result handling (the
fetch_result/perform_fetch/result.tags/merge_and_sort_tags usage) to use
mem::take instead of clone.

In `@packages/rari/src/runtime/shared/get-client-component.ts`:
- Around line 196-202: The path-based resolution is incorrectly skipped by the
id.includes('#') guard, making resolveByPathWithExport()'s non-# path handling
unreachable; remove the id.includes('#') check and always call
resolveByPathWithExport(id, clientComponents, clientComponentPaths) first,
returning its non-null result, and only then fall back to resolveByName(id,
clientComponents, clientComponentNames) if path resolution returns null.

---

Duplicate comments:
In `@packages/rari/src/runtime/shared/get-client-component.ts`:
- Around line 114-123: resolveById currently returns componentInfo.component
directly which bypasses resolving named exports for direct IDs that include a
"#exportName"; update resolveById to detect if id contains a "#" (parse the
export name), and when componentInfo.component is already loaded return the
requested export (e.g., componentInfo.component[exportName]) instead of the
whole module; if no "#" return componentInfo.component as before; keep the
existing lazy load behavior (tryLoadComponent(componentInfo)) when component
isn't loaded. Ensure you reference resolveById, componentInfo.component, and
tryLoadComponent in your changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4f0bf55a-ea16-437f-8f7d-7cce9431a42a

📥 Commits

Reviewing files that changed from the base of the PR and between 915b642 and fe1bf3b.

📒 Files selected for processing (5)
  • crates/rari/src/rsc/rendering/core/js/component_eval_setup.js
  • crates/rari/src/server/middleware/request_context.rs
  • packages/rari/src/router/navigation-error-handler.ts
  • packages/rari/src/runtime/shared/get-client-component.ts
  • test/unit/router/navigation-error-handler.test.ts
💤 Files with no reviewable changes (1)
  • test/unit/router/navigation-error-handler.test.ts

Comment thread crates/rari/src/rsc/rendering/core/js/component_eval_setup.js Outdated
Comment thread crates/rari/src/server/middleware/request_context.rs
Comment thread packages/rari/src/runtime/shared/get-client-component.ts Outdated
Copy link
Copy Markdown

@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 (1)
packages/rari/src/runtime/shared/get-client-component.ts (1)

226-253: ⚠️ Potential issue | 🟠 Major

Async resolver still diverges from sync lookup semantics.

Line 226 uses clientComponents[id] directly, so id values like foo#Bar won’t hit a base-id registry key (foo).
Also, Line 233 gates path lookup on #, so non-hash path ids that sync resolution supports are skipped in async mode.

Proposed fix
 export async function getClientComponentAsync(id: string): Promise<any> {
   const clientComponents = (globalThis as unknown as GlobalWithRari)['~clientComponents'] || {}
   const clientComponentPaths = (globalThis as unknown as GlobalWithRari)['~clientComponentPaths'] || {}
   const clientComponentNames = (globalThis as unknown as GlobalWithRari)['~clientComponentNames'] || {}

-  let componentInfo = clientComponents[id] as LazyComponentInfo
-
-  if (componentInfo) {
-    const exportName = id.includes('#') ? id.slice(id.indexOf('#') + 1) : undefined
-    return await ensureComponentLoaded(componentInfo, exportName)
-  }
-
-  if (id.includes('#')) {
-    const hashIndex = id.indexOf('#')
-    const path = id.slice(0, hashIndex)
-    const exportName = id.slice(hashIndex + 1)
-    const variants = getPathVariants(path)
-
-    for (const variant of variants) {
-      const componentId = clientComponentPaths[variant]
-      if (componentId) {
-        componentInfo = clientComponents[componentId] as LazyComponentInfo
-        if (componentInfo)
-          return await ensureComponentLoaded(componentInfo, exportName)
-      }
-    }
-  }
+  const hashIndex = id.indexOf('#')
+  const baseId = hashIndex === -1 ? id : id.slice(0, hashIndex)
+  const exportName = hashIndex === -1 ? undefined : id.slice(hashIndex + 1)
+
+  let componentInfo = clientComponents[baseId] as LazyComponentInfo
+  if (componentInfo)
+    return await ensureComponentLoaded(componentInfo, exportName)
+
+  const variants = getPathVariants(baseId)
+  for (const variant of variants) {
+    const componentId = clientComponentPaths[variant]
+    if (componentId) {
+      componentInfo = clientComponents[componentId] as LazyComponentInfo
+      if (componentInfo)
+        return await ensureComponentLoaded(componentInfo, exportName)
+    }
+  }

   const componentId = clientComponentNames[id]
   if (componentId) {
     componentInfo = clientComponents[componentId] as LazyComponentInfo
     if (componentInfo)
-      return await ensureComponentLoaded(componentInfo)
+      return await ensureComponentLoaded(componentInfo, exportName)
   }

   return null
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/runtime/shared/get-client-component.ts` around lines 226 -
253, The async resolver in get-client-component.ts incorrectly only checks
clientComponents[id] and only runs path-variant lookup when id contains '#',
causing mismatches with sync lookup; update the logic in the block using
clientComponents, clientComponentPaths, clientComponentNames and
ensureComponentLoaded so that when id contains a '#', you also try the base id
(slice before '#') against clientComponents and clientComponentNames, and run
getPathVariants(path) and check clientComponentPaths for variants regardless of
whether id includes a '#', returning await ensureComponentLoaded(componentInfo,
exportName) when found; ensure exportName is derived correctly (part after '#')
and preserve existing return behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/rari/src/rsc/rendering/core/js/component_eval_setup.js`:
- Around line 25-37: The current createJsxDelegate(runtimeJsx, globalJsx) can
choose the runtime jsx/jsxs that normalize keys (turning 0/'' into null); update
createJsxDelegate to accept an additional reactCreateElement parameter and
prefer a wrapper around globalThis.React.createElement (if typeof
reactCreateElement === 'function') before falling back to runtimeJsx or
globalJsx so falsy keys like 0 or '' are preserved; ensure the wrapper uses the
existing key handling (key !== undefined ? { ...props, key } : props) and then
change the _jsx/_jsxs initializers to call
createJsxDelegate(globalThis['~react']?.jsxRuntime?.jsx, globalThis.jsx,
globalThis.React?.createElement).

---

Duplicate comments:
In `@packages/rari/src/runtime/shared/get-client-component.ts`:
- Around line 226-253: The async resolver in get-client-component.ts incorrectly
only checks clientComponents[id] and only runs path-variant lookup when id
contains '#', causing mismatches with sync lookup; update the logic in the block
using clientComponents, clientComponentPaths, clientComponentNames and
ensureComponentLoaded so that when id contains a '#', you also try the base id
(slice before '#') against clientComponents and clientComponentNames, and run
getPathVariants(path) and check clientComponentPaths for variants regardless of
whether id includes a '#', returning await ensureComponentLoaded(componentInfo,
exportName) when found; ensure exportName is derived correctly (part after '#')
and preserve existing return behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d6a8bce9-5585-4e4a-af38-c20704a1d654

📥 Commits

Reviewing files that changed from the base of the PR and between fe1bf3b and 52aa1fe.

📒 Files selected for processing (4)
  • crates/rari/src/rsc/rendering/core/js/component_eval_setup.js
  • crates/rari/src/server/middleware/request_context.rs
  • packages/rari/src/proxy/runtime-executor.ts
  • packages/rari/src/runtime/shared/get-client-component.ts

Comment thread crates/rari/src/rsc/rendering/core/js/component_eval_setup.js
Copy link
Copy Markdown

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/rari/src/rsc/rendering/core/js/component_eval_setup.js`:
- Around line 50-51: The current check assumes document.getElementById exists
and can throw in partial DOM runtimes; update the guard in the initialization
block to ensure document is defined and document.getElementById is callable
(e.g. typeof document.getElementById === 'function' or 'getElementById' in
document) before calling document.getElementById('spinner-keyframes'), and keep
the existing fallback use of document.head ||
document.getElementsByTagName('head')[0] (referencing document.getElementById,
document.head, getElementsByTagName('head')[0], and the 'spinner-keyframes' id).

In `@packages/rari/src/runtime/shared/get-client-component.ts`:
- Around line 244-249: The name-based fallback currently looks up
clientComponentNames[id] and calls ensureComponentLoaded(componentInfo) without
preserving named-export semantics, so ids like "Button#Icon" lose the
exportName; update the fallback to parse the id for an exportName (split on '#'
to get baseName and exportName), resolve clientComponentNames using the baseName
if the full id key is missing, then pass the exportName through to
ensureComponentLoaded (or set it on the LazyComponentInfo before calling) so
ensureComponentLoaded receives the named export to load; reference
clientComponentNames, clientComponents, LazyComponentInfo,
ensureComponentLoaded, id and exportName when making the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1a91aad1-05d8-4759-8330-8bb55ec0d47a

📥 Commits

Reviewing files that changed from the base of the PR and between 52aa1fe and d93a3d1.

📒 Files selected for processing (2)
  • crates/rari/src/rsc/rendering/core/js/component_eval_setup.js
  • packages/rari/src/runtime/shared/get-client-component.ts

Comment thread crates/rari/src/rsc/rendering/core/js/component_eval_setup.js Outdated
Comment thread packages/rari/src/runtime/shared/get-client-component.ts Outdated
Copy link
Copy Markdown

@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/rari/src/runtime/shared/get-client-component.ts (1)

73-93: 🧹 Nitpick | 🔵 Trivial

Consider extracting shared loading logic to reduce duplication.

loadClientComponent and executeLoader (lines 11-28) duplicate the component loading logic with different error handling strategies. While the divergent error handling (swallow vs rethrow) is intentional, the success path logic is identical.

♻️ Potential consolidation approach
function executeLoaderCore(
  componentInfo: LazyComponentInfo,
  onError: (error: Error) => void,
): Promise<any> {
  componentInfo.loading = true
  componentInfo.loadPromise = componentInfo.loader!()
    .then((module: any) => {
      const component = module.default || module
      componentInfo.component = component
      componentInfo.registered = true
      componentInfo.loading = false
      return component
    })
    .catch((error: Error) => {
      componentInfo.loading = false
      componentInfo.loadPromise = undefined
      onError(error)
    })
  return componentInfo.loadPromise
}

// Then executeLoader rethrows and loadClientComponent swallows
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rari/src/runtime/shared/get-client-component.ts` around lines 73 -
93, The success-path of component loading in loadClientComponent and
executeLoader is duplicated; extract that shared logic into a helper (e.g.,
executeLoaderCore) which accepts (componentInfo: LazyComponentInfo, onError:
(err: Error) => void) and performs componentInfo.loading = true, sets
componentInfo.loadPromise = componentInfo.loader!().then(module => { const c =
module.default || module; componentInfo.component = c; componentInfo.registered
= true; componentInfo.loading = false; return c }), and in .catch calls onError
after resetting componentInfo.loading and componentInfo.loadPromise; then
replace executeLoader to call executeLoaderCore with an onError that rethrows
the error, and replace loadClientComponent to call executeLoaderCore with an
onError that logs/swallow the error (maintain current behavior).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/rari/src/rsc/rendering/core/js/component_eval_setup.js`:
- Around line 55-60: The DOM-injection path uses getElementsByTagName,
createElement and appendChild without checking they exist, which can break in
partial-DOM environments; update the block around the head variable and spinner
injection (the code creating style with id 'spinner-keyframes' and assigning
textContent) to first verify document.getElementsByTagName,
document.createElement and head.appendChild are functions (and that head is
truthy), and bail out early if any are missing so you only call createElement,
set style.id/style.textContent, and appendChild when those helpers are present.

In `@packages/rari/src/runtime/shared/get-client-component.ts`:
- Around line 203-205: When the globalAccessor-backed maps (clientComponents,
clientComponentPaths, clientComponentNames) fall back to {} add a one-time debug
log to make this visible without changing behavior: detect the condition where
all three are empty after assignment and emit a single debug-level message (e.g.
via existing logger or console.debug) that includes which maps are missing and
context (e.g. file/function name). Implement the one-time suppression with a
module-level boolean sentinel (e.g. clientMapsWarned) so the log only appears
once and otherwise leave return values and logic unchanged; reference the
variables clientComponents, clientComponentPaths, clientComponentNames and
globalAccessor to locate where to add the check.

---

Outside diff comments:
In `@packages/rari/src/runtime/shared/get-client-component.ts`:
- Around line 73-93: The success-path of component loading in
loadClientComponent and executeLoader is duplicated; extract that shared logic
into a helper (e.g., executeLoaderCore) which accepts (componentInfo:
LazyComponentInfo, onError: (err: Error) => void) and performs
componentInfo.loading = true, sets componentInfo.loadPromise =
componentInfo.loader!().then(module => { const c = module.default || module;
componentInfo.component = c; componentInfo.registered = true;
componentInfo.loading = false; return c }), and in .catch calls onError after
resetting componentInfo.loading and componentInfo.loadPromise; then replace
executeLoader to call executeLoaderCore with an onError that rethrows the error,
and replace loadClientComponent to call executeLoaderCore with an onError that
logs/swallow the error (maintain current behavior).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2443a521-9e09-45b8-9568-fc44d2ac5ea2

📥 Commits

Reviewing files that changed from the base of the PR and between d93a3d1 and 1e519ff.

📒 Files selected for processing (2)
  • crates/rari/src/rsc/rendering/core/js/component_eval_setup.js
  • packages/rari/src/runtime/shared/get-client-component.ts

Comment thread crates/rari/src/rsc/rendering/core/js/component_eval_setup.js
Comment thread packages/rari/src/runtime/shared/get-client-component.ts
@skiniks skiniks merged commit 5aba3dd into main Mar 18, 2026
8 checks passed
@skiniks skiniks deleted the cold-gibbon branch March 18, 2026 20:34
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.

1 participant