Conversation
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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
commit: |
|
Caution Review failedPull request was closed or merged during review 📝 WalkthroughWalkthroughThe PR deletes the legacy Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 | 🔴 CriticalLogic error:
urlToOriginwill throw for relative URLs.The condition is inverted. When
isRelative(url)returnstrue, the code attemptsnew URL(url)which throws aTypeErrorbecause 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-runtimeproject 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.tsfiles 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-newlinerule expects newlines afterifstatements.♻️ 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 importingNuxtDevToolsNetworkRequestinstead of duplicating.The
NetworkRequestinterface exactly matchesNuxtDevToolsNetworkRequestfromsrc/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/stateimport 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/fetchshould come before../../src/registryper 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
fetchScriptcall is fire-and-forget within the map callback. While this is intentional (populate sizes asynchronously), the UI may briefly show stale data, and multiple rapidsyncScriptscalls 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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (35)
client/app.vueclient/components/OCodeBlock.vueclient/components/OSectionBlock.vueclient/components/ScriptLoadTime.vueclient/components/ScriptSize.vueclient/components/ScriptStatus.vueclient/composables/rpc.tsclient/composables/shiki.tsclient/nuxt.config.tsdevtools/app.config.tsdevtools/app.vuedevtools/bootstrap.tsdevtools/components/NetworkWaterfall.vuedevtools/components/ScriptLoadTime.vuedevtools/components/ScriptSize.vuedevtools/components/ScriptStatus.vuedevtools/composables/rpc.tsdevtools/composables/state.tsdevtools/nuxt.config.tsdevtools/package.jsondevtools/pages/docs.vuedevtools/pages/first-party.vuedevtools/pages/index.vuedevtools/pages/registry.vuedevtools/tsconfig.jsondevtools/utils/fetch.tsdevtools/utils/formatting.tspackage.jsonpnpm-workspace.yamlsrc/devtools.tssrc/runtime/composables/useScript.tssrc/runtime/types.tssrc/runtime/utils.tstsconfig.jsonvitest.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
devtools/package.json
Outdated
| "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" |
There was a problem hiding this comment.
🧩 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:bookdevtools/components/ScriptSize.vue:i-carbon-meterdevtools/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.
| <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" |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (3)
devtools/pages/registry.vue (2)
117-124:⚠️ Potential issue | 🟡 MinorAccessibility: Docs link invisible on keyboard focus.
The link uses
opacity-0and only becomes visible ongroup-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 | 🟡 MinorBug: Conditional logic contradicts the
:srcbinding.Line 67 checks if
script.logois a string starting with'http', but line 69 then accessesscript.logoas an object with.dark || .light. This ternary will never execute the object branch because thev-ifalready guaranteesscript.logois an HTTP URL string.Additionally, the capability matrix (lines 166-170) always uses
v-htmleven 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 | 🟡 MinorBug:
keyvariable misused in validation error message.Line 192 uses
key(the loop index fromv-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 like0.fieldName: messageinstead 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-levelregistry()call may execute before module dependencies are ready.Calling
registry()at module load time creates a promise immediately when this module is first imported. Ifregistry()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_scriptsshould 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 viaObject.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$scriptmethods.Lines 105 and 115 call
script.$script.load()andscript.$script.remove(). While the conditions checkscript.$script.status, if$scriptis undefined or these methods don't exist, runtime errors could occur. The context snippet shows$scriptis typed asanyand 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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (11)
devtools/bootstrap.tsdevtools/components/NetworkWaterfall.vuedevtools/components/ScriptStatus.vuedevtools/composables/state.tsdevtools/package.jsondevtools/pages/first-party.vuedevtools/pages/index.vuedevtools/pages/registry.vuedevtools/utils/formatting.tspnpm-workspace.yamlsrc/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
❓ Type of change
📚 Description
Renames the
client/directory todevtools/for clarity, since it contains the DevTools UI panel code. Updates all references: pnpm workspace, npm scripts (client:build→devtools:build,client:dev→devtools: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.