Skip to content

fix(devtools): correct options cache guard and stop mutating defaults - #1018

Merged
antfu merged 3 commits into
nuxt:mainfrom
antfubot:fix/004-options-rpc-cache
Jul 14, 2026
Merged

fix(devtools): correct options cache guard and stop mutating defaults#1018
antfu merged 3 commits into
nuxt:mainfrom
antfubot:fix/004-options-rpc-cache

Conversation

@antfubot

Copy link
Copy Markdown
Collaborator

Summary

getOptions(tab) in the options RPC had two bugs:

  1. Inverted cache guard — the check if (!options || options[tab]) re-ran the disk read (existsSync + readFile + JSON.parse) on every call once a tab was already populated, instead of only when it wasn't. The in-memory cache was effectively never used, and every settings read/write paid the disk-read cost.
  2. Shared-constant aliasingoptions = defaultTabOptions aliased the imported module-level constant rather than cloning it, so read() mutated defaultTabOptions in place. Per-project user settings then permanently overwrote the process-global defaults, and clearOptions() could no longer restore pristine values.

Changes

  • Clone defaultTabOptions via structuredClone on first use instead of aliasing it.
  • Track which tabs have been read from disk in a Set so each tab is loaded from disk at most once, fixing the inverted guard without breaking tabs whose persisted value is falsy.
  • Added packages/devtools/test/default-tab-options.test.ts to pin the "clone before mutate" contract on defaultTabOptions.

Verification

  • pnpm test:unit — exit 0, all tests pass (including new test).
  • pnpm lint — exit 0.
  • Confirmed no new typecheck errors from this change (pre-existing unrelated vue-tsc errors remain in other files).
  • Only packages/devtools/src/server-rpc/options.ts and the new test file were modified; packages/devtools/src/constant.ts untouched.

This PR was created with the help of an agent.

getOptions(tab) had an inverted cache guard (re-reading from disk on
every call once a tab was populated) and aliased the shared
defaultTabOptions constant, letting per-project settings permanently
overwrite it. Clone the defaults on first use and only read a tab from
disk once, tracked via a Set.

Removes plans/004-fix-options-rpc-cache.md now that it's implemented.
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The options system now creates fresh default option graphs through a factory, supports configurable server-task defaults, and updates module and client consumers. The options RPC tracks per-tab reads with a Set while loading local options from fresh defaults. New tests cover object isolation, RPC mutation safety, and server-task default propagation. Plan 004 is marked DONE.

Estimated code review effort: 3 (Moderate) | ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main fix: correcting the options cache guard and avoiding mutation of shared defaults.
Description check ✅ Passed The description is directly about the bugs and fixes in this PR, so it is clearly related to the changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

packages/devtools/client/composables/storage-options.ts

ESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox.

packages/devtools/src/constant.ts

ESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.

packages/devtools/src/module-main.ts

ESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.

  • 2 others

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/devtools/src/server-rpc/options.ts`:
- Around line 23-28: Update the read function to pass the already-deeply-cloned
options![tab] value as the defaults argument to readLocalOptions instead of
defaultTabOptions[tab]. Preserve the existing root, key, assignment, and
hasReadOnce behavior while ensuring nested option data remains isolated from the
shared defaults.

In `@packages/devtools/test/default-tab-options.test.ts`:
- Around line 1-13: Rewrite the test around setupOptionsRPC so it invokes the
actual options RPC method rather than directly calling structuredClone on
defaultTabOptions. Obtain the returned options, mutate a nested reference such
as an array, and assert that defaultTabOptions remains unchanged; replace the
scale mutation and update the test setup to use the RPC’s required inputs and
dependencies.
🪄 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: b27707b1-c132-497d-bc39-19e2f81f480b

📥 Commits

Reviewing files that changed from the base of the PR and between 1a1b419 and 9fe08fa.

📒 Files selected for processing (4)
  • packages/devtools/src/server-rpc/options.ts
  • packages/devtools/test/default-tab-options.test.ts
  • plans/004-fix-options-rpc-cache.md
  • plans/README.md
💤 Files with no reviewable changes (1)
  • plans/004-fix-options-rpc-cache.md

Comment thread packages/devtools/src/server-rpc/options.ts
Comment thread packages/devtools/test/default-tab-options.test.ts
antfubot added 2 commits July 14, 2026 05:37
Passing defaultTabOptions[tab] into readLocalOptions defeated the
structuredClone in getOptions, since readLocalOptions only shallow-copies
its defaults argument — nested objects/arrays (e.g. ui.hiddenTabs) stayed
aliased to the shared constant. Source the defaults from the already-cloned
options![tab] instead.

Also rewrite the regression test to exercise setupOptionsRPC directly and
assert isolation on a nested array, per review feedback.
…ions() factory

Replace the shared defaultTabOptions object with a factory function that
returns a brand-new object graph on every call, so no caller can alias or
leak mutations into shared state — the underlying class of bug fixed
earlier in this branch by cloning.

This also fixes a latent regression from that clone-based fix: the module
mutated defaultTabOptions.serverTasks.enabled directly from a nitro:config
hook to auto-enable server tasks when Nitro's experimental tasks feature
is on, but once the options cache stopped aliasing the shared object, that
mutation silently stopped propagating. serverTasks.enabled is now tracked
via an explicit setServerTasksEnabledByDefault() flag that the factory
reads fresh on every call (including inside options.ts's read(), so the
first real read of a tab always sees the latest dynamic defaults, not a
frozen initial snapshot).

Updates all call sites (options RPC, module-main settings template,
client storage-options composable) and the regression test.
@antfu
antfu merged commit f71b190 into nuxt:main Jul 14, 2026
4 of 7 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/devtools/src/module-main.ts`:
- Around line 125-129: Update the nitro:config hook to assign
serverTasksEnabledByDefault from the current config.experimental?.tasks value on
every pass, rather than only calling setServerTasksEnabledByDefault(true) when
enabled. Ensure later Nuxt instances reset the process-wide default when tasks
are not opted in.
🪄 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: bf6072a1-eb89-4f67-9d0b-21b5971f53ff

📥 Commits

Reviewing files that changed from the base of the PR and between 422f127 and ede9a0e.

📒 Files selected for processing (5)
  • packages/devtools/client/composables/storage-options.ts
  • packages/devtools/src/constant.ts
  • packages/devtools/src/module-main.ts
  • packages/devtools/src/server-rpc/options.ts
  • packages/devtools/test/default-tab-options.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/devtools/src/server-rpc/options.ts

Comment on lines 125 to 129
nuxt.hook('nitro:config', (config) => {
// Check user opted-in for tasks
if (config.experimental?.tasks)
defaultTabOptions.serverTasks.enabled = true
setServerTasksEnabledByDefault(true)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

set -euo pipefail

printf '\n== module-main outline ==\n'
ast-grep outline packages/devtools/src/module-main.ts --view expanded || true

printf '\n== constant outline ==\n'
ast-grep outline packages/devtools/src/constant.ts --view expanded || true

printf '\n== search setter/getter usages ==\n'
rg -n "setServerTasksEnabledByDefault|serverTasksEnabledByDefault" packages/devtools/src packages/devtools/test packages/devtools -g '!**/dist/**' || true

printf '\n== relevant slice of module-main.ts ==\n'
sed -n '100,155p' packages/devtools/src/module-main.ts

printf '\n== relevant slice of constant.ts ==\n'
sed -n '1,220p' packages/devtools/src/constant.ts

Repository: nuxt/devtools

Length of output: 7760


Reset serverTasksEnabledByDefault on every nitro:config pass.
This is process-wide module state, but the hook only ever sets it to true. A later Nuxt instance in the same process can inherit true from an earlier one and default serverTasks.enabled incorrectly. Set it from the current config instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/devtools/src/module-main.ts` around lines 125 - 129, Update the
nitro:config hook to assign serverTasksEnabledByDefault from the current
config.experimental?.tasks value on every pass, rather than only calling
setServerTasksEnabledByDefault(true) when enabled. Ensure later Nuxt instances
reset the process-wide default when tasks are not opted in.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants