Skip to content

Conversation

@danielroe
Copy link
Member

@danielroe danielroe commented Sep 11, 2025

🔗 Linked issue

📚 Description

spotted while investing nuxt module hooks, this rewrites a lot of the logic in the components module to be more performant (preferring single loops rather than looping multiple times over the same array), as well as to move the components:dirs hook into modules:done. This should be non-breaking and much more performant but perhaps we can test this and revisit this as an experimental change if anyone on @nuxt/core has concerns.

I've added types to an any which highlighted a few edge cases we needed to handle, as well as some inaccurate types for the components option in the Nuxt config.

Additionally, I spotted we were sorting components (if an array) by length but not components.dirs (if an array). We're now treating both the same way.

@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 Sep 11, 2025

Open in StackBlitz

@nuxt/kit

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

nuxt

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

@nuxt/rspack-builder

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

@nuxt/schema

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

@nuxt/vite-builder

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

@nuxt/webpack-builder

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

commit: fb61c4b

@codspeed-hq
Copy link

codspeed-hq bot commented Sep 11, 2025

CodSpeed Performance Report

Merging #33207 will not alter performance

Comparing fix/norm-components (fb61c4b) with main (eb8d779)1

Summary

✅ 10 untouched

Footnotes

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

@danielroe danielroe marked this pull request as ready for review September 11, 2025 11:51
@danielroe
Copy link
Member Author

I'll move the change to when the hook is called into a separate PR so we can discuss properly.

@coderabbitai
Copy link

coderabbitai bot commented Sep 11, 2025

Walkthrough

  • Components module: added a normalizeDirs function; isPureObjectOrString changed to a type guard over unknown; setup parameter renamed from componentOptions to moduleOptions and generateMetadata is read from moduleOptions.
  • Directory resolution reworked to collect per-layer dirs, split into userComponentDirs and libraryComponentDirs (node_modules detection), resolve aliases, compute extensions, refine glob pattern generation, and update build.transpile handling. Missing non-default dirs now warn.
  • Schema: NuxtConfig now omits components from ConfigSchema and reintroduces components?: ConfigSchema['components'].

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60–90 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 succinctly captures the primary change — more efficient normalisation of components and directories in the Nuxt components module — and aligns with the raw_summary and PR objectives describing performance-focused rewrites. It is concise, specific to the affected area, and avoids extraneous details or noise.
Description Check ✅ Passed The description directly outlines the scope and intent of the changes (rewriting the components module for performance, type additions, sorting behaviour adjustments, and hook-related notes) and matches the raw_summary and pr_objectives, so it is on-topic and informative for reviewers. It provides useful context and rationale for the change without being off-topic or misleading.

Pre-merge checks (3 passed)

✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed The title accurately captures the primary change: improved, performance-focused normalisation of components and directories within Nuxt. It is concise, adheres to conventional commit style with scope "nuxt", and avoids noisy details while conveying the main intent. This makes it clear to reviewers scanning history what the PR is primarily about.
Description Check ✅ Passed The description is directly related to the changeset and summarises the key modifications: a performance-driven refactor of the components module, added typing that revealed edge cases, and alignment of sorting behaviour, while noting the hook timing change was moved to a separate PR. It provides useful context about intent, non-breaking expectations, and testing suggestions. Given the lenient criteria for this check, the description is acceptable.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/norm-components

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

🧹 Nitpick comments (4)
packages/nuxt/src/components/module.ts (4)

28-28: Windows path handling in regex.

DEFAULT_COMPONENTS_DIRS_RE only matches /; on Windows this can cause false warnings for default dirs.

Apply:

-const DEFAULT_COMPONENTS_DIRS_RE = /\/components(?:\/(?:global|islands))?$/
+const DEFAULT_COMPONENTS_DIRS_RE = /[\\/]+components(?:[\\/](?:global|islands))?$/

83-85: Deduplicate transpile entries.

Avoid pushing duplicate strings into build.transpile.

Apply:

-        if (transpile) {
-          nuxt.options.build.transpile.push(dirPath)
-        }
+        if (transpile && !nuxt.options.build.transpile.includes(dirPath)) {
+          nuxt.options.build.transpile.push(dirPath)
+        }

21-21: Type guard can narrow to ComponentsDir | string.

This improves downstream inference when accessing path on object cases.

Apply:

-const isPureObjectOrString = (val: unknown): val is object | string => (!Array.isArray(val) && typeof val === 'object') || typeof val === 'string'
+const isPureObjectOrString = (val: unknown): val is ComponentsDir | string =>
+  typeof val === 'string' || (!!val && !Array.isArray(val) && typeof val === 'object')

263-297: Normalisation looks sound; minor note on double alias resolution.

normalizeDirs resolves aliases and makes absolute paths, and later the loop resolves aliases again. It’s harmless, but you could skip the second resolution when dirOptions.path is already absolute for a tiny win.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4568e84 and e052878.

📒 Files selected for processing (2)
  • packages/nuxt/src/components/module.ts (5 hunks)
  • packages/schema/src/types/config.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/schema/src/types/config.ts
  • packages/nuxt/src/components/module.ts
🧠 Learnings (1)
📚 Learning: 2024-11-05T15:22:54.759Z
Learnt from: GalacticHypernova
PR: nuxt/nuxt#26468
File: packages/nuxt/src/components/plugins/loader.ts:24-24
Timestamp: 2024-11-05T15:22:54.759Z
Learning: In `packages/nuxt/src/components/plugins/loader.ts`, the references to `resolve` and `distDir` are legacy code from before Nuxt used the new unplugin VFS and will be removed.

Applied to files:

  • packages/nuxt/src/components/module.ts
🧬 Code graph analysis (1)
packages/nuxt/src/components/module.ts (5)
packages/kit/src/index.ts (3)
  • addBuildPlugin (20-20)
  • resolveAlias (32-32)
  • logger (36-36)
packages/kit/src/build.ts (1)
  • addBuildPlugin (153-165)
packages/nuxt/src/components/plugins/component-names.ts (1)
  • ComponentNamePlugin (16-68)
packages/kit/src/resolve.ts (1)
  • resolveAlias (91-94)
packages/vite/src/utils/transpile.ts (1)
  • transpile (11-28)
⏰ 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). (4)
  • GitHub Check: build
  • GitHub Check: codeql (actions)
  • GitHub Check: codeql (javascript-typescript)
  • GitHub Check: code
🔇 Additional comments (3)
packages/schema/src/types/config.ts (1)

49-50: Explicit components on NuxtConfig looks correct and improves type fidelity.

Re-introducing components?: ConfigSchema['components'] while omitting it from the DeepPartial base avoids accidental deep-partialing of the union and aligns with the schema. No further action from my side.

packages/nuxt/src/components/module.ts (2)

111-114: Ordering user before library dirs is a solid default.

This preserves intuitive resolution precedence. LGTM.


128-130: Switch to moduleOptions.generateMetadata is consistent.

Matches the module generic and top-level config changes. No issues spotted.

Comment on lines 92 to 109
const dirs = dir.path.includes('node_modules') ? libraryComponentDirs : userComponentDirs

dirs.push({
global: moduleOptions.global,
...dirOptions,
// TODO: https://github.com/nuxt/framework/pull/251
enabled: true,
path: dirPath,
extensions,
pattern: dirOptions.pattern || `**/*.{${extensions.join(',')},}`,
pattern: dirOptions.pattern || (extensions.length > 1 ? `**/*.{${extensions.join(',')}}` : `**/*.${extensions[0] || '*'}`),
ignore: [
'**/*{M,.m,-m}ixin.{js,ts,jsx,tsx}', // ignore mixins
'**/*.d.{cts,mts,ts}', // .d.ts files
...(dirOptions.ignore || []),
],
transpile: (transpile === 'auto' ? dirPath.includes('node_modules') : transpile),
}
}).filter(d => d.enabled)
transpile,
})
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Bug: using dir.path can crash when a hook adds string entries.

After components:dirs, allDirs may include strings. The guard wraps them into dirOptions, but Line 92 reads dir.path (possibly undefined). Use dirOptions.path.

Apply:

-        const dirs = dir.path.includes('node_modules') ? libraryComponentDirs : userComponentDirs
+        const dirs = dirOptions.path.includes('node_modules') ? libraryComponentDirs : userComponentDirs
📝 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
const dirs = dir.path.includes('node_modules') ? libraryComponentDirs : userComponentDirs
dirs.push({
global: moduleOptions.global,
...dirOptions,
// TODO: https://github.com/nuxt/framework/pull/251
enabled: true,
path: dirPath,
extensions,
pattern: dirOptions.pattern || `**/*.{${extensions.join(',')},}`,
pattern: dirOptions.pattern || (extensions.length > 1 ? `**/*.{${extensions.join(',')}}` : `**/*.${extensions[0] || '*'}`),
ignore: [
'**/*{M,.m,-m}ixin.{js,ts,jsx,tsx}', // ignore mixins
'**/*.d.{cts,mts,ts}', // .d.ts files
...(dirOptions.ignore || []),
],
transpile: (transpile === 'auto' ? dirPath.includes('node_modules') : transpile),
}
}).filter(d => d.enabled)
transpile,
})
}
const dirs = dirOptions.path.includes('node_modules') ? libraryComponentDirs : userComponentDirs
dirs.push({
global: moduleOptions.global,
...dirOptions,
// TODO: https://github.com/nuxt/framework/pull/251
enabled: true,
path: dirPath,
extensions,
pattern: dirOptions.pattern || (extensions.length > 1 ? `**/*.{${extensions.join(',')}}` : `**/*.${extensions[0] || '*'}`),
ignore: [
'**/*{M,.m,-m}ixin.{js,ts,jsx,tsx}', // ignore mixins
'**/*.d.{cts,mts,ts}', // .d.ts files
...(dirOptions.ignore || []),
],
transpile,
})
}
🤖 Prompt for AI Agents
In packages/nuxt/src/components/module.ts around lines 92 to 109, the code reads
dir.path which can be undefined when a hook adds string entries (allDirs may
include strings); update the logic to use dirOptions.path (the normalized
object) instead of dir.path when deciding between libraryComponentDirs and
userComponentDirs and when pushing the entry so you only access the validated
properties on dirOptions; ensure any other references in this block use
dirOptions rather than dir to avoid crashes from string entries.

@danielroe
Copy link
Member Author

@coderabbitai generate docstrings

@coderabbitai
Copy link

coderabbitai bot commented Sep 11, 2025

✅ Actions performed

Docstrings generation started.

coderabbitai bot added a commit that referenced this pull request Sep 11, 2025
Docstrings generation was requested by @danielroe.

* #33207 (comment)

The following files were modified:

* `packages/nuxt/src/components/module.ts`
@coderabbitai
Copy link

coderabbitai bot commented Sep 11, 2025

Note

Generated docstrings for this pull request at #33209

danielroe and others added 2 commits September 11, 2025 13:06
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@danielroe danielroe changed the title perf(nuxt): normalise components + directories more efficiently perf(nuxt,schema): normalise components + directories more efficiently Sep 11, 2025
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 (4)
packages/nuxt/src/components/module.ts (4)

90-95: Harden node_modules detection and avoid duplicate transpile entries.

  • includes('node_modules') can false-match; check path segments.
  • Guard against pushing the same path multiple times to build.transpile.
-const _transpile = typeof dirOptions.transpile === 'boolean' ? dirOptions.transpile : 'auto'
-const transpile = _transpile === 'auto' ? dirPath.includes('node_modules') : _transpile
-if (transpile) {
-  nuxt.options.build.transpile.push(dirPath)
-}
+const _transpile = typeof dirOptions.transpile === 'boolean' ? dirOptions.transpile : 'auto'
+const inNodeModules = dirPath.split(SLASH_SEPARATOR_RE).includes('node_modules')
+const transpile = _transpile === 'auto' ? inNodeModules : _transpile
+if (transpile && !nuxt.options.build.transpile.includes(dirPath)) {
+  nuxt.options.build.transpile.push(dirPath)
+}
@@
-const dirs = dirPath.includes('node_modules') ? libraryComponentDirs : userComponentDirs
+const dirs = inNodeModules ? libraryComponentDirs : userComponentDirs

Also applies to: 101-104


97-99: Windows paths: default components dir regex misses backslashes.

The warning may fire for default dirs on Windows. Make the regex path-separator agnostic.

-const DEFAULT_COMPONENTS_DIRS_RE = /\/components(?:\/(?:global|islands))?$/
+const DEFAULT_COMPONENTS_DIRS_RE = /[\\/]components(?:[\\/](?:global|islands))?$/

272-287: Docstring says “shallowest first” but comparator sorts “deepest first”.

Either flip the comparator or fix the doc. I suggest updating the docs to reflect the current (and usually desired) “deepest first”.

- * Each resulting entry has its `path` resolved via `resolveAlias` and `cwd`, receives a `priority` from `options.priority` (default 0), and entries without a `path` are skipped. The final list is sorted by path depth (shallowest first).
+ * Each resulting entry has its `path` resolved via `resolveAlias` and `cwd`, receives a `priority` from `options.priority` (default 0), and entries without a `path` are skipped. The final list is sorted by path depth (deepest first).

Also applies to: 321-322


289-291: Minor: avoid repeated sorts in normalizeDirs for arrays.

You can collect then sort once to cut redundant work (tiny perf win on large configs).

-if (Array.isArray(dir)) {
-  return dir.map(dir => normalizeDirs(dir, cwd, options)).flat().sort(compareDirByPathLength)
-}
+if (Array.isArray(dir)) {
+  const flat = dir.flatMap(d => normalizeDirs(d, cwd, options))
+  return flat.sort(compareDirByPathLength)
+}

Also applies to: 308-321

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e052878 and fb61c4b.

📒 Files selected for processing (1)
  • packages/nuxt/src/components/module.ts (5 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/module.ts
🧠 Learnings (2)
📓 Common learnings
Learnt from: GalacticHypernova
PR: nuxt/nuxt#26468
File: packages/nuxt/src/components/plugins/loader.ts:24-24
Timestamp: 2024-11-05T15:22:54.759Z
Learning: In `packages/nuxt/src/components/plugins/loader.ts`, the references to `resolve` and `distDir` are legacy code from before Nuxt used the new unplugin VFS and will be removed.
📚 Learning: 2024-11-05T15:22:54.759Z
Learnt from: GalacticHypernova
PR: nuxt/nuxt#26468
File: packages/nuxt/src/components/plugins/loader.ts:24-24
Timestamp: 2024-11-05T15:22:54.759Z
Learning: In `packages/nuxt/src/components/plugins/loader.ts`, the references to `resolve` and `distDir` are legacy code from before Nuxt used the new unplugin VFS and will be removed.

Applied to files:

  • packages/nuxt/src/components/module.ts
🧬 Code graph analysis (1)
packages/nuxt/src/components/module.ts (6)
packages/nuxt/src/utils.ts (2)
  • isDirectory (10-12)
  • logger (14-14)
packages/kit/src/index.ts (3)
  • addBuildPlugin (20-20)
  • resolveAlias (32-32)
  • logger (36-36)
packages/kit/src/build.ts (1)
  • addBuildPlugin (153-165)
packages/nuxt/src/components/plugins/component-names.ts (1)
  • ComponentNamePlugin (16-68)
packages/kit/src/resolve.ts (1)
  • resolveAlias (91-94)
packages/vite/src/utils/transpile.ts (1)
  • transpile (11-28)
⏰ 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: codeql (javascript-typescript)
  • GitHub Check: code
🔇 Additional comments (5)
packages/nuxt/src/components/module.ts (5)

24-35: Docstring for compareDirByPathLength looks good.

Clear, concise, and matches the implementation.


71-76: Layer dir aggregation via normalizeDirs is a solid simplification.

Single pass over _layers with per-layer priority assignment is clear and efficient.


120-123: User-before-library ordering is the right default.

Preserves expected override semantics.


137-139: Generating metadata gated by moduleOptions.generateMetadata: looks correct.

Matches the updated setup signature and avoids accidental reads from stale names.


50-50: Verify rename: componentOptions → moduleOptions (repo‑wide)

setup parameter renamed to moduleOptions in packages/nuxt/src/components/module.ts (≈ line 50). I couldn't verify automatically — confirm there are no remaining references to "componentOptions", that "components.generateMetadata" is used consistently, and that "exsolve" is declared/imported. Suggested checks (run and paste results):

  • git grep -n -- 'componentOptions' || rg -n --hidden -S '\bcomponentOptions\b' -g '!/node_modules/'
  • git grep -n -- 'generateMetadata' || rg -n --hidden -S 'components.generateMetadata|generateMetadata' -g '!/node_modules/'
  • rg -n --hidden -S "from\s+['"]exsolve['"]" -g '!/node_modules/' || true
  • jq -e '.dependencies.exsolve // .devDependencies.exsolve' package.json >/dev/null && echo 'exsolve in package.json' || echo 'exsolve missing'

import type { Component, ComponentsDir, ComponentsOptions } from 'nuxt/schema'

const isPureObjectOrString = (val: any) => (!Array.isArray(val) && typeof val === 'object') || typeof val === 'string'
const isPureObjectOrString = (val: unknown): val is object | string => (!Array.isArray(val) && typeof val === 'object') || typeof val === 'string'
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix the type guard: exclude null to avoid crashes when hooks mutate allDirs.

typeof null === 'object' so null currently passes the guard, leading to dirOptions = dir and dirOptions.path access throwing. Tighten the predicate.

-const isPureObjectOrString = (val: unknown): val is object | string => (!Array.isArray(val) && typeof val === 'object') || typeof val === 'string'
+const isPureObjectOrString = (val: unknown): val is Record<string, unknown> | string =>
+  typeof val === 'string' || (val !== null && typeof val === 'object' && !Array.isArray(val))
📝 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
const isPureObjectOrString = (val: unknown): val is object | string => (!Array.isArray(val) && typeof val === 'object') || typeof val === 'string'
const isPureObjectOrString = (val: unknown): val is Record<string, unknown> | string =>
typeof val === 'string' || (val !== null && typeof val === 'object' && !Array.isArray(val))
🤖 Prompt for AI Agents
In packages/nuxt/src/components/module.ts around line 21, the type guard is
treating null as an object because typeof null === 'object', which lets null
pass and later causes crashes when code reads dirOptions.path; update the
predicate to explicitly exclude null (e.g., check val !== null) so only actual
objects (non-null, non-array) or strings are accepted as valid values.

}

// TODO: remove in Nuxt v5
if (nuxt.options.experimental.normalizeComponentNames) {
Copy link
Member

Choose a reason for hiding this comment

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

is it about removing the flag or the feature ?

Copy link
Member Author

Choose a reason for hiding this comment

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

it's about removing the flag - ie. hard-coding the behaviour

@danielroe danielroe merged commit 327ba8f into main Sep 11, 2025
86 of 87 checks passed
@danielroe danielroe deleted the fix/norm-components branch September 11, 2025 12:48
@github-project-automation github-project-automation bot moved this from Discussing to Later in Team Board Sep 11, 2025
@blowsie
Copy link
Contributor

blowsie commented Sep 16, 2025

I ran into an issue with nuxt 4.1.2 where shadcn Form (using Vee Validate) is broken.
https://www.shadcn-vue.com/docs/components/form.html

I cant be 100% sure this PR is the cause, but I think it maybe.

The issue is the form/index.ts does not have a default export and the Form component being provided by nuxt vee validate is no longer loaded

@danielroe
Copy link
Member Author

@blowsie do you have a reproduction? 🙏

@blowsie
Copy link
Contributor

blowsie commented Sep 16, 2025

@blowsie do you have a reproduction? 🙏

Not to hand, but if I get a minute I'll try and create one

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

Projects

Status: Later

Development

Successfully merging this pull request may close these issues.

4 participants