Skip to content

Conversation

jamongsalguclub
Copy link
Contributor

@jamongsalguclub jamongsalguclub commented Sep 6, 2025

fixes #4585

Summary by CodeRabbit

  • New Features

    • Added an "Inline Scripts" page (injects two inline scripts) and header links to access it across React and Solid examples.
  • Bug Fixes

    • Prevent duplicate external and inline script tags across SSR and client-side navigations by deduplicating scripts and gating client injection to the appropriate lifecycle.
  • Tests

    • Added e2e tests verifying no script duplication (external and inline) across navigations and updated navigation tests to use exact matching for the "Scripts" link.

Copy link
Contributor

coderabbitai bot commented Sep 6, 2025

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

📥 Commits

Reviewing files that changed from the base of the PR and between 1480587 and 22b268f.

📒 Files selected for processing (2)
  • packages/react-router/src/Asset.tsx (4 hunks)
  • packages/solid-router/src/Asset.tsx (4 hunks)
 _______________________________________________
< Finding more bugs than your brain at 4:00 pm. >
 -----------------------------------------------
  \
   \   \
        \ /\
        ( )
      .( o ).

Walkthrough

Adds an /inline-scripts route and navigation to React Start and Solid Start e2e apps, generates route tree and typings for both frameworks, introduces Playwright tests validating no duplication for external and inline scripts across SSR and client navigations, and updates Asset.Script in both React and Solid routers to deduplicate scripts and gate client injection.

Changes

Cohort / File(s) Summary
Router asset duplication prevention
packages/react-router/src/Asset.tsx, packages/solid-router/src/Asset.tsx
Add environment detection (router.isServer/onMount/useEffect) to gate client injection; normalize external src to absolute URL and skip if already present; deduplicate inline scripts by matching textContent, type, and nonce; keep SSR rendering unchanged.
React Start: route registration & types
e2e/react-start/basic/src/routeTree.gen.ts
Register /inline-scripts across client/server route maps, module augmentations, root children, and FileRouteTypes unions.
React Start: new route + nav
e2e/react-start/basic/src/routes/__root.tsx, e2e/react-start/basic/src/routes/inline-scripts.tsx
Add "Inline Scripts" link to root nav; new route defines head() with two inline scripts and a component exposing a test-id heading.
React Start: tests
e2e/react-start/basic/tests/navigation.spec.ts, e2e/react-start/basic/tests/script-duplication.spec.ts
Tighten "Scripts" selector to exact match; add E2E tests asserting no duplication for external and inline scripts across SSR and client navigations and repeated cycles.
Solid Start: route registration & types
e2e/solid-start/basic/src/routeTree.gen.ts
Register /inline-scripts across Solid route maps, root children, and type augmentations.
Solid Start: new route + nav
e2e/solid-start/basic/src/routes/__root.tsx, e2e/solid-start/basic/src/routes/inline-scripts.tsx
Add "Inline Scripts" link to root nav; new Solid route defines head() with two inline scripts and a component exposing a test-id heading.
Solid Start: tests
e2e/solid-start/basic/tests/navigation.spec.ts, e2e/solid-start/basic/tests/script-duplication.spec.ts
Tighten "Scripts" selector to exact match; add E2E tests asserting no duplication for external and inline scripts across SSR and client navigations and repeated cycles.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant U as User
  participant App as App (SSR / Client)
  participant Head as HeadCollector
  participant Asset as Asset.Script
  participant DOM as Document

  rect rgb(245,245,255)
    note over App,DOM: Server-side render (SSR)
    U->>App: HTTP request
    App->>Head: collect head() scripts
    Head->>Asset: render Script (SSR)
    Asset->>DOM: emit <script> (src or inline)
  end

  rect rgb(245,255,245)
    note over App,DOM: Client hydration / navigation
    U->>App: hydrate or navigate
    App->>Head: render Scripts
    Head->>Asset: Script (client)
    Asset->>Asset: detect environment (router.isServer or onMount)
    Note right of Asset: client render returns null\nuse effect/onMount to inject
    Asset->>DOM: normalize src or compute inline signature
    alt No existing matching script found
      Asset->>DOM: create and append <script>
    else Duplicate found
      Asset-->>DOM: skip injection
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • schiller-manuel
  • Sheraff
  • SeanCassiere

Pre-merge checks (4 passed, 1 warning)

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title Check ✅ Passed The title succinctly and accurately highlights the core purpose of the changes—preventing duplicate script tag insertion during server-side rendering and client-side navigation—which directly reflects the primary modifications in Asset.tsx and associated tests.
Linked Issues Check ✅ Passed The pull request fulfills the primary objective from issue #4585 by adding deduplication logic in packages/react-router and packages/solid-router Asset.tsx components, and by introducing E2E test coverage for both external and inline scripts to verify that no duplicate script tags are injected during SSR or client navigation.
Out of Scope Changes Check ✅ Passed All added code—including route definitions, module augmentations, deduplication logic, and tests—directly supports the prevention and verification of script tag duplication, and no unrelated or extraneous changes are present.
Description Check ✅ Passed The description correctly references issue #4585 and is directly related to preventing script duplication as outlined in the linked issue, making it on-topic despite its brevity.

Poem

I hopped through heads and chased each tag,
Searched for echoes in the script-filled stack,
I nudged the mounts to only run once,
No double beats, no extra run—
The warren smiles, the pages clap. 🐇✨

Tip

You can disable the changed files summary in the walkthrough.

Disable the reviews.changed_files_summary setting in your project's settings in CodeRabbit to disable the changed files summary in the walkthrough.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

❤️ Share

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

Copy link

nx-cloud bot commented Sep 6, 2025

View your CI Pipeline Execution ↗ for commit 22b268f


☁️ Nx Cloud last updated this comment at 2025-09-10 02:58:44 UTC

Copy link

pkg-pr-new bot commented Sep 6, 2025

More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/arktype-adapter@5095

@tanstack/directive-functions-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/directive-functions-plugin@5095

@tanstack/eslint-plugin-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/eslint-plugin-router@5095

@tanstack/history

npm i https://pkg.pr.new/TanStack/router/@tanstack/history@5095

@tanstack/react-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router@5095

@tanstack/react-router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router-devtools@5095

@tanstack/react-router-ssr-query

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router-ssr-query@5095

@tanstack/react-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start@5095

@tanstack/react-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-client@5095

@tanstack/react-start-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-plugin@5095

@tanstack/react-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-server@5095

@tanstack/router-cli

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-cli@5095

@tanstack/router-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-core@5095

@tanstack/router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-devtools@5095

@tanstack/router-devtools-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-devtools-core@5095

@tanstack/router-generator

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-generator@5095

@tanstack/router-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-plugin@5095

@tanstack/router-ssr-query-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-ssr-query-core@5095

@tanstack/router-utils

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-utils@5095

@tanstack/router-vite-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-vite-plugin@5095

@tanstack/server-functions-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/server-functions-plugin@5095

@tanstack/solid-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-router@5095

@tanstack/solid-router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-router-devtools@5095

@tanstack/solid-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start@5095

@tanstack/solid-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-client@5095

@tanstack/solid-start-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-plugin@5095

@tanstack/solid-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-server@5095

@tanstack/start-client-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-client-core@5095

@tanstack/start-plugin-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-plugin-core@5095

@tanstack/start-server-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-server-core@5095

@tanstack/start-server-functions-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-server-functions-client@5095

@tanstack/start-server-functions-fetcher

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-server-functions-fetcher@5095

@tanstack/start-server-functions-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-server-functions-server@5095

@tanstack/start-storage-context

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-storage-context@5095

@tanstack/valibot-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/valibot-adapter@5095

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/TanStack/router/@tanstack/virtual-file-routes@5095

@tanstack/zod-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/zod-adapter@5095

commit: 1480587

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (12)
packages/react-router/src/Asset.tsx (2)

49-55: Make src-dedup robust (relative vs absolute URLs, CSS escaping).

Attribute-selector equality can miss matches (e.g., SSR absolute vs client relative) and can break if src contains characters needing CSS escaping. Compare normalized URLs from document.scripts instead.

-      const existingScript = document.querySelector(
-        `script[src="${attrs.src}"]`,
-      )
-      if (existingScript) {
+      const existingScript = Array.from(document.scripts).find((s) => {
+        const attr = s.getAttribute('src')
+        if (!attr) return false
+        try {
+          const a = new URL(attr, document.baseURI).href
+          const b = new URL(attrs.src!, document.baseURI).href
+          return a === b
+        } catch {
+          return attr === attrs.src
+        }
+      })
+      if (existingScript) {
         return
       }

81-87: Inline-script dedup: consider minor normalization and scoping to head.

textContent equality is fine but brittle to whitespace. Optional: trim or normalize and scope query to document.head to reduce scan cost.

-      const existingScript = Array.from(
-        document.querySelectorAll('script:not([src])'),
-      ).find((script) => script.textContent === children)
+      const needle = children.trim()
+      const existingScript = Array.from(
+        (document.head ?? document).querySelectorAll('script:not([src])'),
+      ).find((script) => (script.textContent ?? '').trim() === needle)
packages/solid-router/src/Asset.tsx (2)

41-47: Harden external-script dedup (normalize URL, avoid CSS selector pitfalls).

Using querySelector('script[src="..."]') can miss absolute/relative URL equivalence and needs escaping. Prefer normalized URL comparison.

-      const existingScript = document.querySelector(
-        `script[src="${attrs.src}"]`,
-      )
-      if (existingScript) {
+      const existingScript = Array.from(document.scripts).find((s) => {
+        const attr = s.getAttribute('src')
+        if (!attr) return false
+        try {
+          const a = new URL(attr, document.baseURI).href
+          const b = new URL(attrs.src!, document.baseURI).href
+          return a === b
+        } catch {
+          return attr === attrs.src
+        }
+      })
+      if (existingScript) {
         return
       }

67-73: Inline-script dedup: optional normalization and head scoping.

Trim to avoid whitespace mismatches and scope to head to reduce scan surface.

-      const existingScript = Array.from(
-        document.querySelectorAll('script:not([src])'),
-      ).find((script) => script.textContent === children)
+      const needle = children.trim()
+      const existingScript = Array.from(
+        (document.head ?? document).querySelectorAll('script:not([src])'),
+      ).find((script) => (script.textContent ?? '').trim() === needle)
e2e/react-start/basic/src/routes/inline-scripts.tsx (1)

9-12: Nit: explicit type is redundant for classic scripts.

type: 'text/javascript' is default; can be omitted to reduce noise (keep if tests assert presence).

-        type: 'text/javascript',
+        // type: 'text/javascript', // optional
e2e/solid-start/basic/tests/script-duplication.spec.ts (4)

10-17: Prefer locators and ends-with selector for robustness and auto-wait

Using page.locator with [src$="script.js"] is more resilient (absolute vs relative paths) and leverages Playwright’s auto-wait.

Apply:

-    // Count script tags with src="script.js"
-    const scriptCount = await page.evaluate(() => {
-      return document.querySelectorAll('script[src="script.js"]').length
-    })
-
-    // Should have exactly one script tag
-    expect(scriptCount).toBe(1)
+    // Should have exactly one script tag (robust selector and auto-wait)
+    await expect(page.locator('script[src$="script.js"]')).toHaveCount(1)

32-37: Reduce flakiness by replacing manual counts with locator assertions

These spots duplicate the same pattern; use toHaveCount for consistency and auto-wait.

Apply:

-    const firstNavCount = await page.evaluate(() => {
-      return document.querySelectorAll('script[src="script.js"]').length
-    })
-    expect(firstNavCount).toBe(1)
+    await expect(page.locator('script[src$="script.js"]')).toHaveCount(1)

-    const secondNavCount = await page.evaluate(() => {
-      return document.querySelectorAll('script[src="script.js"]').length
-    })
-    expect(secondNavCount).toBe(1)
+    await expect(page.locator('script[src$="script.js"]')).toHaveCount(1)

-    const finalCount = await page.evaluate(() => {
-      return document.querySelectorAll('script[src="script.js"]').length
-    })
-    expect(finalCount).toBe(1)
+    await expect(page.locator('script[src$="script.js"]')).toHaveCount(1)

Also applies to: 46-51, 77-81


95-113: Use locator filtering for inline scripts instead of string includes in page.evaluate

Locator.hasText is simpler and less brittle against whitespace/minification differences.

Apply:

-    // Count specific inline scripts
-    const script1Count = await page.evaluate(() => {
-      const scripts = Array.from(document.querySelectorAll('script:not([src])'))
-      return scripts.filter(
-        (script) =>
-          script.textContent &&
-          script.textContent.includes('window.INLINE_SCRIPT_1 = true'),
-      ).length
-    })
-
-    const script2Count = await page.evaluate(() => {
-      const scripts = Array.from(document.querySelectorAll('script:not([src])'))
-      return scripts.filter(
-        (script) =>
-          script.textContent &&
-          script.textContent.includes('window.INLINE_SCRIPT_2 = "test"'),
-      ).length
-    })
-
-    // Should have exactly one of each inline script
-    expect(script1Count).toBe(1)
-    expect(script2Count).toBe(1)
+    // Should have exactly one of each inline script
+    const inlineScripts = page.locator('script:not([src])')
+    await expect(
+      inlineScripts.filter({ hasText: 'window.INLINE_SCRIPT_1 = true' }),
+    ).toHaveCount(1)
+    await expect(
+      inlineScripts.filter({ hasText: 'window.INLINE_SCRIPT_2 = "test"' }),
+    ).toHaveCount(1)

129-145: Tighten selector to exact and use locators for inline script counts

  • Use exact: true for "Inline Scripts" to mirror the "Scripts" fix.
  • Use locator filters + toHaveCount for stable, auto-waiting assertions.

Apply:

-    await page.getByRole('link', { name: 'Inline Scripts' }).click()
+    await page.getByRole('link', { name: 'Inline Scripts', exact: true }).click()
@@
-    const firstNavScript1Count = await page.evaluate(() => {
-      const scripts = Array.from(document.querySelectorAll('script:not([src])'))
-      return scripts.filter(
-        (script) =>
-          script.textContent &&
-          script.textContent.includes('window.INLINE_SCRIPT_1 = true'),
-      ).length
-    })
-    expect(firstNavScript1Count).toBe(1)
+    await expect(
+      page
+        .locator('script:not([src])')
+        .filter({ hasText: 'window.INLINE_SCRIPT_1 = true' }),
+    ).toHaveCount(1)
@@
-    await page.getByRole('link', { name: 'Inline Scripts' }).click()
+    await page.getByRole('link', { name: 'Inline Scripts', exact: true }).click()
@@
-    const secondNavScript1Count = await page.evaluate(() => {
-      const scripts = Array.from(document.querySelectorAll('script:not([src])'))
-      return scripts.filter(
-        (script) =>
-          script.textContent &&
-          script.textContent.includes('window.INLINE_SCRIPT_1 = true'),
-      ).length
-    })
-    expect(secondNavScript1Count).toBe(1)
+    const inlineScripts = page.locator('script:not([src])')
+    await expect(
+      inlineScripts.filter({ hasText: 'window.INLINE_SCRIPT_1 = true' }),
+    ).toHaveCount(1)
+    // Optional: also assert script 2 stays single across navs
+    await expect(
+      inlineScripts.filter({ hasText: 'window.INLINE_SCRIPT_2 = "test"' }),
+    ).toHaveCount(1)

Also applies to: 150-165

e2e/solid-start/basic/src/routes/inline-scripts.tsx (1)

11-14: Normalize the script type to avoid false negatives in de-dup checks.

If the de-dup logic considers attributes, mixing an explicit type with an implicit default can cause mismatches. Prefer being consistent across both inline scripts.

Apply one of the following diffs (pick one):

Remove the explicit type:

       {
         children:
           'window.INLINE_SCRIPT_2 = "test"; console.log("Inline script 2 executed");',
-        type: 'text/javascript',
       },

Or add it to the first one, too:

       {
         children:
           'window.INLINE_SCRIPT_1 = true; console.log("Inline script 1 executed");',
+        type: 'text/javascript',
       },
e2e/react-start/basic/tests/script-duplication.spec.ts (2)

11-13: Make assertions more robust and less timing‑sensitive by using locators with built-in retries.

Direct page.evaluate counts are brittle (attribute equality and timing). Using locator queries with toHaveCount improves resiliency and avoids hard equality on src="script.js" vs /script.js.

Apply this diff to replace querySelectorAll/evaluate with locators and add exact matching for the Inline Scripts link:

-    const scriptCount = await page.evaluate(() => {
-      return document.querySelectorAll('script[src="script.js"]').length
-    })
-    expect(scriptCount).toBe(1)
+    const external = page.locator('script[src$="script.js"]')
+    await expect(external).toHaveCount(1)

-    const firstNavCount = await page.evaluate(() => {
-      return document.querySelectorAll('script[src="script.js"]').length
-    })
-    expect(firstNavCount).toBe(1)
+    const externalAfterFirstNav = page.locator('script[src$="script.js"]')
+    await expect(externalAfterFirstNav).toHaveCount(1)

-    const secondNavCount = await page.evaluate(() => {
-      return document.querySelectorAll('script[src="script.js"]').length
-    })
-    expect(secondNavCount).toBe(1)
+    const externalAfterSecondNav = page.locator('script[src$="script.js"]')
+    await expect(externalAfterSecondNav).toHaveCount(1)

-    const finalCount = await page.evaluate(() => {
-      return document.querySelectorAll('script[src="script.js"]').length
-    })
-    expect(finalCount).toBe(1)
+    const externalFinal = page.locator('script[src$="script.js"]')
+    await expect(externalFinal).toHaveCount(1)

-    const script1Count = await page.evaluate(() => {
-      const scripts = Array.from(document.querySelectorAll('script:not([src])'))
-      return scripts.filter(
-        (script) =>
-          script.textContent &&
-          script.textContent.includes('window.INLINE_SCRIPT_1 = true'),
-      ).length
-    })
-    const script2Count = await page.evaluate(() => {
-      const scripts = Array.from(document.querySelectorAll('script:not([src])'))
-      return scripts.filter(
-        (script) =>
-          script.textContent &&
-          script.textContent.includes('window.INLINE_SCRIPT_2 = "test"'),
-      ).length
-    })
-    expect(script1Count).toBe(1)
-    expect(script2Count).toBe(1)
+    const inline1 = page.locator('script:not([src])', {
+      hasText: 'window.INLINE_SCRIPT_1 = true',
+    })
+    const inline2 = page.locator('script:not([src])', {
+      hasText: 'window.INLINE_SCRIPT_2 = "test"',
+    })
+    await expect(inline1).toHaveCount(1)
+    await expect(inline2).toHaveCount(1)

-    await page.getByRole('link', { name: 'Inline Scripts' }).click()
+    await page.getByRole('link', { name: 'Inline Scripts', exact: true }).click()

-    const firstNavScript1Count = await page.evaluate(() => {
-      const scripts = Array.from(document.querySelectorAll('script:not([src])'))
-      return scripts.filter(
-        (script) =>
-          script.textContent &&
-          script.textContent.includes('window.INLINE_SCRIPT_1 = true'),
-      ).length
-    })
-    expect(firstNavScript1Count).toBe(1)
+    const inline1AfterFirstNav = page.locator('script:not([src])', {
+      hasText: 'window.INLINE_SCRIPT_1 = true',
+    })
+    await expect(inline1AfterFirstNav).toHaveCount(1)

-    const secondNavScript1Count = await page.evaluate(() => {
-      const scripts = Array.from(document.querySelectorAll('script:not([src])'))
-      return scripts.filter(
-        (script) =>
-          script.textContent &&
-          script.textContent.includes('window.INLINE_SCRIPT_1 = true'),
-      ).length
-    })
-    expect(secondNavScript1Count).toBe(1)
+    const inline1AfterSecondNav = page.locator('script:not([src])', {
+      hasText: 'window.INLINE_SCRIPT_1 = true',
+    })
+    await expect(inline1AfterSecondNav).toHaveCount(1)

Optionally also assert inline2 on client nav for completeness.

Also applies to: 32-36, 47-50, 78-81, 96-103, 105-112, 136-143, 156-163


56-85: Add a repeat-navigation check for inline scripts (parity with external).

External scripts are exercised through multiple cycles; inline scripts are only checked once. Add a small loop to navigate away/back a few times and assert counts stay at 1.

Happy to push a follow-up diff that mirrors the “multiple navigation cycles” test for inline scripts.

Also applies to: 123-169

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6202162 and 3d00753.

📒 Files selected for processing (12)
  • e2e/react-start/basic/src/routeTree.gen.ts (13 hunks)
  • e2e/react-start/basic/src/routes/__root.tsx (1 hunks)
  • e2e/react-start/basic/src/routes/inline-scripts.tsx (1 hunks)
  • e2e/react-start/basic/tests/navigation.spec.ts (1 hunks)
  • e2e/react-start/basic/tests/script-duplication.spec.ts (1 hunks)
  • e2e/solid-start/basic/src/routeTree.gen.ts (11 hunks)
  • e2e/solid-start/basic/src/routes/__root.tsx (1 hunks)
  • e2e/solid-start/basic/src/routes/inline-scripts.tsx (1 hunks)
  • e2e/solid-start/basic/tests/navigation.spec.ts (1 hunks)
  • e2e/solid-start/basic/tests/script-duplication.spec.ts (1 hunks)
  • packages/react-router/src/Asset.tsx (3 hunks)
  • packages/solid-router/src/Asset.tsx (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
e2e/react-start/basic/src/routes/inline-scripts.tsx (1)
e2e/solid-start/basic/src/routes/inline-scripts.tsx (1)
  • Route (3-18)
e2e/solid-start/basic/src/routes/inline-scripts.tsx (1)
e2e/react-start/basic/src/routes/inline-scripts.tsx (1)
  • Route (1-16)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Test
🔇 Additional comments (11)
packages/react-router/src/Asset.tsx (1)

118-121: Manually verify no hydration warnings: SSR renders the asset script but the client returns null, causing a DOM mismatch—ensure in React 18/19 (dev and prod) across your supported environments that no hydration warnings or dropped nodes occur.

packages/solid-router/src/Asset.tsx (1)

98-101: Client-side null vs SSR script: verify Solid hydration behavior.

Returning null on client while SSR emitted a script alters structure. Please verify Solid’s hydration doesn’t warn or discard the SSR node across dev/prod.

e2e/react-start/basic/src/routes/inline-scripts.tsx (1)

18-28: LGTM: route component for inline-script tests.

Clear test ids and copy. Matches the e2e intent.

e2e/solid-start/basic/tests/navigation.spec.ts (1)

37-37: Good fix to disambiguate the "Scripts" link

Using exact: true prevents accidental matches with "Inline Scripts" and reduces flakiness after adding the new nav item.

e2e/solid-start/basic/src/routes/__root.tsx (1)

106-113: Nav entry addition looks good

Consistent styling and placement with neighboring links. No concerns.

e2e/react-start/basic/tests/navigation.spec.ts (1)

37-37: Solid selector update

Matching the "Scripts" link with exact: true avoids ambiguity with "Inline Scripts". LGTM.

e2e/react-start/basic/src/routes/__root.tsx (1)

144-151: Inline Scripts link addition LGTM

Matches existing pattern and active styling. No issues spotted.

e2e/solid-start/basic/src/routes/inline-scripts.tsx (2)

3-18: Route definition adds clear inline-script coverage — looks good.

Mirrors the React test route and is appropriate for exercising inline-script de-duplication.


20-30: UI scaffold is fine for e2e anchoring.

Solid’s class usage and data-testid are correct and minimal.

e2e/solid-start/basic/src/routeTree.gen.ts (1)

19-19: Generated route tree updates for /inline-scripts look consistent.

Import added, route updated, and all relevant maps/unions include the new path. No manual edits needed.

Ensure the route tree was regenerated from the same commit that adds routes/inline-scripts.tsx to avoid drift after merges.

Also applies to: 78-82, 243-247, 275-279, 308-312, 345-346, 446-447, 866-867

e2e/react-start/basic/src/routeTree.gen.ts (1)

26-26: React route tree and module augmentations for /inline-scripts are coherent.

The route import/update, type unions, module declarations, and root children wiring are all in place.

Same note: confirm these artifacts are regenerated post-merge to keep them in sync with any route file moves/renames.

Also applies to: 94-98, 273-276, 306-309, 343-346, 384-385, 417-420, 453-454, 494-498, 568-574, 852-858, 1178-1194, 1905-1906

Comment on lines +1 to +16
export const Route = createFileRoute({
head: () => ({
scripts: [
{
children:
'window.INLINE_SCRIPT_1 = true; console.log("Inline script 1 executed");',
},
{
children:
'window.INLINE_SCRIPT_2 = "test"; console.log("Inline script 2 executed");',
type: 'text/javascript',
},
],
}),
component: InlineScriptsComponent,
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix createFileRoute usage: missing path argument.

React Start routes typically call createFileRoute('/inline-scripts'). The current call with an object will fail type-checking or at runtime.

-export const Route = createFileRoute({
+export const Route = createFileRoute('/inline-scripts')({
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const Route = createFileRoute({
head: () => ({
scripts: [
{
children:
'window.INLINE_SCRIPT_1 = true; console.log("Inline script 1 executed");',
},
{
children:
'window.INLINE_SCRIPT_2 = "test"; console.log("Inline script 2 executed");',
type: 'text/javascript',
},
],
}),
component: InlineScriptsComponent,
})
export const Route = createFileRoute('/inline-scripts')({
head: () => ({
scripts: [
{
children:
'window.INLINE_SCRIPT_1 = true; console.log("Inline script 1 executed");',
},
{
children:
'window.INLINE_SCRIPT_2 = "test"; console.log("Inline script 2 executed");',
type: 'text/javascript',
},
],
}),
component: InlineScriptsComponent,
})
🤖 Prompt for AI Agents
In e2e/react-start/basic/src/routes/inline-scripts.tsx around lines 1 to 16, the
createFileRoute call is missing the required path argument; change the
invocation to pass the route path as the first parameter (e.g.
'/inline-scripts') and pass the existing config object as the second parameter
so the function signature matches createFileRoute(path, config) and
TypeScript/runtime checks succeed.

Copy link
Contributor

@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/react-router/src/Asset.tsx (1)

119-119: Narrow effect deps to avoid teardown/re-execution of scripts.

Depending on attrs object identity triggers unnecessary remove/re-add and re-exec of inline code.

-  }, [attrs, children])
+  }, [
+    attrs?.src,
+    attrs?.type,
+    attrs?.crossOrigin,
+    attrs?.integrity,
+    attrs?.referrerPolicy,
+    (attrs as Record<string, unknown>)?.['nonce'],
+    children,
+  ])
🧹 Nitpick comments (1)
packages/react-router/src/Asset.tsx (1)

5-9: Make ScriptAttrs explicit for commonly used fields.

Improves type-safety for deps and dedupe keys.

 interface ScriptAttrs {
   [key: string]: string | boolean | undefined
   src?: string
   suppressHydrationWarning?: boolean
+  type?: string
+  nonce?: string
+  crossOrigin?: string
+  integrity?: string
+  referrerPolicy?: string
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3d00753 and b9d38a1.

📒 Files selected for processing (2)
  • packages/react-router/src/Asset.tsx (4 hunks)
  • packages/solid-router/src/Asset.tsx (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/solid-router/src/Asset.tsx
🧰 Additional context used
🪛 ESLint
packages/react-router/src/Asset.tsx

[error] 3-3: ./useRouter import should occur before type import of @tanstack/router-core

(import/order)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Preview
  • GitHub Check: Test
🔇 Additional comments (1)
packages/react-router/src/Asset.tsx (1)

121-124: Server-only render + client injection pattern looks good.

Prevents hydration dupes while preserving SSR output.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (7)
e2e/react-start/basic/tests/script-duplication.spec.ts (7)

9-13: Prefer locator-based counts with auto-retry to reduce flakiness.

Use Playwright locators + toHaveCount for built-in waiting instead of page.evaluate + manual counts.

Apply:

-    const scriptCount = await page.evaluate(() => {
-      return document.querySelectorAll('script[src="script.js"]').length
-    })
-
-    expect(scriptCount).toBe(1)
+    const externalScripts = page.locator('script[src*="script.js"]')
+    await expect(externalScripts).toHaveCount(1)
@@
-    const firstNavCount = await page.evaluate(() => {
-      return document.querySelectorAll('script[src="script.js"]').length
-    })
-    expect(firstNavCount).toBe(1)
+    const externalScripts1 = page.locator('script[src*="script.js"]')
+    await expect(externalScripts1).toHaveCount(1)
@@
-    const secondNavCount = await page.evaluate(() => {
-      return document.querySelectorAll('script[src="script.js"]').length
-    })
-    expect(secondNavCount).toBe(1)
+    const externalScripts2 = page.locator('script[src*="script.js"]')
+    await expect(externalScripts2).toHaveCount(1)
@@
-    const finalCount = await page.evaluate(() => {
-      return document.querySelectorAll('script[src="script.js"]').length
-    })
-    expect(finalCount).toBe(1)
+    const externalScriptsFinal = page.locator('script[src*="script.js"]')
+    await expect(externalScriptsFinal).toHaveCount(1)

Also applies to: 26-30, 37-41, 61-65


76-83: Inline script checks: use locator filtering with regex for resilience.

Filtering text via JS can be brittle. Use locator.filter({ hasText: /.../ }) with whitespace-tolerant regex.

Apply:

-    const script1Count = await page.evaluate(() => {
-      const scripts = Array.from(document.querySelectorAll('script:not([src])'))
-      return scripts.filter(
-        (script) =>
-          script.textContent &&
-          script.textContent.includes('window.INLINE_SCRIPT_1 = true'),
-      ).length
-    })
+    const inline1 = page
+      .locator('script:not([src])')
+      .filter({ hasText: /window\.INLINE_SCRIPT_1\s*=\s*true/ })
+    await expect(inline1).toHaveCount(1)
@@
-    const script2Count = await page.evaluate(() => {
-      const scripts = Array.from(document.querySelectorAll('script:not([src])'))
-      return scripts.filter(
-        (script) =>
-          script.textContent &&
-          script.textContent.includes('window.INLINE_SCRIPT_2 = "test"'),
-      ).length
-    })
+    const inline2 = page
+      .locator('script:not([src])')
+      .filter({ hasText: /window\.INLINE_SCRIPT_2\s*=\s*["']test["']/ })
+    await expect(inline2).toHaveCount(1)
@@
-    const firstNavScript1Count = await page.evaluate(() => {
-      const scripts = Array.from(document.querySelectorAll('script:not([src])'))
-      return scripts.filter(
-        (script) =>
-          script.textContent &&
-          script.textContent.includes('window.INLINE_SCRIPT_1 = true'),
-      ).length
-    })
-    expect(firstNavScript1Count).toBe(1)
+    const inline1First = page
+      .locator('script:not([src])')
+      .filter({ hasText: /window\.INLINE_SCRIPT_1\s*=\s*true/ })
+    await expect(inline1First).toHaveCount(1)
@@
-    const secondNavScript1Count = await page.evaluate(() => {
-      const scripts = Array.from(document.querySelectorAll('script:not([src])'))
-      return scripts.filter(
-        (script) =>
-          script.textContent &&
-          script.textContent.includes('window.INLINE_SCRIPT_1 = true'),
-      ).length
-    })
-    expect(secondNavScript1Count).toBe(1)
+    const inline1Second = page
+      .locator('script:not([src])')
+      .filter({ hasText: /window\.INLINE_SCRIPT_1\s*=\s*true/ })
+    await expect(inline1Second).toHaveCount(1)

Also applies to: 85-92, 111-118, 129-136


7-7: Use toBeVisible instead of toBeInViewport for stability.

Visibility is sufficient and less flaky across CI viewport differences.

Apply:

-    await expect(page.getByTestId('scripts-test-heading')).toBeInViewport()
+    await expect(page.getByTestId('scripts-test-heading')).toBeVisible()
@@
-    await expect(page.getByTestId('scripts-test-heading')).toBeInViewport()
+    await expect(page.getByTestId('scripts-test-heading')).toBeVisible()
@@
-    await expect(page.getByTestId('scripts-test-heading')).toBeInViewport()
+    await expect(page.getByTestId('scripts-test-heading')).toBeVisible()
@@
-      await expect(page.getByTestId('scripts-test-heading')).toBeInViewport()
+      await expect(page.getByTestId('scripts-test-heading')).toBeVisible()
@@
-    await expect(page.getByTestId('scripts-test-heading')).toBeInViewport()
+    await expect(page.getByTestId('scripts-test-heading')).toBeVisible()
@@
-    await expect(
-      page.getByTestId('inline-scripts-test-heading'),
-    ).toBeInViewport()
+    await expect(page.getByTestId('inline-scripts-test-heading')).toBeVisible()
@@
-    await expect(
-      page.getByTestId('inline-scripts-test-heading'),
-    ).toBeInViewport()
+    await expect(page.getByTestId('inline-scripts-test-heading')).toBeVisible()
@@
-    await expect(
-      page.getByTestId('inline-scripts-test-heading'),
-    ).toBeInViewport()
+    await expect(page.getByTestId('inline-scripts-test-heading')).toBeVisible()

Also applies to: 24-24, 35-35, 53-53, 59-59, 72-75, 106-110, 124-128


15-16: Await script execution deterministically.

Directly reading window props can race with async injection/loading. Wait for the condition.

Apply:

-    expect(await page.evaluate('window.SCRIPT_1')).toBe(true)
+    await page.waitForFunction(() => (window as any).SCRIPT_1 === true)
@@
-    expect(await page.evaluate('window.SCRIPT_1')).toBe(true)
+    await page.waitForFunction(() => (window as any).SCRIPT_1 === true)
@@
-    expect(await page.evaluate('window.SCRIPT_1')).toBe(true)
+    await page.waitForFunction(() => (window as any).SCRIPT_1 === true)
@@
-    expect(await page.evaluate('window.INLINE_SCRIPT_1')).toBe(true)
-    expect(await page.evaluate('window.INLINE_SCRIPT_2')).toBe('test')
+    await page.waitForFunction(() => (window as any).INLINE_SCRIPT_1 === true)
+    await page.waitForFunction(() => (window as any).INLINE_SCRIPT_2 === 'test')
@@
-    expect(await page.evaluate('window.INLINE_SCRIPT_1')).toBe(true)
-    expect(await page.evaluate('window.INLINE_SCRIPT_2')).toBe('test')
+    await page.waitForFunction(() => (window as any).INLINE_SCRIPT_1 === true)
+    await page.waitForFunction(() => (window as any).INLINE_SCRIPT_2 === 'test')

Also applies to: 42-42, 66-66, 97-99, 140-142


106-106: Use exact: true for “Inline Scripts” for selector consistency.

Avoid accidental partial matches and align with “Scripts” links.

Apply:

-    await page.getByRole('link', { name: 'Inline Scripts' }).click()
+    await page.getByRole('link', { name: 'Inline Scripts', exact: true }).click()
@@
-    await page.getByRole('link', { name: 'Inline Scripts' }).click()
+    await page.getByRole('link', { name: 'Inline Scripts', exact: true }).click()

Also applies to: 124-124


143-143: Optional: add history back/forward scenario.

Duplicates sometimes surface on bfcache/history restores. Consider adding:

test('no duplicates on back/forward restores', async ({ page }) => {
  await page.goto('/')
  await page.getByRole('link', { name: 'Scripts', exact: true }).click()
  await expect(page.getByTestId('scripts-test-heading')).toBeVisible()

  await page.goBack()
  await expect(page.getByRole('link', { name: 'Posts' })).toBeVisible()

  await page.goForward()
  await expect(page.getByTestId('scripts-test-heading')).toBeVisible()
  await expect(page.locator('script[src*="script.js"]')).toHaveCount(1)
})

10-10: Verify script selector robustness

Injected scripts use a strict src="script.js" match and have no stable identifiers. To avoid breakage when asset names include hashes or query params, either:

  • Add a fixed id or data-testid to the <script> in head()
  • Or update the test selector to something like querySelectorAll('script[src*="script.js"]')
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c72b7e8 and cd2bffb.

📒 Files selected for processing (2)
  • e2e/react-start/basic/tests/script-duplication.spec.ts (1 hunks)
  • e2e/solid-start/basic/tests/script-duplication.spec.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • e2e/solid-start/basic/tests/script-duplication.spec.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Preview
  • GitHub Check: Test
🔇 Additional comments (1)
e2e/react-start/basic/tests/script-duplication.spec.ts (1)

3-3: Good coverage and intent.

Solid end-to-end coverage for SSR, CSR navigations, and inline/external scripts. Nice!

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
packages/react-router/src/Asset.tsx (1)

139-139: Effect deps will cause teardown/re-exec; narrow to stable keys.
Depending on attrs object identity will remove/recreate scripts on benign re-renders, re-executing inline code and reloading external scripts.

-  }, [attrs, children])
+  }, [attrs?.src, attrs?.type, attrs?.nonce, children])

If you also want re-runs on other attrs (async, defer, crossOrigin, integrity, referrerPolicy), include them explicitly.

🧹 Nitpick comments (2)
packages/react-router/src/Asset.tsx (2)

52-66: Normalize against document.baseURI to honor tags.
Use document.baseURI as the base when resolving relative src so dedupe works with <base href> present.

-          return new URL(attrs.src, window.location.href).href
+          return new URL(attrs.src, document.baseURI).href

91-106: Harden inline-script matching with type guards for type/nonce.
attrs’ index signature allows booleans; ensure we only compare string values to avoid false mismatches.

-      const typeAttr = attrs?.type ?? 'text/javascript'
-      const nonceAttr = attrs?.nonce
+      const typeAttr =
+        typeof attrs?.type === 'string' ? attrs.type : 'text/javascript'
+      const nonceAttr =
+        typeof attrs?.nonce === 'string' ? attrs.nonce : undefined
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cd2bffb and e424427.

📒 Files selected for processing (2)
  • packages/react-router/src/Asset.tsx (4 hunks)
  • packages/solid-router/src/Asset.tsx (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/solid-router/src/Asset.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
packages/react-router/src/Asset.tsx (1)
packages/react-router/src/index.tsx (1)
  • useRouter (308-308)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Test
  • GitHub Check: Preview
🔇 Additional comments (3)
packages/react-router/src/Asset.tsx (3)

2-2: Import order fix LGTM.
Value import before type-only import satisfies lint/CI.


48-49: Client/server gating hook usage looks correct.
Using useRouter() to branch SSR vs client behavior is appropriate here.


141-144: Verify hydration doesn’t double-execute scripts when returning null on client.
Returning null on the client while SSR rendered a <script> can lead React to remove the SSR node before the effect runs, causing the effect to reinsert and re-execute. Please confirm with an SSR page whose inline script increments a counter that it runs exactly once on first load. If it double-executes, consider preserving the SSR node during the first client render (e.g., gate return null behind a “post-hydration” ref) or manage a registry to avoid reinsert on first mount.

Copy link
Contributor

@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 (2)
packages/solid-router/src/Asset.tsx (2)

42-123: Make external-vs-inline injection mutually exclusive

If both attrs.src and children are provided, the current logic can inject twice (external, then inline), defeating dedupe. Short-circuit after handling the external path; only run the inline path when no src.

   onMount(() => {
-    if (attrs?.src) {
+    const hasSrc = typeof attrs?.src === 'string' && attrs.src.length > 0
+    if (hasSrc) {
       const normSrc = (() => {
         try {
-          return new URL(attrs.src, window.location.href).href
+          const base = document.baseURI || window.location.href
+          return new URL(attrs!.src!, base).href
         } catch {
-          return attrs.src
+          return attrs!.src!
         }
       })()
       const existingScript = Array.from(
         document.querySelectorAll('script[src]'),
       ).find((el) => (el as HTMLScriptElement).src === normSrc)
 
       if (existingScript) {
-        return
+        return
       }
 
       const script = document.createElement('script')
 
       for (const [key, value] of Object.entries(attrs)) {
         if (value !== undefined && value !== false) {
           script.setAttribute(
             key,
             typeof value === 'boolean' ? '' : String(value),
           )
         }
       }
 
       document.head.appendChild(script)
 
       onCleanup(() => {
         if (script.parentNode) {
           script.parentNode.removeChild(script)
         }
       })
-    }
+      return
+    }
 
-    if (typeof children === 'string') {
+    if (!hasSrc && typeof children === 'string') {
       const typeAttr =
         typeof attrs?.type === 'string' ? attrs.type : 'text/javascript'
       const nonceAttr =
         typeof attrs?.nonce === 'string' ? attrs.nonce : undefined
       const existingScript = Array.from(
         document.querySelectorAll('script:not([src])'),
       ).find((el) => {
         if (!(el instanceof HTMLScriptElement)) return false
         const sType = el.getAttribute('type') ?? 'text/javascript'
         const sNonce = el.getAttribute('nonce') ?? undefined
         return (
           el.textContent === children &&
           sType === typeAttr &&
           sNonce === nonceAttr
         )
       })
 
       if (existingScript) {
         return
       }
 
       const script = document.createElement('script')
       script.textContent = children
 
       if (attrs) {
         for (const [key, value] of Object.entries(attrs)) {
+          if (key === 'src') continue
           if (value !== undefined && value !== false) {
             script.setAttribute(
               key,
               typeof value === 'boolean' ? '' : String(value),
             )
           }
         }
       }
 
       document.head.appendChild(script)
 
       onCleanup(() => {
         if (script.parentNode) {
           script.parentNode.removeChild(script)
         }
       })
     }
   })

101-113: Inline path should not propagate src attribute

When creating an inline script, copying attrs.src turns it into an external script, defeating the not([src]) dedupe and risking duplication. Skip src here (even if upstream passes it).

-      if (attrs) {
-        for (const [key, value] of Object.entries(attrs)) {
+      if (attrs) {
+        for (const [key, value] of Object.entries(attrs)) {
+          if (key === 'src') continue
           if (value !== undefined && value !== false) {
             script.setAttribute(
               key,
               typeof value === 'boolean' ? '' : String(value),
             )
           }
         }
       }
🧹 Nitpick comments (2)
packages/solid-router/src/Asset.tsx (2)

125-127: Client render guard: prefer compile-time isServer and confirm no hydration warnings

Using router.isServer couples this component to router context and may mask hydration mismatches if Asset ever renders inside a hydratable tree. Consider using isServer from solid-js/web (tree-shakable) and please confirm no hydration mismatch warnings in dev.

If you want, I can add a quick check to run the e2e/dev app and grep console output for hydration warnings.


61-68: Set nonce via property before insertion (CSP best practice)

For CSP, browsers honor the nonce property; setting it pre-append is safest. This is additive and backward-compatible.

-      for (const [key, value] of Object.entries(attrs)) {
+      for (const [key, value] of Object.entries(attrs)) {
         if (value !== undefined && value !== false) {
-          script.setAttribute(
+          if (key === 'nonce' && typeof value === 'string') {
+            // ensure CSP nonce is applied via property
+            (script as any).nonce = value
+          }
+          script.setAttribute(
             key,
             typeof value === 'boolean' ? '' : String(value),
           )
         }
       }

(Apply the same change in the inline branch.)

Also applies to: 105-112

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e424427 and 1480587.

📒 Files selected for processing (2)
  • packages/react-router/src/Asset.tsx (4 hunks)
  • packages/solid-router/src/Asset.tsx (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/react-router/src/Asset.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Preview
  • GitHub Check: Test
🔇 Additional comments (1)
packages/solid-router/src/Asset.tsx (1)

51-57: LGTM on external dedupe approach

Absolute URL normalization + src equality check is a sound, low-cost dedupe.

Comment on lines 44 to 54
const normSrc = (() => {
try {
return new URL(attrs.src, window.location.href).href
} catch {
return attrs.src
}
})()
const existingScript = Array.from(
document.querySelectorAll('script[src]'),
).find((el) => (el as HTMLScriptElement).src === normSrc)

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Normalize src against document.baseURI to handle correctly

Using window.location.href ignores a document , which can break dedupe. Prefer document.baseURI (with a location fallback).

-      const normSrc = (() => {
-        try {
-          return new URL(attrs.src, window.location.href).href
-        } catch {
-          return attrs.src
-        }
-      })()
+      const normSrc = (() => {
+        try {
+          const base = document.baseURI || window.location.href
+          return new URL(attrs.src, base).href
+        } catch {
+          return attrs.src
+        }
+      })()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const normSrc = (() => {
try {
return new URL(attrs.src, window.location.href).href
} catch {
return attrs.src
}
})()
const existingScript = Array.from(
document.querySelectorAll('script[src]'),
).find((el) => (el as HTMLScriptElement).src === normSrc)
const normSrc = (() => {
try {
const base = document.baseURI || window.location.href
return new URL(attrs.src, base).href
} catch {
return attrs.src
}
})()
const existingScript = Array.from(
document.querySelectorAll('script[src]'),
).find((el) => (el as HTMLScriptElement).src === normSrc)
🤖 Prompt for AI Agents
In packages/solid-router/src/Asset.tsx around lines 44 to 54, the URL
normalization uses window.location.href which ignores a document <base> element
and can break script deduping; change the base used to document.baseURI with a
fallback to window.location.href (e.g. construct the URL with new URL(attrs.src,
document.baseURI || window.location.href).href inside the existing try/catch) so
normalization respects <base href> while preserving current fallback behavior.

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

Successfully merging this pull request may close these issues.

Using createRootRoute, Scripts, and HeadContent components adds same script multiple times
1 participant