Skip to content

Conversation

@antfu
Copy link
Member

@antfu antfu commented Oct 31, 2025

I think it's a common use case to install plugins lazily, imagine:

addVitePlugin(() => import('my-cool-plugin').then(r => r.default()))

The current workaround is:

const plugin = await import('my-cool-plugin').then(r => r.default)
addVitePlugin(() => plugin())

But this would defeat the point of the factory function to lazy load, as

const pluginVite = await import('my-cool-plugin/vite').then(r => r.default)
const pluginWebpack = await import('my-cool-plugin/webpack').then(r => r.default)
addVitePlugin(() => pluginVite())
addWebpackPlugin(() => pluginWebpack())

would result in loading unnecessary code.

This PR makes those utility support async usages.

Not sure if there are any performance concerns on this tho.

@antfu antfu requested a review from danielroe as a code owner October 31, 2025 06:50
@bolt-new-by-stackblitz
Copy link

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

@coderabbitai
Copy link

coderabbitai bot commented Oct 31, 2025

Walkthrough

This PR adds local type aliases Arrayable<T> and Thenable<T> and updates several build APIs in packages/kit/src/build.ts to accept and return Thenable values. Signatures and internal flows for extendViteConfig, extendWebpackCompatibleConfig, addVitePlugin, addWebpackPlugin, addRspackPlugin, and AddBuildPluginFactory were changed to support async plugin/config getters; internal hook registrations now await these getters.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20–30 minutes

  • Review focus:
    • Correct placement of await/async in hook registrations (vite:extend, vite:extendConfig, webpack/rspack extend hooks)
    • Verification of updated function signatures and propagated Thenable<Arrayable<...>> types
    • addBuildPlugin usage sites and propagation of new factory return types
    • Handling of arrayable plugin inputs and functions returning promises
    • Potential impacts on callers that relied on synchronous behaviour (ensure compatibility or documented change)

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarises the main change: adding async constructor support for plugin functions.
Description check ✅ Passed The description is directly related to the changeset, explaining the motivation for async factory functions and demonstrating the use case with examples.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/async-add-plugin

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.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Oct 31, 2025

Open in StackBlitz

@nuxt/kit

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

@nuxt/nitro-server

npm i https://pkg.pr.new/@nuxt/nitro-server@33619

nuxt

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

@nuxt/rspack-builder

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

@nuxt/schema

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

@nuxt/vite-builder

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

@nuxt/webpack-builder

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

commit: dc42229

@codspeed-hq
Copy link

codspeed-hq bot commented Oct 31, 2025

CodSpeed Performance Report

Merging #33619 will not alter performance

Comparing feat/async-add-plugin (dc42229) with main (7b83538)1

Summary

✅ 10 untouched

Footnotes

  1. No successful run was found on main (01d459e) during the generation of this report, so 7b83538 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

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.

👌

(need to investigate lint failure)

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

🧹 Nitpick comments (2)
packages/kit/src/build.ts (2)

9-10: Consider renaming Thenable<T> to avoid confusion.

In JavaScript/TypeScript terminology, "thenable" typically refers to objects implementing a .then() method (duck-typed promises), not a union of T | Promise<T>. Consider using MaybePromise<T> or Awaitable<T> for better clarity.

Apply this diff to improve the type name:

-type Thenable<T> = T | Promise<T>
+type MaybePromise<T> = T | Promise<T>

Then update all usages of Thenable to MaybePromise throughout the file.


157-209: Consider caching the plugin resolution result.

The plugin factory is invoked twice—at line 171 in the vite:extend hook and at line 200 in the vite:extendConfig hook. Whilst dynamic imports are cached by the module system, if the factory creates new plugin instances or has side effects, these will occur twice. Consider caching the result after the first resolution to ensure consistent behaviour and avoid unnecessary work.

 export function addVitePlugin (pluginOrGetter: Arrayable<VitePlugin> | (() => Thenable<Arrayable<VitePlugin>>), options: ExtendConfigOptions = {}): void {
   const nuxt = useNuxt()
 
   if (options.dev === false && nuxt.options.dev) {
     return
   }
   if (options.build === false && nuxt.options.build) {
     return
   }
 
+  let cachedPlugin: VitePlugin[] | undefined
+  async function getPlugin() {
+    if (!cachedPlugin) {
+      cachedPlugin = toArray(typeof pluginOrGetter === 'function' ? await pluginOrGetter() : pluginOrGetter)
+    }
+    return cachedPlugin
+  }
+
   let needsEnvInjection = false
   nuxt.hook('vite:extend', async ({ config }) => {
     config.plugins ||= []
 
-    const plugin = toArray(typeof pluginOrGetter === 'function' ? await pluginOrGetter() : pluginOrGetter)
+    const plugin = await getPlugin()
     if (options.server !== false && options.client !== false) {
       const method: 'push' | 'unshift' = options?.prepend ? 'unshift' : 'push'
       config.plugins[method](...plugin)
       return
     }
 
     if (!config.environments?.ssr || !config.environments.client) {
       needsEnvInjection = true
       return
     }
 
     const environmentName = options.server === false ? 'client' : 'ssr'
     const pluginName = plugin.map(p => p.name).join('|')
     config.plugins.push({
       name: `${pluginName}:wrapper`,
       enforce: options?.prepend ? 'pre' : 'post',
       applyToEnvironment (environment) {
         if (environment.name === environmentName) {
           return plugin
         }
       },
     })
   })
 
   nuxt.hook('vite:extendConfig', async (config, env) => {
     if (!needsEnvInjection) {
       return
     }
-    const plugin = toArray(typeof pluginOrGetter === 'function' ? await pluginOrGetter() : pluginOrGetter)
+    const plugin = await getPlugin()
     const method: 'push' | 'unshift' = options?.prepend ? 'unshift' : 'push'
     if (env.isClient && options.server === false) {
       config.plugins![method](...plugin)
     }
     if (env.isServer && options.client === false) {
       config.plugins![method](...plugin)
     }
   })
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8bcdbdc and dc42229.

📒 Files selected for processing (1)
  • packages/kit/src/build.ts (10 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx,vue}

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

Follow standard TypeScript conventions and best practices

Files:

  • packages/kit/src/build.ts
**/*.{ts,tsx,js,jsx,vue}

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

**/*.{ts,tsx,js,jsx,vue}: Use clear, descriptive variable and function names
Add comments only to explain complex logic or non-obvious implementations
Keep functions focused and manageable (generally under 50 lines), and extract complex logic into separate domain-specific files
Remove code that is not used or needed
Use error handling patterns consistently

Files:

  • packages/kit/src/build.ts
🧠 Learnings (1)
📚 Learning: 2024-11-28T21:22:40.496Z
Learnt from: GalacticHypernova
Repo: nuxt/nuxt PR: 29661
File: packages/kit/src/template.ts:227-229
Timestamp: 2024-11-28T21:22:40.496Z
Learning: In `packages/kit/src/template.ts`, when updating the `EXTENSION_RE` regular expression for TypeScript configuration, avoid using patterns like `(\.\w+)+$` as they can result in catastrophic backtracking.

Applied to files:

  • packages/kit/src/build.ts
🧬 Code graph analysis (1)
packages/kit/src/build.ts (3)
packages/webpack/builder.d.ts (1)
  • builder (10-10)
packages/kit/src/index.ts (9)
  • ExtendWebpackConfigOptions (23-23)
  • extendViteConfig (22-22)
  • ExtendViteConfigOptions (23-23)
  • addWebpackPlugin (22-22)
  • extendWebpackConfig (22-22)
  • addRspackPlugin (22-22)
  • extendRspackConfig (22-22)
  • addVitePlugin (22-22)
  • ExtendConfigOptions (23-23)
packages/kit/src/utils.ts (1)
  • toArray (2-4)
🔇 Additional comments (5)
packages/kit/src/build.ts (5)

61-85: LGTM!

The async/await pattern is correctly implemented for both server and client configuration mutations. The function properly awaits the callback result, enabling asynchronous plugin factories.


132-140: LGTM!

The async factory pattern is correctly implemented. The function properly awaits the plugin getter when it's a function, enabling lazy loading of plugins via dynamic imports.


144-152: LGTM!

The async factory pattern is correctly implemented and consistent with addWebpackPlugin. The function properly enables lazy loading of Rspack plugins.


211-229: LGTM!

The AddBuildPluginFactory interface correctly reflects the async factory capability, and the implementation properly delegates to the respective plugin-adding functions.


107-127: The implicit promise return is correct and properly awaited by the hook system.

The hook handler at line 126 correctly returns the promise from fn(config) implicitly. Nuxt's hook system awaits all returned promises (as confirmed in packages/vite/src/vite.ts line 262: await nuxt.callHook('vite:extend', ctx)), so there is no risk of race conditions.

However, there is a stylistic inconsistency: addVitePlugin() at line 168 uses an explicit async keyword for the same vite:extend hook, whilst extendViteConfig() returns the promise implicitly without async. For consistency within the file, consider using the same pattern—either explicit async or implicit promise return—across both functions.

@danielroe danielroe merged commit f48ea4c into main Dec 15, 2025
55 checks passed
@danielroe danielroe deleted the feat/async-add-plugin branch December 15, 2025 21:19
@github-actions github-actions bot mentioned this pull request Dec 15, 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.

3 participants