Skip to content

Conversation

@KazariEX
Copy link
Member

🔗 Linked issue

📚 Description

See playground or the code below. This is just a workaround.

import { type DefineComponent, defineComponent, h } from "vue";

type LazyComponent1<T> = T & DefineComponent;

type LazyComponent2<T> = DefineComponent & T;

const Comp = defineComponent({
    __typeProps: {} as {
        onClick: (foo: string) => any;
    },
});

const Comp1 = {} as LazyComponent1<typeof Comp>;
h(Comp1, { onClick: (foo) => foo });
//                   ^? (parameter) foo: any

const Comp2 = {} as LazyComponent2<typeof Comp>;
h(Comp2, { onClick: (foo) => foo });
//                   ^? (parameter) foo: string

@KazariEX KazariEX requested a review from danielroe as a code owner August 23, 2025 10:59
@bolt-new-by-stackblitz
Copy link

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Aug 23, 2025

Open in StackBlitz

@nuxt/kit

npm i https://pkg.pr.new/@nuxt/kit@33046

nuxt

npm i https://pkg.pr.new/nuxt@33046

@nuxt/rspack-builder

npm i https://pkg.pr.new/@nuxt/rspack-builder@33046

@nuxt/schema

npm i https://pkg.pr.new/@nuxt/schema@33046

@nuxt/vite-builder

npm i https://pkg.pr.new/@nuxt/vite-builder@33046

@nuxt/webpack-builder

npm i https://pkg.pr.new/@nuxt/webpack-builder@33046

commit: 017cbc7

@coderabbitai
Copy link

coderabbitai bot commented Aug 23, 2025

Walkthrough

The pull request changes TypeScript type aliases in packages/nuxt/src/components/templates.ts by reordering intersections for two types: IslandComponent and LazyComponent so the DefineComponent part precedes & T. It exports these types via the #components module and adds/updates type-checking tests in test/fixtures/basic-types/app/app-types.ts to validate event prop typings for island and lazy components. No runtime behaviour changes are introduced.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Pre-merge checks (2 passed, 1 warning)

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title accurately describes the primary change: addressing TypeScript compatibility between lazy component types and Vue's h helper. It is concise and directly relates to the type signature updates in packages/nuxt/src/components/templates.ts described in the PR. The phrasing is specific enough for a reviewer scanning history to understand the main intent.
Description Check ✅ Passed The description includes a focused TypeScript example and a Vue playground link that directly demonstrate the h() typing issue this PR targets, so it is clearly related to the changeset. Although it does not list every modified file, it explains the problem and the workaround, meeting the lenient criteria. Therefore the description is relevant to the changeset.

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d625ffa and 0a4d888.

📒 Files selected for processing (2)
  • packages/nuxt/src/components/templates.ts (2 hunks)
  • test/fixtures/basic-types/app/app-types.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • test/fixtures/basic-types/app/app-types.ts
  • packages/nuxt/src/components/templates.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: code
  • GitHub Check: build
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

❤️ Share

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

Copy link

@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

🧹 Nitpick comments (1)
packages/nuxt/src/components/templates.ts (1)

135-135: Constrain T and document the rationale to prevent regressions

Recommend constraining T to DefineComponent (as done for IslandComponent) and adding a short comment explaining why the DefineComponent part must come first. This guards accidental misuse and prevents future “helpful” reorderings.

Apply this diff:

- type LazyComponent<T> = DefineComponent<HydrationStrategies, {}, {}, {}, {}, {}, {}, { hydrated: () => void }> & T
+ /**
+  * Keep DefineComponent on the left: improves Vue `h()`/JSX contextual typing so props/emits
+  * from the original component are preserved (see PR #33046).
+  */
+ type LazyComponent<T extends DefineComponent = DefineComponent> =
+   DefineComponent<HydrationStrategies, {}, {}, {}, {}, {}, {}, { hydrated: () => void }> & T
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5d78366 and 3aae24f.

📒 Files selected for processing (1)
  • packages/nuxt/src/components/templates.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Follow standard TypeScript conventions and best practices

Files:

  • packages/nuxt/src/components/templates.ts
🧠 Learnings (1)
📚 Learning: 2024-11-05T20:04:39.622Z
Learnt from: Tofandel
PR: nuxt/nuxt#26468
File: packages/nuxt/src/components/runtime/client-delayed-component.ts:62-62
Timestamp: 2024-11-05T20:04:39.622Z
Learning: In Vue components, when typing the `hydrate` prop for hydration strategies like `hydrateOnInteraction`, use `type: null as PropType<Arguments<typeof hydrateOnInteraction>[0]>` to get the type directly from Vue's hydration methods. Also, remember that `HTMLElementEventMap` is not a type but an object; use `keyof HTMLElementEventMap` when defining prop types for events.

Applied to files:

  • packages/nuxt/src/components/templates.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build
  • GitHub Check: code
🔇 Additional comments (1)
packages/nuxt/src/components/templates.ts (1)

135-135: Intersection order fix restores proper h() inference — LGTM

Switching to DefineComponent<…> & T fixes the widening of callback params (e.g. onClick’s argument no longer becomes any) when used with h(). This aligns with how TypeScript performs contextual typing over intersections for Vue component types.

@codspeed-hq
Copy link

codspeed-hq bot commented Aug 23, 2025

CodSpeed Performance Report

Merging #33046 will not alter performance

Comparing KazariEX:fix/lazy-component-h (017cbc7) with main (dc69e26)

Summary

✅ 10 untouched

@danielroe
Copy link
Member

@KazariEX would you be able to have a look at the type test? 🙏

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

🧹 Nitpick comments (1)
test/fixtures/basic-types/app/app-types.ts (1)

406-423: Harden lazy component event typing and (optionally) assert built-in emits.

  • Mirror the stricter checks so we catch regressions where foo becomes any.
  • Optional: since LazyComponent declares an emitted hydrated event, add a quick check for onHydrated props.

Apply these changes:

     h(LazyComp, {
       onClick: (foo) => {
-        foo satisfies string
+        // Ensure the parameter is strictly typed
+        expectTypeOf(foo).toEqualTypeOf<string>()
+        expectTypeOf(foo).not.toBeAny()
         return foo
       },
     })
+
+    // Optional: assert emits typing for hydrated event
+    h(LazyComp, { onHydrated: () => {} })
+    // @ts-expect-error: hydrated does not receive a payload
+    h(LazyComp, { onHydrated: (e: unknown) => {} })
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 3aae24f and d625ffa.

📒 Files selected for processing (2)
  • packages/nuxt/src/components/templates.ts (2 hunks)
  • test/fixtures/basic-types/app/app-types.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/nuxt/src/components/templates.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Follow standard TypeScript conventions and best practices

Files:

  • test/fixtures/basic-types/app/app-types.ts
🧠 Learnings (2)
📚 Learning: 2024-12-12T12:36:34.871Z
Learnt from: huang-julien
PR: nuxt/nuxt#29366
File: packages/nuxt/src/app/components/nuxt-root.vue:16-19
Timestamp: 2024-12-12T12:36:34.871Z
Learning: In `packages/nuxt/src/app/components/nuxt-root.vue`, when optimizing bundle size by conditionally importing components based on route metadata, prefer using inline conditional imports like:

```js
const IsolatedPage = route?.meta?.isolate ? defineAsyncComponent(() => import('#build/isolated-page.mjs')) : null
```

instead of wrapping the import in a computed property or importing the component unconditionally.

Applied to files:

  • test/fixtures/basic-types/app/app-types.ts
📚 Learning: 2024-11-05T20:04:39.622Z
Learnt from: Tofandel
PR: nuxt/nuxt#26468
File: packages/nuxt/src/components/runtime/client-delayed-component.ts:62-62
Timestamp: 2024-11-05T20:04:39.622Z
Learning: In Vue components, when typing the `hydrate` prop for hydration strategies like `hydrateOnInteraction`, use `type: null as PropType<Arguments<typeof hydrateOnInteraction>[0]>` to get the type directly from Vue's hydration methods. Also, remember that `HTMLElementEventMap` is not a type but an object; use `keyof HTMLElementEventMap` when defining prop types for events.

Applied to files:

  • test/fixtures/basic-types/app/app-types.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (20)
  • GitHub Check: test-fixtures (windows-latest, built, rspack, async, manifest-on, json, lts/-1)
  • GitHub Check: test-fixtures (windows-latest, built, webpack, default, manifest-on, json, lts/-1)
  • GitHub Check: test-fixtures (windows-latest, dev, vite, async, manifest-off, json, lts/-1)
  • GitHub Check: test-fixtures (windows-latest, built, vite, async, manifest-on, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, built, webpack, default, manifest-on, json, lts/-1)
  • GitHub Check: test-fixtures (windows-latest, dev, vite, default, manifest-off, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, built, rspack, async, manifest-on, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, built, vite, default, manifest-on, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, built, webpack, async, manifest-on, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, built, vite, async, manifest-on, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, dev, vite, async, manifest-on, js, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, dev, vite, async, manifest-on, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, dev, vite, default, manifest-off, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, built, vite, async, manifest-on, js, lts/-1)
  • GitHub Check: test-benchmark
  • GitHub Check: release-pkg-pr-new
  • GitHub Check: typecheck (windows-latest, bundler)
  • GitHub Check: typecheck (ubuntu-latest, bundler)
  • GitHub Check: test-size
  • GitHub Check: code
🔇 Additional comments (4)
test/fixtures/basic-types/app/app-types.ts (4)

15-15: Type-only import for public component types is correct.

Importing IslandComponent and LazyComponent via import type avoids any runtime impact and matches the intent of this test.


378-378: No review needed for whitespace-only change.


386-386: No review needed for whitespace-only change.


433-433: No review needed for whitespace-only change.

Comment on lines 387 to 404
it('correctly includes event types with island components', () => {
const Comp = defineComponent({
__typeProps: {
onClick: Function as PropType<(foo: string) => any>,
},
})
const IslandComp = Comp as unknown as IslandComponent<typeof Comp>
h(IslandComp, {
// @ts-expect-error: foo must be string, not number
onClick: (foo: number) => foo,
})
h(IslandComp, {
onClick: (foo) => {
foo satisfies string
return foo
},
})
})
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Import PropType and harden the assertion to fail if the callback param is inferred as any.

  • PropType is used but not imported in this file; TS will complain unless it’s globally available (unlikely).
  • foo satisfies string won’t fail if foo is any. Add explicit expectTypeOf checks to ensure foo is a string and not any.

Apply these changes:

  • Add the missing type import (outside the shown hunk):
- import type { Ref, SlotsType } from 'vue'
+ import type { Ref, SlotsType, PropType } from 'vue'
  • Strengthen the assertion in this block:
     h(IslandComp, {
       onClick: (foo) => {
-        foo satisfies string
+        // Ensure the parameter is strictly typed
+        expectTypeOf(foo).toEqualTypeOf<string>()
+        expectTypeOf(foo).not.toBeAny()
         return foo
       },
     })
📝 Committable suggestion

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

Suggested change
it('correctly includes event types with island components', () => {
const Comp = defineComponent({
__typeProps: {
onClick: Function as PropType<(foo: string) => any>,
},
})
const IslandComp = Comp as unknown as IslandComponent<typeof Comp>
h(IslandComp, {
// @ts-expect-error: foo must be string, not number
onClick: (foo: number) => foo,
})
h(IslandComp, {
onClick: (foo) => {
foo satisfies string
return foo
},
})
})
// file: test/fixtures/basic-types/app/app-types.ts
// ← existing imports
import type { Ref, SlotsType, PropType } from 'vue'
// …later, around line 387…
it('correctly includes event types with island components', () => {
const Comp = defineComponent({
__typeProps: {
onClick: Function as PropType<(foo: string) => any>,
},
})
const IslandComp = Comp as unknown as IslandComponent<typeof Comp>
h(IslandComp, {
// @ts-expect-error: foo must be string, not number
onClick: (foo: number) => foo,
})
h(IslandComp, {
onClick: (foo) => {
// Ensure the parameter is strictly typed
expectTypeOf(foo).toEqualTypeOf<string>()
expectTypeOf(foo).not.toBeAny()
return foo
},
})
})
🤖 Prompt for AI Agents
In test/fixtures/basic-types/app/app-types.ts around lines 387 to 404, PropType
is used but not imported and the runtime-free "foo satisfies string" check won't
fail if foo is inferred as any; add a top-of-file import for PropType (e.g.
import { PropType } from 'vue') and replace the inline "foo satisfies string"
assertion with explicit expectTypeOf checks such as
expectTypeOf(foo).toEqualTypeOf<string>() and expectTypeOf(foo).not.toBeAny(),
ensuring expectTypeOf is imported from your test-assertions helper (e.g.
vitest/expect-type) if not already.

@KazariEX
Copy link
Member Author

KazariEX commented Aug 24, 2025

It looks fine! The essence of the problem is that the h function only takes the last DefineComponent of the intersected types. So when T comes first, all of its props are overshadowed, and the event parameter types merely make this type issue explicitly visible.

@danielroe
Copy link
Member

I wonder why the typecheck is failing then. I'll look at it again when I'm back at the computer.

@danielroe
Copy link
Member

did you have any ideas why the type test fails?

@KazariEX
Copy link
Member Author

KazariEX commented Aug 27, 2025

No, I tested locally and there was no the same error reported as in CI.

Copy link
Member

@danielroe danielroe left a comment

Choose a reason for hiding this comment

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

thank you for persisting! ❤️

@danielroe danielroe merged commit 7b2ff48 into nuxt:main Sep 23, 2025
46 checks passed
@github-actions github-actions bot mentioned this pull request Sep 23, 2025
@KazariEX KazariEX deleted the fix/lazy-component-h branch September 23, 2025 09:57
@github-actions github-actions bot mentioned this pull request Oct 6, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants