Skip to content

chore: rename client dir to devtools#668

Merged
harlan-zw merged 4 commits intomainfrom
chore/rename-client-to-devtools
Mar 24, 2026
Merged

chore: rename client dir to devtools#668
harlan-zw merged 4 commits intomainfrom
chore/rename-client-to-devtools

Conversation

@harlan-zw
Copy link
Collaborator

❓ Type of change

  • 🧹 Chore

📚 Description

Renames the client/ directory to devtools/ for clarity, since it contains the DevTools UI panel code. Updates all references: pnpm workspace, npm scripts (client:builddevtools:build, client:devdevtools:dev), tsconfig exclude, module resolve path, and nitro output directory.

Also includes devtools UI enhancements: network request tracking via Resource Timing API, new pages (first-party, registry, docs), and a vitest config fix to exclude .claude/worktrees/ from the unit test project.

Rename `client/` to `devtools/` for clarity. Update workspace config,
scripts, tsconfig exclude, and module resolve path to match.

Also adds network request tracking via Resource Timing API for devtools,
new devtools pages (first-party, registry, docs), and excludes
`.claude/worktrees/` from vitest unit project.
@vercel
Copy link
Contributor

vercel bot commented Mar 24, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
scripts-playground Ready Ready Preview, Comment Mar 24, 2026 0:37am

@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 24, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@nuxt/scripts@668

commit: 9b0ada8

@coderabbitai
Copy link

coderabbitai bot commented Mar 24, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

The PR deletes the legacy client/ devtools UI and introduces a new devtools/ application with its own Nuxt config, pages (index, first-party, registry, docs), components (including NetworkWaterfall, ScriptStatus, ScriptSize, ScriptLoadTime), and composables (state, rpc). It moves build/dev packaging from client to devtools, updates workspace/package references, and adds runtime changes: network request observation, new devtools types/fields for network requests and domains, and helpers to populate and sync script/state data.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 24.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'chore: rename client dir to devtools' clearly summarizes the main change—directory renaming from client to devtools for improved clarity.
Description check ✅ Passed The description is directly related to the changeset, explaining the directory rename rationale, updated references, and additional DevTools UI enhancements included in the PR.

✏️ 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 chore/rename-client-to-devtools

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

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

Caution

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

⚠️ Outside diff range comments (1)
devtools/utils/formatting.ts (1)

13-17: ⚠️ Potential issue | 🔴 Critical

Logic error: urlToOrigin will throw for relative URLs.

The condition is inverted. When isRelative(url) returns true, the code attempts new URL(url) which throws a TypeError because relative URLs require a base URL. The logic should extract origin only for absolute URLs.

🐛 Proposed fix
 export function urlToOrigin(url: string) {
-  if (isRelative(url, { acceptRelative: true }))
-    return new URL(url).origin
-  return url
+  if (!isRelative(url, { acceptRelative: true }))
+    return new URL(url).origin
+  return url
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@devtools/utils/formatting.ts` around lines 13 - 17, The function urlToOrigin
has its condition inverted causing new URL(url) to be called for relative URLs
and throw; in the export function urlToOrigin change the check so you only call
new URL(url).origin for absolute URLs (i.e. if NOT isRelative(url, {
acceptRelative: true })) and otherwise return the original input for relative
URLs; update the conditional in urlToOrigin accordingly to avoid constructing a
URL for relative values.
🧹 Nitpick comments (12)
vitest.config.ts (1)

52-68: Consider adding the same exclusion to the nuxt-runtime project for consistency.

The nuxt-runtime project includes a broad glob pattern ./**/*.nuxt.test.ts (line 58) that would also match files in .claude/worktrees/. For consistency with the unit project and to prevent potential issues if .nuxt.test.ts files are created in that directory, consider adding the same exclusion here.

♻️ Proposed fix for consistency
          exclude: [
            // exclude other tests
            './test/e2e/**/*.test.ts',
            './test/e2e-dev/**/*.test.ts',
            './test/unit/**/*.test.ts',
            '**/node_modules/**',
+           '.claude/worktrees/**',
          ],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vitest.config.ts` around lines 52 - 68, The nuxt-runtime Vitest project
(defined via defineVitestProject with test.name 'nuxt-runtime') currently uses a
broad include './**/*.nuxt.test.ts' which can match files under
.claude/worktrees; update the test.exclude array for this project to add the
same exclusion used elsewhere (e.g. the .claude/worktrees pattern) so files in
.claude/worktrees are ignored by the nuxt-runtime project.
devtools/pages/first-party.vue (2)

6-15: Object literals should have line breaks between items (optional).

ESLint flags these object literals for missing line breaks. While functional, multi-line formatting improves readability.

♻️ Suggested formatting
 const privacyFlagLabels: Record<string, string> = {
-  ip: 'IP Address', userAgent: 'User Agent', language: 'Language', screen: 'Screen', timezone: 'Timezone', hardware: 'Hardware',
+  ip: 'IP Address',
+  userAgent: 'User Agent',
+  language: 'Language',
+  screen: 'Screen',
+  timezone: 'Timezone',
+  hardware: 'Hardware',
 }
 const privacyFlagShort: Record<string, string> = {
-  ip: 'IP', userAgent: 'UA', language: 'Lang', screen: 'Screen', timezone: 'TZ', hardware: 'HW',
+  ip: 'IP',
+  userAgent: 'UA',
+  language: 'Lang',
+  screen: 'Screen',
+  timezone: 'TZ',
+  hardware: 'HW',
 }
 const privacyFlagIcons: Record<string, string> = {
-  ip: 'i-carbon-location', userAgent: 'i-carbon-user-profile', language: 'i-carbon-translate',
-  screen: 'i-carbon-screen', timezone: 'i-carbon-time', hardware: 'i-carbon-chip',
+  ip: 'i-carbon-location',
+  userAgent: 'i-carbon-user-profile',
+  language: 'i-carbon-translate',
+  screen: 'i-carbon-screen',
+  timezone: 'i-carbon-time',
+  hardware: 'i-carbon-chip',
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@devtools/pages/first-party.vue` around lines 6 - 15, The three object
literals privacyFlagLabels, privacyFlagShort, and privacyFlagIcons are written
as single-line multi-entry objects which ESLint flags; reformat each object so
every key/value pair is on its own line (one property per line) with trailing
commas and consistent indentation to satisfy the rule and improve readability.

22-34: Single-line if statements should have newlines (optional).

ESLint's antfu/if-newline rule expects newlines after if statements.

♻️ Suggested formatting
 function privacyLevelClass(level: string) {
-  if (level === 'full') return 'privacy-full'
-  if (level === 'partial') return 'privacy-partial'
+  if (level === 'full')
+    return 'privacy-full'
+  if (level === 'partial')
+    return 'privacy-partial'
   return 'privacy-none'
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@devtools/pages/first-party.vue` around lines 22 - 34, The three helper
functions include single-line if statements that violate the antfu/if-newline
rule; update privacyLevelClass to use multi-line if blocks with braces and line
breaks (e.g., if (level === 'full') { return 'privacy-full' } etc.) and ensure
mechanismClass and mechanismLabel do not use single-line ifs (they already use
ternaries—leave as-is or convert to multi-line ifs for consistency); modify the
functions privacyLevelClass, mechanismClass, and mechanismLabel so all if/return
statements are formatted with newlines and braces to satisfy the lint rule.
src/runtime/composables/useScript.ts (2)

156-156: Multiple statements on single line violates style rule.

ESLint flags this line for having 2 statements. Consider splitting for readability.

♻️ Suggested formatting
-      remove: () => { disconnectObserver(); return false },
+      remove: () => {
+        disconnectObserver()
+        return false
+      },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/runtime/composables/useScript.ts` at line 156, The arrow function
assigned to remove currently contains two statements on one line; split them
into separate statements for readability and to satisfy ESLint: change remove:
() => { disconnectObserver(); return false } to a multi-statement body (e.g.,
call disconnectObserver() on its own line then return false) within the same
remove property in the object so the function name remove and the helper
disconnectObserver remain clearly referenced.

343-344: Multiple statements on single line violates style rule.

ESLint flags line 344 for having 3 statements. The pattern of wrapping remove() is correct, but the formatting should be improved.

♻️ Suggested formatting
       const disconnectObserver = observeNetworkRequests(payload, domains, syncScripts)
       // Clean up observer when script is removed
       const _origRemove = instance.remove
-      instance.remove = () => { disconnectObserver(); return _origRemove() }
+      instance.remove = () => {
+        disconnectObserver()
+        return _origRemove()
+      }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/runtime/composables/useScript.ts` around lines 343 - 344, The line that
reassigns instance.remove currently has multiple statements on one line; split
and reformat the wrapper so it’s clear and lint-friendly while preserving
behavior: store the original method in _origRemove, assign instance.remove to a
function that first calls disconnectObserver() and then returns the result of
calling _origRemove(), and ensure the wrapper uses a multi-line function block
(not multiple statements on one line) and preserves this binding if needed.
devtools/components/NetworkWaterfall.vue (3)

75-81: Move regex to module scope to avoid re-compilation.

ESLint flags the regex on line 78 for being recreated on every function call. Moving it to module scope improves performance.

♻️ Suggested fix
+const PROXY_PATH_REGEX = /\/_scripts\/p\/([^/]+)/
+
 function extractHostname(url: string): string {
-  try { return new URL(url).hostname }
+  try {
+    return new URL(url).hostname
+  }
   catch {
-    const match = url.match(/\/_scripts\/p\/([^/]+)/)
+    const match = url.match(PROXY_PATH_REGEX)
     return match?.[1] || 'localhost'
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@devtools/components/NetworkWaterfall.vue` around lines 75 - 81, The regex
literal in extractHostname is recreated on every call; move the pattern const
SCRIPT_ID_RE = /\/_scripts\/p\/([^/]+)/ to module scope (above the function) and
replace the inline url.match(/\/_scripts\/p\/([^/]+)/) with
url.match(SCRIPT_ID_RE) in extractHostname to avoid repeated compilation while
preserving the same fallback behavior and return types.

109-115: Object literals should have line breaks between items (optional).

ESLint flags these object literals for missing line breaks per antfu/consistent-list-newline.

♻️ Suggested formatting
 function initiatorIcon(type: string) {
   const map: Record<string, string> = {
-    script: 'i-carbon-code', xmlhttprequest: 'i-carbon-send-alt', fetch: 'i-carbon-send-alt',
-    img: 'i-carbon-image', css: 'i-carbon-paint-brush', beacon: 'i-carbon-satellite-radar',
+    script: 'i-carbon-code',
+    xmlhttprequest: 'i-carbon-send-alt',
+    fetch: 'i-carbon-send-alt',
+    img: 'i-carbon-image',
+    css: 'i-carbon-paint-brush',
+    beacon: 'i-carbon-satellite-radar',
   }
   return map[type] || 'i-carbon-document'
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@devtools/components/NetworkWaterfall.vue` around lines 109 - 115, The object
literal in initiatorIcon uses multiple properties on one line which violates
antfu/consistent-list-newline; reformat the map constant so each key/value pair
is on its own line (e.g., separate lines for 'script', 'xmlhttprequest',
'fetch', 'img', 'css', 'beacon') and keep the same keys and values, then return
map[type] || 'i-carbon-document' as before; this preserves behavior while
satisfying the ESLint rule.

4-18: Consider importing NuxtDevToolsNetworkRequest instead of duplicating.

The NetworkRequest interface exactly matches NuxtDevToolsNetworkRequest from src/runtime/types.ts. Importing the existing type would reduce duplication and ensure consistency.

♻️ Suggested change
 <script setup lang="ts">
+import type { NuxtDevToolsNetworkRequest } from '../../src/runtime/types'
 import { bytesToHumanReadable, msToHumanReadable } from '~/utils/formatting'
 
-interface NetworkRequest {
-  url: string
-  startTime: number
-  duration: number
-  transferSize: number
-  encodedBodySize: number
-  decodedBodySize: number
-  initiatorType: string
-  dns: number
-  connect: number
-  ssl: number
-  ttfb: number
-  download: number
-  isProxied: boolean
-}
+type NetworkRequest = NuxtDevToolsNetworkRequest
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@devtools/components/NetworkWaterfall.vue` around lines 4 - 18, Replace the
duplicated NetworkRequest interface with an import of the shared
NuxtDevToolsNetworkRequest type: remove the local NetworkRequest declaration and
add an import for NuxtDevToolsNetworkRequest (use the exported type name) and
update any usages of NetworkRequest to NuxtDevToolsNetworkRequest; ensure the
import path points to the module that exports NuxtDevToolsNetworkRequest and
update any references in the component to use the imported type name.
devtools/pages/index.vue (1)

2-14: Import ordering can be improved (optional).

ESLint flags import ordering issues. The ~/composables/state import should come before ~/utils/formatting, and named imports should be alphabetically sorted.

♻️ Suggested import ordering
-import { humanFriendlyTimestamp, urlToOrigin } from '~/utils/formatting'
-import {
-  formatScriptInterface,
-  firstPartyData,
-  getActiveTab,
-  getFirstPartyScript,
-  isFirstPartyScript,
-  scriptErrors,
-  scriptLogo,
-  scriptSizes,
-  scripts,
-  setActiveTab,
-} from '~/composables/state'
+import {
+  firstPartyData,
+  formatScriptInterface,
+  getActiveTab,
+  getFirstPartyScript,
+  isFirstPartyScript,
+  scriptErrors,
+  scriptLogo,
+  scripts,
+  scriptSizes,
+  setActiveTab,
+} from '~/composables/state'
+import { humanFriendlyTimestamp, urlToOrigin } from '~/utils/formatting'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@devtools/pages/index.vue` around lines 2 - 14, Move the import of
~/composables/state above ~/utils/formatting and alphabetize the named imports
from the state module (e.g., ensure formatScriptInterface, firstPartyData,
getActiveTab, getFirstPartyScript, isFirstPartyScript, scriptErrors, scriptLogo,
scriptSizes, scripts, setActiveTab are sorted) so ESLint import-order and
sorting rules pass; keep humanFriendlyTimestamp and urlToOrigin (from
utils/formatting) as a separate import below the composables import.
devtools/composables/state.ts (3)

3-4: Import ordering can be improved (optional).

ESLint flags that ~/utils/fetch should come before ../../src/registry per import ordering rules.

♻️ Suggested fix
 import { reactive, ref } from 'vue'
+import { fetchScript } from '~/utils/fetch'
+import { msToHumanReadable } from '~/utils/formatting'
 import { registry } from '../../src/registry'
-import { fetchScript } from '~/utils/fetch'
-import { msToHumanReadable } from '~/utils/formatting'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@devtools/composables/state.ts` around lines 3 - 4, Reorder the imports so
they follow the project's ESLint import-order rules: move the import of
fetchScript from '~/utils/fetch' (and its companion msToHumanReadable if in the
same block) to appear before the import that references ../../src/registry;
update the import sequence in devtools/composables/state.ts accordingly and run
the linter to verify the ordering is satisfied.

71-84: Async operation inside synchronous map may cause subtle race conditions.

The fetchScript call is fire-and-forget within the map callback. While this is intentional (populate sizes asynchronously), the UI may briefly show stale data, and multiple rapid syncScripts calls could cause concurrent fetches for the same script.

Consider adding a pending/in-flight tracking mechanism to avoid duplicate fetches.

💡 Suggested improvement
+const pendingFetches = new Set<string>()
+
 export function syncScripts(_scripts: any[]) {
   // ...
         const scriptSizeKey = script.src
-        if (!scriptSizes[scriptSizeKey] && script.src) {
+        if (!scriptSizes[scriptSizeKey] && script.src && !pendingFetches.has(scriptSizeKey)) {
+          pendingFetches.add(scriptSizeKey)
           fetchScript(script.src)
             .then((res) => {
               if (res.size) {
                 scriptSizes[scriptSizeKey] = res.size
                 script.size = res.size
               }
               if (res.error) {
                 scriptErrors[scriptSizeKey] = res.error
                 script.error = res.error
               }
             })
+            .finally(() => {
+              pendingFetches.delete(scriptSizeKey)
+            })
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@devtools/composables/state.ts` around lines 71 - 84, The map callback starts
fire-and-forget fetches which can cause duplicate concurrent requests and stale
UI; introduce an in-flight tracking map (e.g., scriptFetchInFlight or
scriptFetchPromises keyed by script.src) and use it inside the block that
currently calls fetchScript so you skip starting a new fetch if an entry exists;
when starting a fetch store the promise in the map, await/then it, update
scriptSizes, scriptErrors, and script.size/script.error on resolution, and
finally remove the entry from the in-flight map (and handle rejections) so
subsequent syncScripts calls reuse the existing promise and avoid duplicate
fetches.

47-52: Move regex patterns to module scope.

ESLint flags regexes on lines 48, 64, and 120 for being recreated on every function call. Moving them to module scope avoids recompilation overhead.

♻️ Suggested fix
+const TITLE_SEPARATOR_REGEX = /([\s_-])+/g
+const SPACE_REGEX = / /g
+const PARAM_MATCH_REGEX = /\(([^)]*)\)/
+
 function titleToCamelCase(s: string) {
-  return s.replace(/([\s_-])+/g, ' ').split(' ').map((w, i) => {
+  return s.replace(TITLE_SEPARATOR_REGEX, ' ').split(' ').map((w, i) => {
     if (i === 0) return w.toLowerCase()
     return w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()
   }).join('')
 }

And similarly for line 64:

-          const kebabCaseLabel = script.registry.label.toLowerCase().replace(/ /g, '-')
+          const kebabCaseLabel = script.registry.label.toLowerCase().replace(SPACE_REGEX, '-')

And line 120:

-  const paramMatch = funcStr.match(/\(([^)]*)\)/)
+  const paramMatch = funcStr.match(PARAM_MATCH_REGEX)

Also applies to: 64-64, 120-120

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

In `@devtools/composables/state.ts` around lines 47 - 52, Hoist the regex literals
out of the functions into module-scope constants and reuse them instead of
recreating them on every call: for example create const SEPARATORS_RE =
/([\s_-])+/g (or other appropriately named constants) at top-level and replace
the inline regex in titleToCamelCase with SEPARATORS_RE, and do the same for the
two other regexes used elsewhere in this module so each function references the
shared module-scope constants rather than constructing new RegExp instances.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@devtools/app.vue`:
- Around line 10-19: currentTab is a getter-only computed based on route.path
but is bound with v-model:active-tab on DevtoolsLayout, causing a runtime
warning when update:activeTab is emitted; either change the binding to one-way
:active-tab to reflect that route is the single source of truth, or make
currentTab writable by adding a setter that updates the route (e.g., using
router.push/replace) so writes from DevtoolsLayout (update:activeTab) will
update route.path correctly. Ensure you reference currentTab, route,
DevtoolsLayout and the emitted event update:activeTab when applying the change.

In `@devtools/components/ScriptStatus.vue`:
- Around line 7-19: The ESLint antfu/if-newline rule is violated by single-line
if returns in the computed properties; update the displayStatus and statusClass
computed functions (symbols: displayStatus, statusClass) to use a proper
multiline if form—either add braces and put the return on the next line or move
the return to a new line after the if—so lines like "if (error) return ..." and
"if (error || status === 'error') return ..." become multiline compliant (e.g.,
"if (error) { return ... }" or "if (error)\n  return ...").

In `@devtools/package.json`:
- Around line 13-19: The Carbon icon JSON package was removed from
devDependencies causing icon resolution failures (icons like carbon:script,
i-carbon-meter, i-carbon-script etc. used across devtools/app.vue,
devtools/components/ScriptSize.vue and devtools/pages/*). Restore support by
re-adding "@iconify-json/carbon" into the "devDependencies" section of
devtools/package.json (pin to a compatible version) or alternatively replace all
Carbon icon references with icons provided by an installed package; ensure
nuxtseo-layer-devtools is not relied upon to supply "@iconify-json/carbon".
Update package.json accordingly and run install/build to verify icons resolve.

In `@devtools/pages/index.vue`:
- Around line 182-190: The message incorrectly prefixes validation issue paths
with the outer v-for index `key`; update the template that builds the error
string (the expression inside the span using event.args.issues.map) to stop
using `key.` and instead reference the issue path or an appropriate event field
(e.g., use only i.path?.map(p => p.key).join(',') or, if the event contains a
field name like event.field or event.name, prefix with that property). Edit the
mapping expression in the span so it produces `${i.path?.map(p =>
p.key).join(',')}: ${i.message}` or `${event.name}.${i.path?.map(...)}:
${i.message}` as appropriate.

In `@devtools/pages/registry.vue`:
- Around line 61-70: The two logo render paths diverge: consolidate them by
creating a single theme-aware logo resolver (e.g., a computed helper or small
component named LogoRenderer or useResolvedLogo) that accepts script.logo and
the current theme and returns either a URL string or safe HTML/SVG string plus a
flag indicating render mode; update both the inline img/v-html block and the
capability-matrix rendering (where v-html is always used) to call this helper
and: if mode === 'url' render an <img :src="url">, otherwise render sanitized
HTML/SVG via v-html, and ensure the resolver prefers the theme-appropriate
variant (choose light vs dark based on current theme) instead of unconditionally
using dark || light.
- Around line 106-110: The docs link is invisible to keyboard users because it
uses "opacity-0" and only becomes visible on hover; update the anchor element
(the <a> with aria-label "View documentation" and class "opacity-0
group-hover:opacity-60 transition-opacity flex-shrink-0") to also change opacity
on keyboard focus (e.g. add a focus or focus-visible utility such as
"focus:opacity-60" or "focus-visible:opacity-60") so tabbing to the control
makes it visible.
- Around line 12-22: The lint errors are from single-line if statements; update
capState and capStateLabel so each if places its consequent on the next line (or
use braces) instead of using the inline "if (...) return ..." form—specifically
change the checks in capState (the "if (!supported)" and "if (active)" usages
around supported/active and their returns) and the checks in capStateLabel (the
"if (state === 'active')" and "if (state === 'available')" single-line returns)
to multi-line ifs or brace-wrapped blocks to satisfy the antfu/if-newline rule.

In `@devtools/utils/formatting.ts`:
- Around line 29-34: The function bytesToHumanReadable contains single-line if
statements that violate the antfu/if-newline rule; update each single-line if
(the checks inside bytesToHumanReadable) to use multi-line blocks with a newline
after the if (e.g., if (bytes === 0) { return '0 B' } → if (bytes === 0) {↵ 
return '0 B'↵}) or add braces and place the return on the next line for all four
conditions so each if has a newline and block body.

In `@src/runtime/composables/useScript.ts`:
- Around line 58-61: The empty catch block in useScript.ts that swallows URL
parsing errors should be made explicit: replace catch {} with catch (err) { /*
ignore expected URL parsing errors */ } or catch (err) {
console.debug('useScript: URL parse failed', err); } — reference the catch block
around the URL parsing in useScript.ts and use a named variable like err or
parseError to either log debug/error details (preferred) or include a clear
comment indicating the error is intentionally ignored.

---

Outside diff comments:
In `@devtools/utils/formatting.ts`:
- Around line 13-17: The function urlToOrigin has its condition inverted causing
new URL(url) to be called for relative URLs and throw; in the export function
urlToOrigin change the check so you only call new URL(url).origin for absolute
URLs (i.e. if NOT isRelative(url, { acceptRelative: true })) and otherwise
return the original input for relative URLs; update the conditional in
urlToOrigin accordingly to avoid constructing a URL for relative values.

---

Nitpick comments:
In `@devtools/components/NetworkWaterfall.vue`:
- Around line 75-81: The regex literal in extractHostname is recreated on every
call; move the pattern const SCRIPT_ID_RE = /\/_scripts\/p\/([^/]+)/ to module
scope (above the function) and replace the inline
url.match(/\/_scripts\/p\/([^/]+)/) with url.match(SCRIPT_ID_RE) in
extractHostname to avoid repeated compilation while preserving the same fallback
behavior and return types.
- Around line 109-115: The object literal in initiatorIcon uses multiple
properties on one line which violates antfu/consistent-list-newline; reformat
the map constant so each key/value pair is on its own line (e.g., separate lines
for 'script', 'xmlhttprequest', 'fetch', 'img', 'css', 'beacon') and keep the
same keys and values, then return map[type] || 'i-carbon-document' as before;
this preserves behavior while satisfying the ESLint rule.
- Around line 4-18: Replace the duplicated NetworkRequest interface with an
import of the shared NuxtDevToolsNetworkRequest type: remove the local
NetworkRequest declaration and add an import for NuxtDevToolsNetworkRequest (use
the exported type name) and update any usages of NetworkRequest to
NuxtDevToolsNetworkRequest; ensure the import path points to the module that
exports NuxtDevToolsNetworkRequest and update any references in the component to
use the imported type name.

In `@devtools/composables/state.ts`:
- Around line 3-4: Reorder the imports so they follow the project's ESLint
import-order rules: move the import of fetchScript from '~/utils/fetch' (and its
companion msToHumanReadable if in the same block) to appear before the import
that references ../../src/registry; update the import sequence in
devtools/composables/state.ts accordingly and run the linter to verify the
ordering is satisfied.
- Around line 71-84: The map callback starts fire-and-forget fetches which can
cause duplicate concurrent requests and stale UI; introduce an in-flight
tracking map (e.g., scriptFetchInFlight or scriptFetchPromises keyed by
script.src) and use it inside the block that currently calls fetchScript so you
skip starting a new fetch if an entry exists; when starting a fetch store the
promise in the map, await/then it, update scriptSizes, scriptErrors, and
script.size/script.error on resolution, and finally remove the entry from the
in-flight map (and handle rejections) so subsequent syncScripts calls reuse the
existing promise and avoid duplicate fetches.
- Around line 47-52: Hoist the regex literals out of the functions into
module-scope constants and reuse them instead of recreating them on every call:
for example create const SEPARATORS_RE = /([\s_-])+/g (or other appropriately
named constants) at top-level and replace the inline regex in titleToCamelCase
with SEPARATORS_RE, and do the same for the two other regexes used elsewhere in
this module so each function references the shared module-scope constants rather
than constructing new RegExp instances.

In `@devtools/pages/first-party.vue`:
- Around line 6-15: The three object literals privacyFlagLabels,
privacyFlagShort, and privacyFlagIcons are written as single-line multi-entry
objects which ESLint flags; reformat each object so every key/value pair is on
its own line (one property per line) with trailing commas and consistent
indentation to satisfy the rule and improve readability.
- Around line 22-34: The three helper functions include single-line if
statements that violate the antfu/if-newline rule; update privacyLevelClass to
use multi-line if blocks with braces and line breaks (e.g., if (level ===
'full') { return 'privacy-full' } etc.) and ensure mechanismClass and
mechanismLabel do not use single-line ifs (they already use ternaries—leave
as-is or convert to multi-line ifs for consistency); modify the functions
privacyLevelClass, mechanismClass, and mechanismLabel so all if/return
statements are formatted with newlines and braces to satisfy the lint rule.

In `@devtools/pages/index.vue`:
- Around line 2-14: Move the import of ~/composables/state above
~/utils/formatting and alphabetize the named imports from the state module
(e.g., ensure formatScriptInterface, firstPartyData, getActiveTab,
getFirstPartyScript, isFirstPartyScript, scriptErrors, scriptLogo, scriptSizes,
scripts, setActiveTab are sorted) so ESLint import-order and sorting rules pass;
keep humanFriendlyTimestamp and urlToOrigin (from utils/formatting) as a
separate import below the composables import.

In `@src/runtime/composables/useScript.ts`:
- Line 156: The arrow function assigned to remove currently contains two
statements on one line; split them into separate statements for readability and
to satisfy ESLint: change remove: () => { disconnectObserver(); return false }
to a multi-statement body (e.g., call disconnectObserver() on its own line then
return false) within the same remove property in the object so the function name
remove and the helper disconnectObserver remain clearly referenced.
- Around line 343-344: The line that reassigns instance.remove currently has
multiple statements on one line; split and reformat the wrapper so it’s clear
and lint-friendly while preserving behavior: store the original method in
_origRemove, assign instance.remove to a function that first calls
disconnectObserver() and then returns the result of calling _origRemove(), and
ensure the wrapper uses a multi-line function block (not multiple statements on
one line) and preserves this binding if needed.

In `@vitest.config.ts`:
- Around line 52-68: The nuxt-runtime Vitest project (defined via
defineVitestProject with test.name 'nuxt-runtime') currently uses a broad
include './**/*.nuxt.test.ts' which can match files under .claude/worktrees;
update the test.exclude array for this project to add the same exclusion used
elsewhere (e.g. the .claude/worktrees pattern) so files in .claude/worktrees are
ignored by the nuxt-runtime project.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 78f8925e-1458-4f87-93be-5fda013f800c

📥 Commits

Reviewing files that changed from the base of the PR and between b5e2858 and 49da481.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (35)
  • client/app.vue
  • client/components/OCodeBlock.vue
  • client/components/OSectionBlock.vue
  • client/components/ScriptLoadTime.vue
  • client/components/ScriptSize.vue
  • client/components/ScriptStatus.vue
  • client/composables/rpc.ts
  • client/composables/shiki.ts
  • client/nuxt.config.ts
  • devtools/app.config.ts
  • devtools/app.vue
  • devtools/bootstrap.ts
  • devtools/components/NetworkWaterfall.vue
  • devtools/components/ScriptLoadTime.vue
  • devtools/components/ScriptSize.vue
  • devtools/components/ScriptStatus.vue
  • devtools/composables/rpc.ts
  • devtools/composables/state.ts
  • devtools/nuxt.config.ts
  • devtools/package.json
  • devtools/pages/docs.vue
  • devtools/pages/first-party.vue
  • devtools/pages/index.vue
  • devtools/pages/registry.vue
  • devtools/tsconfig.json
  • devtools/utils/fetch.ts
  • devtools/utils/formatting.ts
  • package.json
  • pnpm-workspace.yaml
  • src/devtools.ts
  • src/runtime/composables/useScript.ts
  • src/runtime/types.ts
  • src/runtime/utils.ts
  • tsconfig.json
  • vitest.config.ts
💤 Files with no reviewable changes (9)
  • client/composables/rpc.ts
  • client/components/ScriptLoadTime.vue
  • client/components/ScriptSize.vue
  • client/composables/shiki.ts
  • client/components/OSectionBlock.vue
  • client/components/OCodeBlock.vue
  • client/components/ScriptStatus.vue
  • client/app.vue
  • client/nuxt.config.ts

Comment on lines 13 to 19
"devDependencies": {
"@iconify-json/carbon": "^1.2.19",
"@nuxt/devtools-kit": "^3.2.4",
"@nuxt/devtools-ui-kit": "^3.2.4",
"@nuxt/kit": "^4.4.2",
"nuxt": "^4.4.2",
"nuxtseo-layer-devtools": "^0.2.5",
"vue": "^3.5.30",
"vue-router": "latest"
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if nuxtseo-layer-devtools includes `@iconify-json/carbon` as a dependency
npm view nuxtseo-layer-devtools@0.2.5 dependencies --json 2>/dev/null | jq '.'

# Also check devDependencies and peerDependencies
npm view nuxtseo-layer-devtools@0.2.5 devDependencies --json 2>/dev/null | jq '.'
npm view nuxtseo-layer-devtools@0.2.5 peerDependencies --json 2>/dev/null | jq '.'

# Search for all carbon icon usages in devtools
rg -n "carbon:" devtools/
rg -n "i-carbon" devtools/

Repository: nuxt/scripts

Length of output: 5522


Verify that Carbon icons remain available after removing @iconify-json/carbon.

The @iconify-json/carbon package was removed from devDependencies, but Carbon icons are actively used throughout the devtools codebase in at least 8 files with 27+ icon references:

  • devtools/app.vue: carbon:script, carbon:security, carbon:catalog, carbon:book
  • devtools/components/ScriptSize.vue: i-carbon-meter
  • devtools/pages/ (multiple): i-carbon-script, i-carbon-security, i-carbon-list, i-carbon-network-4, i-carbon-code, i-carbon-play-filled-alt, i-carbon-close, i-carbon-location, i-carbon-user-profile, i-carbon-translate, i-carbon-screen, i-carbon-time, i-carbon-chip, i-carbon-security, i-carbon-information, i-carbon-direction-fork, i-carbon-arrows-horizontal, i-carbon-chevron-up, i-carbon-chevron-down, i-carbon-arrow-right, i-carbon-data-table, i-carbon-archive, i-carbon-container-software, i-carbon-catalog, i-carbon-launch, i-carbon-timer, i-carbon-send-alt, i-carbon-image, i-carbon-paint-brush, i-carbon-satellite-radar, i-carbon-document, i-carbon-network-4

nuxtseo-layer-devtools does not provide @iconify-json/carbon as a dependency. Without this package, icon resolution will fail at build time.

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

In `@devtools/package.json` around lines 13 - 19, The Carbon icon JSON package was
removed from devDependencies causing icon resolution failures (icons like
carbon:script, i-carbon-meter, i-carbon-script etc. used across
devtools/app.vue, devtools/components/ScriptSize.vue and devtools/pages/*).
Restore support by re-adding "@iconify-json/carbon" into the "devDependencies"
section of devtools/package.json (pin to a compatible version) or alternatively
replace all Carbon icon references with icons provided by an installed package;
ensure nuxtseo-layer-devtools is not relied upon to supply
"@iconify-json/carbon". Update package.json accordingly and run install/build to
verify icons resolve.

Comment on lines +61 to +70
<img
v-if="script.logo && typeof script.logo === 'string' && script.logo.startsWith('http')"
class="w-5 h-5 object-contain"
:src="typeof script.logo === 'object' ? script.logo.dark || script.logo.light : script.logo"
alt=""
>
<div
v-else-if="script.logo"
class="w-5 h-5 flex items-center [&>svg]:max-h-5 [&>svg]:max-w-5"
v-html="typeof script.logo === 'object' ? (script.logo.dark || script.logo.light) : script.logo"
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Centralize logo resolution for both registry views.

These two render paths have drifted apart. Lines 61-70 already acknowledge that script.logo can be a URL string, but Lines 153-156 always push script.logo through v-html, so any URL-backed logo will render incorrectly in the capability matrix. Both blocks also hard-code dark || light, which picks the dark asset even in light mode when both variants exist. A shared, theme-aware logo helper/component would fix both problems at once.

Also applies to: 153-156

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

In `@devtools/pages/registry.vue` around lines 61 - 70, The two logo render paths
diverge: consolidate them by creating a single theme-aware logo resolver (e.g.,
a computed helper or small component named LogoRenderer or useResolvedLogo) that
accepts script.logo and the current theme and returns either a URL string or
safe HTML/SVG string plus a flag indicating render mode; update both the inline
img/v-html block and the capability-matrix rendering (where v-html is always
used) to call this helper and: if mode === 'url' render an <img :src="url">,
otherwise render sanitized HTML/SVG via v-html, and ensure the resolver prefers
the theme-appropriate variant (choose light vs dark based on current theme)
instead of unconditionally using dark || light.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (3)
devtools/pages/registry.vue (2)

117-124: ⚠️ Potential issue | 🟡 Minor

Accessibility: Docs link invisible on keyboard focus.

The link uses opacity-0 and only becomes visible on group-hover, making it invisible to keyboard users who tab to it.

♿ Proposed fix
             <a
               :href="`https://scripts.nuxt.com/scripts/${script.label.toLowerCase().replace(/ /g, '-')}`"
               target="_blank"
-              class="opacity-0 group-hover:opacity-60 transition-opacity flex-shrink-0"
+              class="opacity-0 group-hover:opacity-60 focus:opacity-60 focus-visible:opacity-100 transition-opacity flex-shrink-0"
               aria-label="View documentation"
             >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@devtools/pages/registry.vue` around lines 117 - 124, The anchor for the docs
link is hidden with class "opacity-0" and only revealed on mouse hover
("group-hover:opacity-60"), so keyboard users who tab to the link remain
invisible; update the anchor element (the <a> that contains <UIcon
name="i-carbon-launch" ... />) to also reveal on keyboard focus by adding
focus/focus-visible utility classes (e.g., "group-focus:opacity-60" or
"focus:opacity-60 focus-visible:opacity-60") or remove the hard "opacity-0" and
instead control visibility with combined hover+focus states so that tabbing
shows the icon while preserving the visual hover behavior and existing
aria-label.

66-76: ⚠️ Potential issue | 🟡 Minor

Bug: Conditional logic contradicts the :src binding.

Line 67 checks if script.logo is a string starting with 'http', but line 69 then accesses script.logo as an object with .dark || .light. This ternary will never execute the object branch because the v-if already guarantees script.logo is an HTTP URL string.

Additionally, the capability matrix (lines 166-170) always uses v-html even for URL-based logos, which would render incorrectly.

🐛 Proposed fix
             <img
-                v-if="script.logo && typeof script.logo === 'string' && script.logo.startsWith('http')"
+                v-if="typeof script.logo === 'string' && script.logo.startsWith('http')"
                 class="w-5 h-5 object-contain"
-                :src="typeof script.logo === 'object' ? script.logo.dark || script.logo.light : script.logo"
+                :src="script.logo"
                 alt=""
               >

For the matrix, consider adding a similar conditional:

                   <div
                     v-if="script.logo"
                     class="w-4 h-4 flex items-center [&>svg]:max-h-4 [&>svg]:max-w-4"
-                    v-html="typeof script.logo === 'object' ? (script.logo.dark || script.logo.light) : script.logo"
                   />
+                  >
+                    <img v-if="typeof script.logo === 'string' && script.logo.startsWith('http')" :src="script.logo" class="w-4 h-4 object-contain" alt="">
+                    <div v-else v-html="typeof script.logo === 'object' ? (script.logo.dark || script.logo.light) : script.logo" />
+                  </div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@devtools/pages/registry.vue` around lines 66 - 76, The current template
wrongly assumes an object logo inside the <img> block and always uses v-html
elsewhere; update the conditional logic so URL logos and object (dark/light
SVG/HTML) logos are distinguished consistently: change the <img> v-if to check
for a URL (script.logo && typeof script.logo === 'string' &&
script.logo.startsWith('http')) and use :src binding only in that branch, and in
the alternate branch (v-else / v-else-if when script.logo is an object or
SVG/HTML) render with v-html (using script.logo.dark || script.logo.light when
object) so both the image block (the <img> with :src) and the capability matrix
(where v-html is currently used) switch based on the same
typeof/script.logo.startsWith('http') checks to avoid treating URLs as objects
and vice versa.
devtools/pages/index.vue (1)

191-193: ⚠️ Potential issue | 🟡 Minor

Bug: key variable misused in validation error message.

Line 192 uses key (the loop index from v-for="(event, key) in script.events") as a prefix in the error path. The loop index doesn't represent the actual validation context and produces misleading output like 0.fieldName: message instead of the actual field path.

🐛 Proposed fix
                   <span class="text-[11px] text-(--color-text-subtle) truncate">
-                      {{ event.args.issues.map((i: any) => `${key}.${i.path?.map((i: any) => i.key).join(',')}: ${i.message}`).join(',') }}
+                      {{ event.args.issues.map((i: any) => `${i.path?.map((p: any) => p.key).join('.')}: ${i.message}`).join(', ') }}
                   </span>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@devtools/pages/index.vue` around lines 191 - 193, The template is using the
v-for index variable "key" as a misleading prefix in the validation message;
update the expression inside the span to stop using the loop index and instead
use the actual event identifier (or nothing) — e.g. replace
`${key}.${i.path?.map(...` with a safe event identifier like `${event.key ??
event.name ?? ''}${(event.key ?? event.name) ? '.' : ''}${i.path?.map((p: any)
=> p.key).join(',')}: ${i.message}` or simply omit the prefix and render
`${i.path?.map((p: any) => p.key).join(',')}: ${i.message}`; target the template
expression that maps event.args.issues in the component using v-for="(event,
key) in script.events".
🧹 Nitpick comments (3)
devtools/composables/state.ts (2)

33-33: Top-level registry() call may execute before module dependencies are ready.

Calling registry() at module load time creates a promise immediately when this module is first imported. If registry() depends on runtime context or resolvers that aren't yet initialized, this could fail silently or cause race conditions.

💡 Consider lazy initialization
-const _registryPromise = registry()
+let _registryPromise: ReturnType<typeof registry> | null = null

 export async function initRegistry() {
+  if (!_registryPromise) {
+    _registryPromise = registry()
+  }
   scriptRegistry.value = await _registryPromise
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@devtools/composables/state.ts` at line 33, The top-level initialization
"const _registryPromise = registry()" can run too early; change it to lazy
initialization by replacing the top-level call with a memoized accessor function
(e.g., getRegistryPromise) that calls registry() the first time it's needed and
caches the returned promise for subsequent calls, ensuring registry() is only
invoked at runtime when dependencies are ready; update any code that referenced
_registryPromise to call the new accessor instead so there are no module-load
side effects.

57-61: Input validation could be more precise.

The check typeof _scripts !== 'object' will pass for arrays (which are objects). If _scripts should be a plain object, consider using !_scripts || Array.isArray(_scripts) || typeof _scripts !== 'object'. However, this may be intentional if arrays are valid input that should be converted via Object.entries.

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

In `@devtools/composables/state.ts` around lines 57 - 61, The input validation in
syncScripts is too permissive because typeof _scripts === 'object' is true for
arrays; update the guard to reject arrays and non-plain objects before assigning
scripts.value = {}. Replace the condition in syncScripts with a check that
ensures _scripts is a plain object (for example: if (!_scripts ||
Array.isArray(_scripts) || Object.prototype.toString.call(_scripts) !== '[object
Object]') { scripts.value = {}; return }) so that arrays or other non-plain
objects are not treated as valid script maps while still preserving behavior
that converts plain objects into scripts.value.
devtools/pages/index.vue (1)

99-118: Consider defensive checks for $script methods.

Lines 105 and 115 call script.$script.load() and script.$script.remove(). While the conditions check script.$script.status, if $script is undefined or these methods don't exist, runtime errors could occur. The context snippet shows $script is typed as any and optional.

🛡️ Defensive approach
           <UButton
             v-if="script.$script.status === 'awaitingLoad'"
             size="xs"
             color="primary"
             variant="soft"
             icon="i-carbon-play-filled-alt"
-            `@click`="script.$script.load()"
+            `@click`="script.$script?.load?.()"
           >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@devtools/pages/index.vue` around lines 99 - 118, Add defensive checks before
invoking script.$script.load() and script.$script.remove(): ensure
script.$script exists and that load/remove are functions (e.g., check
script.$script && typeof script.$script.load === 'function' and similarly for
remove) and/or use optional chaining when binding the click handlers so the UI
doesn't call undefined methods; keep the existing status checks
(script.$script.status) but guard the click handlers for the symbols
script.$script.load and script.$script.remove to avoid runtime errors.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@devtools/composables/state.ts`:
- Around line 82-84: Assigning res.error (an Error) into string-typed storage
causes a type mismatch; update the assignment in the block that touches
scriptErrors and script (references: scriptErrors, scriptSizeKey, res.error,
script.error) to store a string representation (e.g., res.error.message or
res.error.toString()) instead of the Error object, or change the target types to
accept Error if you intend to keep the object; pick the string approach if
scriptErrors is expected to be Record<string,string> and ensure both
scriptErrors[scriptSizeKey] and script.error receive the chosen string value.

In `@src/runtime/composables/useScript.ts`:
- Around line 46-47: The hostname-only match using entryUrl.hostname and
domains.has is too permissive and causes same-origin resources to be
misattributed; update the matching logic so that if the script src (entry.name)
resolves to the same origin as window.location.origin you require a stricter
match (e.g., full resolved URL equality or compare origin+pathname) or require
the caller to include the host in options.devtools.domains, but for cross-origin
absolute URLs allow the hostname-only check (keep domains.has on
entryUrl.hostname only when entryUrl.origin !== window.location.origin); apply
the same fix to the other similar matching blocks that use entryUrl.hostname
(references: entryUrl, entry.name, domains.has, networkRequests,
options.devtools.domains).
- Around line 352-358: The observer created by observeNetworkRequests is being
disconnected inside the overridden instance.remove(), but reload() calls
instance.remove() which causes the observer to be torn down during reload and
never reconnected; modify the logic so remove() only calls disconnectObserver
when it’s a final removal, not during reload: implement a reload-aware mechanism
(e.g., add a boolean flag like isReloading toggled by reload() around its call
to instance.remove(), or change instance.remove to accept a force/final boolean)
and update reload() to set that flag or call remove(false) so the override skips
disconnectObserver during reload; ensure disconnectObserver is still invoked
when a true final removal occurs.

---

Duplicate comments:
In `@devtools/pages/index.vue`:
- Around line 191-193: The template is using the v-for index variable "key" as a
misleading prefix in the validation message; update the expression inside the
span to stop using the loop index and instead use the actual event identifier
(or nothing) — e.g. replace `${key}.${i.path?.map(...` with a safe event
identifier like `${event.key ?? event.name ?? ''}${(event.key ?? event.name) ?
'.' : ''}${i.path?.map((p: any) => p.key).join(',')}: ${i.message}` or simply
omit the prefix and render `${i.path?.map((p: any) => p.key).join(',')}:
${i.message}`; target the template expression that maps event.args.issues in the
component using v-for="(event, key) in script.events".

In `@devtools/pages/registry.vue`:
- Around line 117-124: The anchor for the docs link is hidden with class
"opacity-0" and only revealed on mouse hover ("group-hover:opacity-60"), so
keyboard users who tab to the link remain invisible; update the anchor element
(the <a> that contains <UIcon name="i-carbon-launch" ... />) to also reveal on
keyboard focus by adding focus/focus-visible utility classes (e.g.,
"group-focus:opacity-60" or "focus:opacity-60 focus-visible:opacity-60") or
remove the hard "opacity-0" and instead control visibility with combined
hover+focus states so that tabbing shows the icon while preserving the visual
hover behavior and existing aria-label.
- Around line 66-76: The current template wrongly assumes an object logo inside
the <img> block and always uses v-html elsewhere; update the conditional logic
so URL logos and object (dark/light SVG/HTML) logos are distinguished
consistently: change the <img> v-if to check for a URL (script.logo && typeof
script.logo === 'string' && script.logo.startsWith('http')) and use :src binding
only in that branch, and in the alternate branch (v-else / v-else-if when
script.logo is an object or SVG/HTML) render with v-html (using script.logo.dark
|| script.logo.light when object) so both the image block (the <img> with :src)
and the capability matrix (where v-html is currently used) switch based on the
same typeof/script.logo.startsWith('http') checks to avoid treating URLs as
objects and vice versa.

---

Nitpick comments:
In `@devtools/composables/state.ts`:
- Line 33: The top-level initialization "const _registryPromise = registry()"
can run too early; change it to lazy initialization by replacing the top-level
call with a memoized accessor function (e.g., getRegistryPromise) that calls
registry() the first time it's needed and caches the returned promise for
subsequent calls, ensuring registry() is only invoked at runtime when
dependencies are ready; update any code that referenced _registryPromise to call
the new accessor instead so there are no module-load side effects.
- Around line 57-61: The input validation in syncScripts is too permissive
because typeof _scripts === 'object' is true for arrays; update the guard to
reject arrays and non-plain objects before assigning scripts.value = {}. Replace
the condition in syncScripts with a check that ensures _scripts is a plain
object (for example: if (!_scripts || Array.isArray(_scripts) ||
Object.prototype.toString.call(_scripts) !== '[object Object]') { scripts.value
= {}; return }) so that arrays or other non-plain objects are not treated as
valid script maps while still preserving behavior that converts plain objects
into scripts.value.

In `@devtools/pages/index.vue`:
- Around line 99-118: Add defensive checks before invoking script.$script.load()
and script.$script.remove(): ensure script.$script exists and that load/remove
are functions (e.g., check script.$script && typeof script.$script.load ===
'function' and similarly for remove) and/or use optional chaining when binding
the click handlers so the UI doesn't call undefined methods; keep the existing
status checks (script.$script.status) but guard the click handlers for the
symbols script.$script.load and script.$script.remove to avoid runtime errors.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3dc2b346-b917-455a-8a5e-bcbd37df5ef9

📥 Commits

Reviewing files that changed from the base of the PR and between 49da481 and 016340a.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (11)
  • devtools/bootstrap.ts
  • devtools/components/NetworkWaterfall.vue
  • devtools/components/ScriptStatus.vue
  • devtools/composables/state.ts
  • devtools/package.json
  • devtools/pages/first-party.vue
  • devtools/pages/index.vue
  • devtools/pages/registry.vue
  • devtools/utils/formatting.ts
  • pnpm-workspace.yaml
  • src/runtime/composables/useScript.ts
✅ Files skipped from review due to trivial changes (2)
  • devtools/bootstrap.ts
  • devtools/components/ScriptStatus.vue
🚧 Files skipped from review as they are similar to previous changes (4)
  • pnpm-workspace.yaml
  • devtools/package.json
  • devtools/components/NetworkWaterfall.vue
  • devtools/utils/formatting.ts

- Fix same-origin hostname matching capturing all local network requests
- Reconnect PerformanceObserver after script reload()
- Fix Error-to-string type mismatch in devtools state
- Fix validation error path using loop index instead of issue path
- Fix antfu/if-newline lint violations across devtools
- Add focus-visible to registry docs link for keyboard accessibility
- Fix capability matrix rendering URL logos via v-html
- Use :active-tab instead of v-model on getter-only computed
@harlan-zw harlan-zw merged commit bd82ce6 into main Mar 24, 2026
9 of 10 checks passed
@harlan-zw harlan-zw deleted the chore/rename-client-to-devtools branch March 24, 2026 12:39
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