Skip to content

Comments

fix(lazy-load): prefer cloning component over wrapping component#232

Merged
huang-julien merged 5 commits intomainfrom
fix/remove-lazytracker-wrapper
Feb 22, 2026
Merged

fix(lazy-load): prefer cloning component over wrapping component#232
huang-julien merged 5 commits intomainfrom
fix/remove-lazytracker-wrapper

Conversation

@huang-julien
Copy link
Member

🔗 Linked issue

fix #231

❓ Type of change

  • 📖 Documentation (updates to the documentation or readme)
  • 🐞 Bug fix (a non-breaking change that fixes an issue)
  • 👌 Enhancement (improving an existing functionality)
  • ✨ New feature (a non-breaking change that adds functionality)
  • 🧹 Chore (updates to the build process or auxiliary tools and libraries)
  • ⚠️ Breaking change (fix or feature that would cause existing functionality to change)

📚 Description

This PR makes __wrapImportedComponent clone the component instead of using a wrapper component

@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 20, 2026

Open in StackBlitz

npm i https://pkg.pr.new/nuxt/hints/@nuxt/hints@232

commit: 3231313

@coderabbitai
Copy link

coderabbitai bot commented Feb 20, 2026

📝 Walkthrough

Walkthrough

Imports in the lazy-load composables were adjusted to use a type-only import from Vue. Two helpers, __wrapMainComponent and __wrapImportedComponent, received documentation and now augment components' setup functions in-place: __wrapMainComponent calls useLazyComponentTracking(imports) before invoking the original setup; __wrapImportedComponent tracks usage, ensures state.directImports entries exist and marks components as rendered before invoking the original setup (or returning undefined if none). No public API signatures changed. A new comprehensive test suite for these composables was added and the runtime test glob was broadened to include nested runtime tests.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: modifying __wrapImportedComponent to clone components instead of wrapping them.
Description check ✅ Passed The description clearly states the PR intent to fix issue #231 by cloning components instead of wrapping them, with appropriate linking.
Linked Issues check ✅ Passed The PR changes implement the fix for issue #231 by modifying __wrapImportedComponent to clone components instead of using a wrapper, preserving original component instances and exposed properties.
Out of Scope Changes check ✅ Passed All changes are scoped to implementing the lazy-load component cloning fix and supporting test infrastructure without unrelated modifications.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/remove-lazytracker-wrapper

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.

Caution

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

⚠️ Outside diff range comments (2)
src/runtime/lazy-load/composables.ts (2)

1-1: ⚠️ Potential issue | 🟡 Minor

Remove unused defineComponent and h imports — CI failure.

Both defineComponent and h were only used in the old wrapper-component approach. Neither is referenced anywhere in the file after this change.

🔧 Proposed fix
-import { defineComponent, h, type DefineComponent } from 'vue'
+import { type DefineComponent } from 'vue'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/runtime/lazy-load/composables.ts` at line 1, The file imports unused
symbols defineComponent and h which cause CI failures; update the import
statement in src/runtime/lazy-load/composables.ts to remove defineComponent and
h and only keep the used symbol(s) (e.g., type DefineComponent) so the import
becomes minimal and the linter/CI will pass.

55-77: ⚠️ Potential issue | 🟠 Major

In-place mutation of component.setup on a shared module singleton risks double-wrapping.

Vue's defineComponent returns the same object at runtime. In ESM, the component definition is a module-level singleton, so if two parent SFCs each call __wrapImportedComponent on the same ChildComp:

  1. Second call captures the already-wrapped setup as originalSetup, nesting the setup chain.
  2. Each render invokes useLazyComponentTracking() twice.
  3. The importedBy registered in directImports reflects whichever parent's module was evaluated last — non-deterministic.

The PR title says "prefer cloning" but the implementation mutates in place. Shallow-cloning the component before patching setup both prevents the double-wrap and matches the stated intent:

🛠️ Proposed fix — clone before patching
-  const originalSetup = component.setup
-
-  component.setup = (props, ctx) => {
+  const cloned: DefineComponent = { ...component }
+  const originalSetup = cloned.setup
+
+  cloned.setup = (props, ctx) => {
       const state = useLazyComponentTracking()
       if (state) {
         if (!state.directImports.has(componentName)) {
           state.directImports.set(componentName, {
             componentName,
             importSource,
             importedBy,
             rendered: false,
           })
         }

         const info = state.directImports.get(componentName)
         if (info) {
           info.rendered = true
         }
       }
     return originalSetup ? originalSetup(props, ctx) : undefined
   }

-  return component
+  return cloned
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/runtime/lazy-load/composables.ts` around lines 55 - 77, The current code
mutates component.setup in place which can double-wrap due to module singletons;
instead, shallow-clone the component object before patching so each call to
__wrapImportedComponent operates on a fresh object. Create a shallow copy of the
incoming component (copy properties like setup, name, etc.), capture
originalSetup from the clone, then assign the wrapped setup on the clone
(leaving the original module export untouched), and return the cloned component;
keep using useLazyComponentTracking(), componentName, importSource and
importedBy as before so directImports stays consistent while avoiding nested
setups and non-deterministic importedBy updates.
🧹 Nitpick comments (2)
test/runtime/lazy-load/composables.test.ts (1)

116-184: Well-structured coverage overall; consider adding a double-wrap test.

The existing cases are correct and thorough. If the composables fix adopts the shallow-clone approach (flagged in composables.ts), a regression test guarding that the original component's setup is not mutated and that a second call for a different importedBy returns an independent clone would be valuable:

it('should not mutate the original component when wrapping', () => {
  const comp = makeComponent()
  const originalSetup = comp.setup

  __wrapImportedComponent(comp, 'MyComp', './MyComp.vue', 'ParentA.vue')

  // Original component's setup must remain unchanged
  expect(comp.setup).toBe(originalSetup)
})

it('should produce independent clones for different importedBy callers', () => {
  const comp = makeComponent()
  const wrappedA = __wrapImportedComponent(comp, 'MyComp', './MyComp.vue', 'ParentA.vue')
  const wrappedB = __wrapImportedComponent(comp, 'MyComp', './MyComp.vue', 'ParentB.vue')

  expect(wrappedA).not.toBe(wrappedB)
  expect(wrappedA.setup).not.toBe(wrappedB.setup)
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/runtime/lazy-load/composables.test.ts` around lines 116 - 184, Add two
tests to ensure __wrapImportedComponent does not mutate the original component
and returns independent clones per importer: 1) verify that calling
__wrapImportedComponent(comp, 'MyComp', './MyComp.vue', 'ParentA.vue') leaves
comp.setup unchanged (compare reference to originalSetup), and 2) call
__wrapImportedComponent twice with different importedBy values (e.g.,
'ParentA.vue' and 'ParentB.vue') and assert the returned components are not the
same object and their setup functions are different references; reference the
__wrapImportedComponent function and the component produced by makeComponent in
your assertions.
src/runtime/lazy-load/composables.ts (1)

57-75: Two optional cleanups in the setup override.

  1. Redundant if (state) guard (line 59) — useLazyComponentTracking() always initialises and returns the state object; it never returns a falsy value, so this branch can never be false.
  2. Inconsistent indentation — lines 58–73 are indented by 6 spaces while line 74 uses 4 spaces.
♻️ Proposed cleanup
   component.setup = (props, ctx) => {
-      const state = useLazyComponentTracking()
-      if (state) {
-        if (!state.directImports.has(componentName)) {
-          state.directImports.set(componentName, {
-            componentName,
-            importSource,
-            importedBy,
-            rendered: false,
-          })
-        }
-
-        const info = state.directImports.get(componentName)
-        if (info) {
-          info.rendered = true
-        }
-      }
-    return originalSetup ? originalSetup(props, ctx) : undefined
-  }
+    const state = useLazyComponentTracking()
+    if (!state.directImports.has(componentName)) {
+      state.directImports.set(componentName, {
+        componentName,
+        importSource,
+        importedBy,
+        rendered: false,
+      })
+    }
+    const info = state.directImports.get(componentName)
+    if (info) {
+      info.rendered = true
+    }
+    return originalSetup ? originalSetup(props, ctx) : undefined
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/runtime/lazy-load/composables.ts` around lines 57 - 75, Remove the
unnecessary runtime guard around the setup override: call
useLazyComponentTracking() and operate on the returned state directly (delete
the "if (state) { ... }" block) because useLazyComponentTracking() always
returns a valid object; keep the logic that checks and sets state.directImports
(componentName, importSource, importedBy, rendered) and update info.rendered,
then return originalSetup(props, ctx) as before. Also normalize indentation in
the component.setup override so all lines inside the function use consistent
spacing. Ensure you reference the existing symbols component.setup,
useLazyComponentTracking, state.directImports, componentName, importSource,
importedBy, info, and originalSetup when making the edits.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/runtime/lazy-load/composables.ts`:
- Line 1: The file imports unused symbols defineComponent and h which cause CI
failures; update the import statement in src/runtime/lazy-load/composables.ts to
remove defineComponent and h and only keep the used symbol(s) (e.g., type
DefineComponent) so the import becomes minimal and the linter/CI will pass.
- Around line 55-77: The current code mutates component.setup in place which can
double-wrap due to module singletons; instead, shallow-clone the component
object before patching so each call to __wrapImportedComponent operates on a
fresh object. Create a shallow copy of the incoming component (copy properties
like setup, name, etc.), capture originalSetup from the clone, then assign the
wrapped setup on the clone (leaving the original module export untouched), and
return the cloned component; keep using useLazyComponentTracking(),
componentName, importSource and importedBy as before so directImports stays
consistent while avoiding nested setups and non-deterministic importedBy
updates.

---

Nitpick comments:
In `@src/runtime/lazy-load/composables.ts`:
- Around line 57-75: Remove the unnecessary runtime guard around the setup
override: call useLazyComponentTracking() and operate on the returned state
directly (delete the "if (state) { ... }" block) because
useLazyComponentTracking() always returns a valid object; keep the logic that
checks and sets state.directImports (componentName, importSource, importedBy,
rendered) and update info.rendered, then return originalSetup(props, ctx) as
before. Also normalize indentation in the component.setup override so all lines
inside the function use consistent spacing. Ensure you reference the existing
symbols component.setup, useLazyComponentTracking, state.directImports,
componentName, importSource, importedBy, info, and originalSetup when making the
edits.

In `@test/runtime/lazy-load/composables.test.ts`:
- Around line 116-184: Add two tests to ensure __wrapImportedComponent does not
mutate the original component and returns independent clones per importer: 1)
verify that calling __wrapImportedComponent(comp, 'MyComp', './MyComp.vue',
'ParentA.vue') leaves comp.setup unchanged (compare reference to originalSetup),
and 2) call __wrapImportedComponent twice with different importedBy values
(e.g., 'ParentA.vue' and 'ParentB.vue') and assert the returned components are
not the same object and their setup functions are different references;
reference the __wrapImportedComponent function and the component produced by
makeComponent in your assertions.

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: 2

Caution

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

⚠️ Outside diff range comments (1)
src/runtime/lazy-load/composables.ts (1)

1-1: ⚠️ Potential issue | 🟠 Major

Remove unused defineComponent and h imports — CI is failing.

Neither defineComponent nor h is referenced anywhere in this file's implementation. The pipeline confirms defineComponent triggers a lint error; h is equally unused.

🔧 Proposed fix
-import { defineComponent, h, type DefineComponent } from 'vue'
+import type { DefineComponent } from 'vue'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/runtime/lazy-load/composables.ts` at line 1, The import statement in
composables.ts unnecessarily includes defineComponent and h which are not used
and cause CI/lint failures; update the import to only import the needed
symbol(s) (keep type DefineComponent) by removing defineComponent and h from the
import list (or split runtime vs type imports so only used identifiers remain).
🧹 Nitpick comments (4)
test/runtime/lazy-load/composables.test.ts (2)

81-114: Consider adding a double-wrap guard test.

Neither __wrapMainComponent nor __wrapImportedComponent is tested when called twice on the same component object. Given the in-place mutation design, a second call would stack useLazyComponentTracking calls (captured in originalSetup). A test asserting idempotent behaviour (or documenting the expected stacking) would prevent accidental regressions.

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

In `@test/runtime/lazy-load/composables.test.ts` around lines 81 - 114, Add a test
that calls __wrapMainComponent (and similarly __wrapImportedComponent) twice on
the same component and asserts idempotent behavior: call
__wrapMainComponent(comp, imports) twice, invoke wrapped.setup and ensure
originalSetup/useLazyComponentTracking is not stacked (e.g. the returned setup
value is the same as single-wrap case and the
nuxtApp.payload._lazyHydrationState.directImports only contains one entry for
the component), and include a similar case for components without an original
setup to ensure repeated wrapping returns undefined; reference
__wrapMainComponent, __wrapImportedComponent, originalSetup and
useLazyComponentTracking in the test to locate the wrapping logic.

90-90: Replace repeated as any casts with a shared typed mock — CI is failing.

The same inline context mock with as any appears ~8 times. Extracting it as a typed constant fixes the lint failures and removes duplication.

♻️ Proposed fix — add a shared `mockCtx` at the top of the test file
+const mockCtx = { attrs: {}, slots: {}, emit: () => {}, expose: () => {} } as unknown as SetupContext

 // Then replace every occurrence, e.g.:
-wrapped.setup!({}, { attrs: {}, slots: {}, emit: () => {}, expose: () => {} } as any)
+wrapped.setup!({}, mockCtx)

Also applies to: 103-103, 111-111, 121-121, 142-142, 164-164, 173-173, 181-181

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

In `@test/runtime/lazy-load/composables.test.ts` at line 90, Create a shared typed
mock context constant (e.g. const mockCtx = { attrs: {}, slots: {}, emit: () =>
{}, expose: () => {} } as any) at the top of the test file and replace every
inline cast instance like wrapped.setup!({}, { attrs: {}, slots: {}, emit: () =>
{}, expose: () => {} } as any) with wrapped.setup!({}, mockCtx); update all
calls referencing the inline mock (the occurrences around wrapped.setup!) so the
repeated `as any` casts are removed and the single `mockCtx` is reused.
src/runtime/lazy-load/composables.ts (2)

27-38: In-place mutation doesn't match the PR's stated "clone" semantics — consider a shallow copy.

Both helpers directly assign component.setup, permanently mutating the shared component definition object. If either helper is called more than once for the same component reference (HMR, test reuse, future refactors), originalSetup captures the already-augmented setup and useLazyComponentTracking fires multiple times per mount. A shallow clone isolates the mutation:

♻️ Proposed fix for `__wrapMainComponent`
 export function __wrapMainComponent(
   component: DefineComponent,
   imports: DirectImportInfo[] = [],
 ): DefineComponent {
+  component = { ...component }
   const originalSetup = component.setup

   component.setup = (props, ctx) => {
     useLazyComponentTracking(imports)
     return originalSetup ? originalSetup(props, ctx) : undefined
   }

   return component
 }

Apply the same pattern to __wrapImportedComponent (Line 55 onwards):

♻️ Proposed fix for `__wrapImportedComponent`
   const originalSetup = component.setup
+  component = { ...component }

   component.setup = (props, ctx) => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/runtime/lazy-load/composables.ts` around lines 27 - 38, The helpers
mutate the incoming component by reassigning component.setup which causes
double-wrapping on repeated calls; instead make a shallow clone of the component
object, capture the original setup from the clone (e.g. const wrapped = {
...component } as DefineComponent; const originalSetup = wrapped.setup), assign
wrapped.setup = (props, ctx) => { useLazyComponentTracking(imports); return
originalSetup ? originalSetup(props, ctx) : undefined }, and return the cloned
wrapped component; apply the same shallow-clone-and-wrap approach to
__wrapImportedComponent so you never mutate the original component reference and
avoid multiple tracking invocations.

58-73: if (state) is always truthy — optional cleanup.

useLazyComponentTracking() always initialises and returns state; the null-guard at Line 59 is never falsy. Removing it reduces indentation and makes the invariant explicit.

♻️ Proposed simplification
   component.setup = (props, ctx) => {
     const state = useLazyComponentTracking()
-    if (state) {
-      if (!state.directImports.has(componentName)) {
-        state.directImports.set(componentName, {
-          componentName,
-          importSource,
-          importedBy,
-          rendered: false,
-        })
-      }
-
-      const info = state.directImports.get(componentName)
-      if (info) {
-        info.rendered = true
-      }
+    if (!state.directImports.has(componentName)) {
+      state.directImports.set(componentName, {
+        componentName,
+        importSource,
+        importedBy,
+        rendered: false,
+      })
     }
+    const info = state.directImports.get(componentName)
+    if (info) {
+      info.rendered = true
+    }
     return originalSetup ? originalSetup(props, ctx) : undefined
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/runtime/lazy-load/composables.ts` around lines 58 - 73, The null-guard
around useLazyComponentTracking() is redundant because useLazyComponentTracking
always returns an initialized state; remove the if (state) check and operate on
state directly: call const state = useLazyComponentTracking(); then check/insert
into state.directImports using componentName/importSource/importedBy and set
info.rendered = true—optionally add a brief comment or a non-null assertion to
make the invariant explicit around state and state.directImports (referencing
useLazyComponentTracking, state, directImports, componentName, importSource,
importedBy, and info).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/runtime/lazy-load/composables.test.ts`:
- Line 108: There are trailing spaces after the line that defines comp using
defineComponent (the line containing "const comp = defineComponent({ render: ()
=> h('div') }) as unknown as DefineComponent"); remove the trailing whitespace
at the end of that line so the file contains no trailing spaces (ensure the
defineComponent line ends immediately after the semicolon) to satisfy CI.
- Line 9: The import line in composables.test.ts includes unused symbols
Component and DefineSetupFnComponent; remove Component and
DefineSetupFnComponent from the import list (keep defineComponent, h,
DefineComponent, SetupContext) so the import becomes only the actually used
symbols and the linter/CI will stop failing; update the import statement that
declares defineComponent, h, type Component, type DefineComponent, type
DefineSetupFnComponent, type SetupContext by deleting the unused type Component
and type DefineSetupFnComponent entries.

---

Outside diff comments:
In `@src/runtime/lazy-load/composables.ts`:
- Line 1: The import statement in composables.ts unnecessarily includes
defineComponent and h which are not used and cause CI/lint failures; update the
import to only import the needed symbol(s) (keep type DefineComponent) by
removing defineComponent and h from the import list (or split runtime vs type
imports so only used identifiers remain).

---

Nitpick comments:
In `@src/runtime/lazy-load/composables.ts`:
- Around line 27-38: The helpers mutate the incoming component by reassigning
component.setup which causes double-wrapping on repeated calls; instead make a
shallow clone of the component object, capture the original setup from the clone
(e.g. const wrapped = { ...component } as DefineComponent; const originalSetup =
wrapped.setup), assign wrapped.setup = (props, ctx) => {
useLazyComponentTracking(imports); return originalSetup ? originalSetup(props,
ctx) : undefined }, and return the cloned wrapped component; apply the same
shallow-clone-and-wrap approach to __wrapImportedComponent so you never mutate
the original component reference and avoid multiple tracking invocations.
- Around line 58-73: The null-guard around useLazyComponentTracking() is
redundant because useLazyComponentTracking always returns an initialized state;
remove the if (state) check and operate on state directly: call const state =
useLazyComponentTracking(); then check/insert into state.directImports using
componentName/importSource/importedBy and set info.rendered = true—optionally
add a brief comment or a non-null assertion to make the invariant explicit
around state and state.directImports (referencing useLazyComponentTracking,
state, directImports, componentName, importSource, importedBy, and info).

In `@test/runtime/lazy-load/composables.test.ts`:
- Around line 81-114: Add a test that calls __wrapMainComponent (and similarly
__wrapImportedComponent) twice on the same component and asserts idempotent
behavior: call __wrapMainComponent(comp, imports) twice, invoke wrapped.setup
and ensure originalSetup/useLazyComponentTracking is not stacked (e.g. the
returned setup value is the same as single-wrap case and the
nuxtApp.payload._lazyHydrationState.directImports only contains one entry for
the component), and include a similar case for components without an original
setup to ensure repeated wrapping returns undefined; reference
__wrapMainComponent, __wrapImportedComponent, originalSetup and
useLazyComponentTracking in the test to locate the wrapping logic.
- Line 90: Create a shared typed mock context constant (e.g. const mockCtx = {
attrs: {}, slots: {}, emit: () => {}, expose: () => {} } as any) at the top of
the test file and replace every inline cast instance like wrapped.setup!({}, {
attrs: {}, slots: {}, emit: () => {}, expose: () => {} } as any) with
wrapped.setup!({}, mockCtx); update all calls referencing the inline mock (the
occurrences around wrapped.setup!) so the repeated `as any` casts are removed
and the single `mockCtx` is reused.

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: 1

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

Inline comments:
In `@test/runtime/lazy-load/composables.test.ts`:
- Line 90: The tests use unsafe inline casts to any for SetupContext when
calling wrapped.setup — create a single typed mock context constant (e.g., const
mockSetupContext: SetupContext = { attrs: {}, slots: {}, emit: () => {}, expose:
() => {} }) after makeComponent and reuse it instead of casting to any; then
replace the inline casts at wrapped.setup! calls (lines referencing
wrapped.setup) across the file (including the instances at lines 90, 103, 111,
121, 142, 164, 181) to pass mockSetupContext so lint errors are removed and
typing is preserved.

---

Duplicate comments:
In `@test/runtime/lazy-load/composables.test.ts`:
- Line 9: The import line imports unused symbols Component and
DefineSetupFnComponent which are flagged by the linter; remove Component and
DefineSetupFnComponent from the import list while keeping the existing used
imports (e.g., defineComponent, h, DefineComponent, SetupContext) so the file
(composables.test.ts) no longer imports the unused identifiers and CI/linting
will pass.


const wrapped = __wrapMainComponent(comp, imports)
// Invoke setup to trigger registration
wrapped.setup!({}, { attrs: {}, slots: {}, emit: () => {}, expose: () => {} } as any)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Replace as any with a typed mock context — CI is failing on all 6 reported sites (and line 181 follows the same pattern).

SetupContext is already imported. Define a shared constant once and reuse it, eliminating both the any lint errors and the verbosity.

🔧 Proposed fix — add a shared mock context constant and apply it everywhere

Add after line 16 (after makeComponent):

+const mockCtx = {
+  attrs: {},
+  slots: {},
+  emit: () => {},
+  expose: () => {},
+} as unknown as SetupContext

Then replace every inline cast across the file:

-wrapped.setup!({}, { attrs: {}, slots: {}, emit: () => {}, expose: () => {} } as any)
+wrapped.setup!({}, mockCtx)
-const result = wrapped.setup!({}, { attrs: {}, slots: {}, emit: () => {}, expose: () => {} } as any)
+const result = wrapped.setup!({}, mockCtx)

Apply the same substitution on lines 103, 111, 121, 142, 164, and 181.

Also applies to: 103-103, 111-111, 121-121, 142-142, 164-164, 181-181

🧰 Tools
🪛 GitHub Check: ci

[failure] 90-90:
Unexpected any. Specify a different type

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

In `@test/runtime/lazy-load/composables.test.ts` at line 90, The tests use unsafe
inline casts to any for SetupContext when calling wrapped.setup — create a
single typed mock context constant (e.g., const mockSetupContext: SetupContext =
{ attrs: {}, slots: {}, emit: () => {}, expose: () => {} }) after makeComponent
and reuse it instead of casting to any; then replace the inline casts at
wrapped.setup! calls (lines referencing wrapped.setup) across the file
(including the instances at lines 90, 103, 111, 121, 142, 164, 181) to pass
mockSetupContext so lint errors are removed and typing is preserved.

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.

Caution

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

⚠️ Outside diff range comments (1)
src/runtime/lazy-load/composables.ts (1)

33-36: ⚠️ Potential issue | 🟠 Major

Add idempotency guards to prevent double-wrapping of shared components.

Both __wrapMainComponent and __wrapImportedComponent mutate component.setup directly without checking if wrapping has already been applied. When a shared component (e.g., Button.vue) is imported by multiple pages, Vite's module system creates a single component singleton. The build transform generates separate wrapper calls for each importing file at module scope—if PageA.vue and PageB.vue both import Button, Button's setup gets wrapped twice, creating a chain. The first wrapper already calls the original setup, so the second wrapper wraps the wrapped version, causing:

  1. Redundant useLazyComponentTracking() calls on every render
  2. Incorrect importedBy attribution (always from the last-loaded page)

Add a module-level WeakSet guard to both functions:

🛡️ Proposed fix — WeakSet guard
+const _wrappedComponents = new WeakSet<object>()

 export function __wrapMainComponent(
   component: DefineComponent,
   imports: DirectImportInfo[] = [],
 ): DefineComponent {
+  if (_wrappedComponents.has(component)) {
+    return component
+  }
+  _wrappedComponents.add(component)
+
   const originalSetup = component.setup
   ...
 }

 export function __wrapImportedComponent(
   component: DefineComponent,
   componentName: string,
   importSource: string,
   importedBy: string,
 ) {
   if (component && component.name === 'AsyncComponentWrapper') {
     return component
   }
+  if (_wrappedComponents.has(component)) {
+    return component
+  }
+  _wrappedComponents.add(component)
+
   const originalSetup = component.setup
   ...
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/runtime/lazy-load/composables.ts` around lines 33 - 36, The component
wrapping functions __wrapMainComponent and __wrapImportedComponent currently
mutate component.setup without guarding against repeated wrapping; add a
module-level WeakSet (e.g., wrappedComponents) and have both functions check
wrappedComponents.has(component) before mutating and return early if true,
otherwise perform the existing wrap (calling useLazyComponentTracking(imports)
and delegating to originalSetup) and then add the component to wrappedComponents
to ensure idempotency and avoid double-wrapping and duplicated
tracking/importedBy attribution.
🧹 Nitpick comments (2)
test/runtime/lazy-load/composables.test.ts (1)

19-23: beforeEach only resets the payload state, not component object mutations.

This is fine because every test creates a new comp via makeComponent() or an inline defineComponent(...). If a future test were to hoist a component to the describe scope and reuse it across tests, the stacked setup wrappers from __wrapImportedComponent / __wrapMainComponent would carry over. Worth a brief comment to warn future contributors.

📝 Suggested comment
 beforeEach(() => {
   const nuxtApp = useNuxtApp()
   // Reset lazy hydration state between tests
   delete nuxtApp.payload._lazyHydrationState
+  // Note: __wrapMainComponent and __wrapImportedComponent mutate component.setup
+  // in-place; always create a fresh component instance per test to avoid stacking.
 })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/runtime/lazy-load/composables.test.ts` around lines 19 - 23, The
beforeEach currently only clears nuxtApp.payload._lazyHydrationState but doesn't
undo mutations applied to component objects by the wrapper utilities; add a
concise inline comment above the beforeEach explaining that tests create fresh
components via makeComponent() or defineComponent(...) so component wrappers
(__wrapImportedComponent and __wrapMainComponent) won't leak, and warn future
contributors that if they hoist a component into describe scope they must either
recreate/reset that component between tests or explicitly remove/restore
wrappers (or call a provided cleanup) to avoid stacked setup wrappers carrying
over across tests.
src/runtime/lazy-load/composables.ts (1)

57-75: if (state) is always truthy; consider simplifying the double Map lookup.

useLazyComponentTracking() always initialises _lazyHydrationState before returning it — it can never return a falsy value. The if (state) guard on line 59 is misleading (implies state could be absent) and adds unnecessary nesting.

Additionally, the has(componentName) + get(componentName) pattern on lines 60–71 is two lookups for the same key.

♻️ Proposed simplification
   component.setup = (props, ctx) => {
-    const state = useLazyComponentTracking()
-    if (state) {
-      if (!state.directImports.has(componentName)) {
-        state.directImports.set(componentName, {
-          componentName,
-          importSource,
-          importedBy,
-          rendered: false,
-        })
-      }
-
-      const info = state.directImports.get(componentName)
-      if (info) {
-        info.rendered = true
-      }
-    }
-    return originalSetup ? originalSetup(props, ctx) : undefined
+    const state = useLazyComponentTracking()
+    const info = state.directImports.get(componentName) ?? {
+      componentName,
+      importSource,
+      importedBy,
+      rendered: false,
+    }
+    state.directImports.set(componentName, info)
+    info.rendered = true
+    return originalSetup ? originalSetup(props, ctx) : undefined
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/runtime/lazy-load/composables.ts` around lines 57 - 75, The code
currently checks if (state) even though useLazyComponentTracking() always
returns a valid state and does a has(componentName) followed by
get(componentName) causing two Map lookups; remove the unnecessary if (state)
guard and simplify the directImports handling by doing a single lookup: let info
= state.directImports.get(componentName); if (!info) { info = { componentName,
importSource, importedBy, rendered: false };
state.directImports.set(componentName, info); } then set info.rendered = true
and finally return originalSetup ? originalSetup(props, ctx) : undefined —
update the logic inside component.setup accordingly (referencing
component.setup, useLazyComponentTracking, state.directImports, componentName,
importSource, importedBy, originalSetup).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/runtime/lazy-load/composables.ts`:
- Around line 33-36: The component wrapping functions __wrapMainComponent and
__wrapImportedComponent currently mutate component.setup without guarding
against repeated wrapping; add a module-level WeakSet (e.g., wrappedComponents)
and have both functions check wrappedComponents.has(component) before mutating
and return early if true, otherwise perform the existing wrap (calling
useLazyComponentTracking(imports) and delegating to originalSetup) and then add
the component to wrappedComponents to ensure idempotency and avoid
double-wrapping and duplicated tracking/importedBy attribution.

---

Nitpick comments:
In `@src/runtime/lazy-load/composables.ts`:
- Around line 57-75: The code currently checks if (state) even though
useLazyComponentTracking() always returns a valid state and does a
has(componentName) followed by get(componentName) causing two Map lookups;
remove the unnecessary if (state) guard and simplify the directImports handling
by doing a single lookup: let info = state.directImports.get(componentName); if
(!info) { info = { componentName, importSource, importedBy, rendered: false };
state.directImports.set(componentName, info); } then set info.rendered = true
and finally return originalSetup ? originalSetup(props, ctx) : undefined —
update the logic inside component.setup accordingly (referencing
component.setup, useLazyComponentTracking, state.directImports, componentName,
importSource, importedBy, originalSetup).

In `@test/runtime/lazy-load/composables.test.ts`:
- Around line 19-23: The beforeEach currently only clears
nuxtApp.payload._lazyHydrationState but doesn't undo mutations applied to
component objects by the wrapper utilities; add a concise inline comment above
the beforeEach explaining that tests create fresh components via makeComponent()
or defineComponent(...) so component wrappers (__wrapImportedComponent and
__wrapMainComponent) won't leak, and warn future contributors that if they hoist
a component into describe scope they must either recreate/reset that component
between tests or explicitly remove/restore wrappers (or call a provided cleanup)
to avoid stacked setup wrappers carrying over across tests.

@huang-julien huang-julien merged commit 17ce552 into main Feb 22, 2026
7 checks passed
@huang-julien huang-julien deleted the fix/remove-lazytracker-wrapper branch February 22, 2026 11:37
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.

LazyTracker wrapper breaks defineExpose / useTemplateRef on wrapped components

1 participant