Skip to content

Conversation

@danielroe
Copy link
Member

@danielroe danielroe commented Sep 1, 2025

🔗 Linked issue

vuejs/pinia#3028 (comment)

📚 Description

This adds a new utility to replace direct access of nuxt.options._layers (which is private) that will expose key nuxt layer directories for module authors, namely:

export interface LayerDirectories {
  /** Nuxt rootDir (`/` by default) */
  root: string
  /** Nitro source directory (`/server` by default) */
  server: string
  /** Local modules directory (`/modules` by default) */
  modules: string
  /** Shared directory (`/shared` by default) */
  shared: string
  /** Public directory (`/public` by default) */
  public: string
  /** Nuxt srcDir (`/app/` by default) */
  app: string
  /** Layouts directory (`/app/layouts` by default) */
  appLayouts: string
  /** Middleware directory (`/app/middleware` by default) */
  appMiddleware: string
  /** Pages directory (`/app/pages` by default) */
  appPages: string
  /** Plugins directory (`/app/plugins` by default) */
  appPlugins: string
}

I've tried to mirror nuxt config options (hence srcDir vs dir.layouts) but I wonder if it would make sense at least to move modules/shared/public out of dir as they are relative to the root directory by default. (thoughts welcome @nuxt/core)

As a very nice side effect, I've been able to refactor a lot of Nuxt internals that accessed nuxt.options._layers and I think this PR should nicely improve performance as we were performing the same normalisation steps over and over again and now we can do it once + cache it. Let's see 🤞

@danielroe danielroe self-assigned this Sep 1, 2025
@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 Sep 1, 2025

Walkthrough

Adds a new kit API to compute and cache per-layer directory descriptors (LayerDirectories and getLayerDirectories) and migrates many internal subsystems to use it instead of directly reading nuxt.options._layers or per-layer config shapes. Changes touch kit, nuxt core, builder, nitro, schema, imports, pages, pages utils, vite, template/type generation (including resolveLayerPaths signature change), module installation, ignore handling, tests, schema types (making _layers readonly), and docs. Path computations are centralised, trailing-slash normalisation added, and layer-aware discovery/watch logic updated across the codebase.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/layer-dirs

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Sep 1, 2025

Open in StackBlitz

@nuxt/kit

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

nuxt

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

@nuxt/rspack-builder

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

@nuxt/schema

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

@nuxt/vite-builder

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

@nuxt/webpack-builder

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

commit: 3c5a6fa

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

Caution

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

⚠️ Outside diff range comments (2)
packages/nuxt/src/core/schema.ts (1)

85-96: Watch the actual schema files (depth=1 misses layers//nuxt.schema.).

Chokidar with depth: 1 won’t notice rootDir/layers//nuxt.schema.. Watch explicit globs and dedupe inputs instead.

-      const rootDirs = layerDirs.map(layer => layer.rootDir)
-      const SCHEMA_RE = /(?:^|\/)nuxt.schema.\w+$/
-      const watcher = watch(rootDirs, {
+      const schemaGlobs = [...new Set(layerDirs.flatMap(l => [
+        join(l.rootDir, 'nuxt.schema.*'),
+        join(l.rootDir, 'layers/*/nuxt.schema.*'),
+      ]))]
+      const watcher = watch(schemaGlobs, {
         ...nuxt.options.watchers.chokidar,
-        depth: 1,
         ignored: [
-          (path, stats) => (stats && !stats.isFile()) || !SCHEMA_RE.test(path),
           isIgnored,
           /[\\/]node_modules[\\/]/,
         ],
         ignoreInitial: true,
       })

Also applies to: 97-99

packages/nuxt/src/core/nitro.ts (1)

42-45: Make excludePattern Windows-safe

The current regex only matches forward slashes, so on Windows paths with backslashes won’t be excluded, causing excessive scans. Use a [\/]-based pattern.

-  const excludePattern = excludePaths.length
-    ? [new RegExp(`node_modules\\/(?!${excludePaths.join('|')})`)]
-    : [/node_modules/]
+  const excludePattern = excludePaths.length
+    ? [new RegExp(`[\\\\/]node_modules[\\\\/](?!${excludePaths.join('|')})`)]
+    : [/[\\/]node_modules[\\/]/]
🧹 Nitpick comments (26)
packages/kit/src/index.ts (1)

13-15: Also export the LayerDirectories type for ergonomics.

Most consumers will want both the function and its result type.

Apply this diff:

 // Layers
-export { getLayerDirectories } from './layers'
+export { getLayerDirectories } from './layers'
+export type { LayerDirectories } from './layers'
packages/kit/src/ignore.ts (1)

25-31: Normalise pathname before prefix checks to avoid false negatives with relative paths.

Ensures consistent behaviour across platforms and call sites that pass relative paths.

Apply this diff:

-  const cwds = getLayerDirectories(nuxt)?.map(layer => layer.rootDir).sort((a, b) => b.length - a.length)
-  const layer = cwds?.find(cwd => pathname.startsWith(cwd))
-  const relativePath = relative(layer ?? nuxt.options.rootDir, pathname)
+  const absPath = pathname.startsWith('/') ? pathname : resolve(nuxt.options.rootDir, pathname)
+  const cwds = getLayerDirectories(nuxt)?.map(layer => layer.rootDir).sort((a, b) => b.length - a.length)
+  const layer = cwds?.find(cwd => absPath.startsWith(cwd))
+  const relativePath = relative(layer ?? nuxt.options.rootDir, absPath)
packages/kit/src/module/install.ts (1)

29-32: Deduplicate local module dirs and simplify collection.

Avoid repeated entries when multiple layers resolve to the same modules dir.

Apply this diff:

-  const localLayerModuleDirs: string[] = []
-  for (const l of getLayerDirectories(nuxt)) {
-    if (!NODE_MODULES_RE.test(l.srcDir)) {
-      localLayerModuleDirs.push(l.dir.modules)
-    }
-  }
+  const localLayerModuleDirs = [...new Set(
+    getLayerDirectories(nuxt)
+      .filter(l => !NODE_MODULES_RE.test(l.srcDir))
+      .map(l => l.dir.modules)
+  )]
packages/nuxt/src/pages/utils.ts (1)

50-51: Verify layer precedence when de-duplicating pages across layers.
You sort by relativePath and then uniqueBy keeps the first occurrence. With stable sort this implicitly depends on getLayerDirectories(nuxt) order. Please confirm root-layer pages correctly override upstream layers for duplicate relative paths. If not, prefer “keep last” semantics:

-  const allRoutes = generateRoutesFromFiles(uniqueBy(scannedFiles, 'relativePath'), {
+  const deduped = Array.from(new Map(scannedFiles.map(f => [f.relativePath, f])).values())
+  const allRoutes = generateRoutesFromFiles(deduped, {
     shouldUseServerComponents: !!nuxt.options.experimental.componentIslands,
   })
packages/nuxt/src/imports/module.ts (1)

117-117: Normalise paths before priority matching; double-check priority direction.
startsWith comparisons can fail cross‑platform if not normalised. Also confirm that Unimport interprets more-negative priorities as higher precedence as intended.

-    const priorities = getLayerDirectories(nuxt).map((layer, i) => [layer.srcDir, -i] as const).sort(([a], [b]) => b.length - a.length)
+    const priorities = getLayerDirectories(nuxt)
+      .map((layer, i) => [normalize(layer.srcDir), -i] as const)
+      .sort(([a], [b]) => b.length - a.length)

And when assigning:

-            i.priority ||= priorities.find(([dir]) => i.from.startsWith(dir))?.[1]
+            const from = normalize(i.from)
+            i.priority ||= priorities.find(([dir]) => from.startsWith(dir))?.[1]
packages/kit/test/generate-types.spec.ts (1)

118-119: Nice: exercising resolveLayerPaths with LayerDirectories.
Consider adding a quick assertion that getLayerDirectories preserves the expected layer order (root last) to guard precedence-sensitive behaviour elsewhere.

packages/nuxt/src/core/builder.ts (1)

105-105: Deduplicate watched srcDirs to avoid redundant watchers.
Not critical, but a Set can prevent duplicate entries.

-  const watcher = chokidarWatch(getLayerDirectories(nuxt).map(i => i.srcDir), {
+  const watcher = chokidarWatch([...new Set(getLayerDirectories(nuxt).map(i => i.srcDir))], {
packages/vite/src/vite.ts (1)

174-177: Prefer withTrailingSlash for robustness.
String concatenation may miss normalisation on Windows or double slashes.

-    const delimitedRootDir = nuxt.options.rootDir + '/'
+    const delimitedRootDir = withTrailingSlash(nuxt.options.rootDir)
packages/nuxt/src/core/schema.ts (1)

67-77: Parcel watch ignore negations unsupported – filter in callback instead
ignore only accepts positive globs (no !-style negation), so ignore: ['!nuxt.schema.*'] won’t work. Subscribe on the root (omit ignore) and invoke onChange only for matching events:

- for (const layer of layerDirs) {
-   const subscription = await subscribe(layer.rootDir, onChange, {
-     ignore: ['!nuxt.schema.*'],
-   })
+ const SCHEMA_RE = /(?:^|\/)nuxt\.schema\.\w+$/
+ for (const layer of layerDirs) {
+   const subscription = await subscribe(layer.rootDir, (_err, events) => {
+     if (events.some(e => SCHEMA_RE.test(e.path))) onChange()
+   })
   nuxt.hook('close', () => subscription.unsubscribe())
 }
packages/nuxt/src/core/app.ts (2)

137-139: Call getLayerDirectories once and derive others from it.

Avoid recomputation and guarantee ordering alignment with _layers.

-  const layerSrcs = getLayerDirectories(nuxt).map(l => l.srcDir)
+  const layerDirs = getLayerDirectories(nuxt)
+  const layerSrcs = layerDirs.map(l => l.srcDir)
...
-  const layerConfigs = nuxt.options._layers.map(layer => layer.config)
-  const reversedConfigs = layerConfigs.slice().reverse()
-  const layerDirs = getLayerDirectories(nuxt)
-  const reversedLayerDirs = [...layerDirs].reverse()
+  const layerConfigs = nuxt.options._layers.map(layer => layer.config)
+  const reversedConfigs = [...layerConfigs].reverse()
+  const reversedLayerDirs = [...layerDirs].reverse()

Also applies to: 154-159


195-205: Keep config/plugins and directory plugins in lockstep.

Index-based pairing of reversedConfigs[i] with reversedLayerDirs[i] assumes identical lengths/order. Add a quick invariant check or guard to prevent subtle misalignment.

Example guard:

+  if (reversedConfigs.length !== reversedLayerDirs.length) {
+    logger.warn('Layer configs and directories length mismatch; plugin resolution order may be off.')
+  }
packages/kit/src/template.ts (2)

171-211: resolveLayerPaths(layer, projectBuildDir): solid refactor; one micro-tweak.

Great consolidation. Consider adding globalDeclarations for nested layer roots beyond one level if you intend to support deeper nesting in future.


171-211: Potential public API break (signature change).

resolveLayerPaths changed its signature; if external consumers import it, this is breaking. Consider an overload/deprecation shim to accept the old parameters for one minor cycle.

Would you like a backward-compatible overload implementation?

packages/nuxt/src/core/nuxt.ts (4)

162-163: Cache and reuse layerDirs within initNuxt

You compute layerDirs once here; later you recompute via getLayerDirectories in the watcher. Prefer using this cached value for clarity and to avoid redundant calls (even if memoised).

Apply this diff near the watcher to reuse the cached array:

-    const layerRelativePaths = new Set(getLayerDirectories(nuxt).map(l => relative(l.srcDir, path)))
+    const layerRelativePaths = new Set(layerDirs.map(l => relative(l.srcDir, path)))

419-421: Transpile selection for node_modules-backed layers looks good; consider deduping

Pushing many paths can introduce duplicates. Optional: dedupe nuxt.options.build.transpile after population to keep config lean.

   nuxt.options.build.transpile.push(
     ...layerDirs.filter(i => i.rootDir.includes('node_modules')).map(i => i.rootDir),
   )
+  // Optional: dedupe
+  nuxt.options.build.transpile = [...new Set(nuxt.options.build.transpile)]

423-431: Modules dir injection: path prefix check is OK; add trailing-slash normalisation for robustness

The startsWith check is string-based. Guard against accidental partial matches by ensuring both sides are normalised with a trailing slash.

-  const locallyScannedLayersDirs = layerDirs.map(l => join(l.rootDir, 'layers/'))
+  const locallyScannedLayersDirs = layerDirs.map(l => withTrailingSlash(join(l.rootDir, 'layers')))
   for (const layer of layerDirs) {
-    if (locallyScannedLayersDirs.every(dir => !layer.rootDir.startsWith(dir))) {
+    const layerRoot = withTrailingSlash(layer.rootDir)
+    if (locallyScannedLayersDirs.every(dir => !layerRoot.startsWith(dir))) {
       nuxt.options.modulesDir.push(join(layer.rootDir, 'node_modules'))
     }
   }

685-699: Reuse cached layerDirs in watcher

Small nit: use the earlier layerDirs variable to avoid recomputation and keep intent clear.

-    const layerRelativePaths = new Set(getLayerDirectories(nuxt).map(l => relative(l.srcDir, path)))
+    const layerRelativePaths = new Set(layerDirs.map(l => relative(l.srcDir, path)))
packages/nuxt/src/core/nitro.ts (3)

158-160: Reuse cached layerDirs instead of recomputing

Minor tidy-up to avoid repeated getLayerDirectories calls.

-          ...getLayerDirectories(nuxt).map(layer =>
+          ...layerDirs.map(layer =>
             relativeWithDot(nuxt.options.buildDir, join(layer.dir.shared, '**/*.d.ts')),
           ),

177-181: Reuse cached layerDirs for public assets

Avoid repeated getLayerDirectories calls.

-      ...getLayerDirectories(nuxt)
-        .map(layer => layer.dir.public)
+      ...layerDirs
+        .map(layer => layer.dir.public)
         .filter(dir => existsSync(dir))
         .map(dir => ({ dir })),

206-207: Reuse cached layerDirs for externals inline

Same tidy-up here.

-        ...getLayerDirectories(nuxt).map(layer => join(layer.srcDir, 'app.config')),
+        ...layerDirs.map(layer => join(layer.srcDir, 'app.config')),
packages/nuxt/src/pages/module.ts (1)

307-312: Template regeneration watch set: good coverage of pages/layouts/middleware

Optional: dedupe directories to avoid redundant startsWith checks when multiple layers point to the same path.

-    const updateTemplatePaths = getLayerDirectories(nuxt)
-      .flatMap(l => [
-        l.dir.pages,
-        l.dir.layouts,
-        l.dir.middleware,
-      ])
+    const updateTemplatePaths = Array.from(new Set(
+      getLayerDirectories(nuxt).flatMap(l => [
+        l.dir.pages,
+        l.dir.layouts,
+        l.dir.middleware,
+      ]),
+    ))
packages/kit/src/layers.ts (5)

21-21: Add an explicit return type for the public API.

This is part of the public kit surface; make the return type explicit to avoid accidental breaking changes due to inference shifts.

-export function getLayerDirectories (nuxt = useNuxt()) {
+export function getLayerDirectories (nuxt = useNuxt()): LayerDirectories[] {

52-54: Use a more robust trailing-slash helper (optional).

Local regex is fine, but we already rely on pathe. Consider a shared utility (or at least guard the empty-string case) to avoid subtle edge cases.

-function withTrailingSlash (dir: string) {
-  return dir.replace(/[^/]$/, '$&/')
-}
+function withTrailingSlash (dir: string) {
+  return dir.endsWith('/') ? dir : (dir ? `${dir}/` : '/`)
+}

19-25: Cache invalidation note (FYI).

WeakMap caching is good. If any code mutates layer config at runtime (rare), results may become stale. If that’s possible in dev mode, consider an explicit invalidateLayerDirectories(layer) or clearing the cache on nuxt.hook('restart', ...).


5-18: Consider including other commonly used directories (optional).

Depending on adoption plans, assets and components are frequent lookups. Including them up-front can reduce repeated ad-hoc resolutions in consumers.


37-45: Document rootDir-relative keys in dir or lift them out
dir.modules, dir.shared and dir.public are all resolved from rootDir, whereas other dir.* entries use srcDir. This asymmetry is intentional and consistently handled across the codebase, but may confuse consumers. Consider either:

  • Extracting modules, shared, and public into dedicated top-level options (e.g. modulesDir, sharedDir, publicDir), or
  • Adding clear documentation/comments that these specific entries are rootDir-relative.
📜 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 7c5efac and 4db503d.

📒 Files selected for processing (17)
  • packages/kit/src/ignore.test.ts (1 hunks)
  • packages/kit/src/ignore.ts (2 hunks)
  • packages/kit/src/index.ts (1 hunks)
  • packages/kit/src/layers.ts (1 hunks)
  • packages/kit/src/module/install.ts (2 hunks)
  • packages/kit/src/template.ts (4 hunks)
  • packages/kit/test/generate-types.spec.ts (3 hunks)
  • packages/nuxt/src/core/app.ts (5 hunks)
  • packages/nuxt/src/core/builder.ts (4 hunks)
  • packages/nuxt/src/core/nitro.ts (7 hunks)
  • packages/nuxt/src/core/nuxt.ts (5 hunks)
  • packages/nuxt/src/core/schema.ts (5 hunks)
  • packages/nuxt/src/imports/module.ts (2 hunks)
  • packages/nuxt/src/pages/module.ts (4 hunks)
  • packages/nuxt/src/pages/utils.ts (2 hunks)
  • packages/schema/src/types/config.ts (1 hunks)
  • packages/vite/src/vite.ts (3 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

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

Follow standard TypeScript conventions and best practices

Files:

  • packages/kit/src/index.ts
  • packages/nuxt/src/core/builder.ts
  • packages/nuxt/src/imports/module.ts
  • packages/kit/src/ignore.ts
  • packages/kit/src/ignore.test.ts
  • packages/nuxt/src/core/schema.ts
  • packages/kit/src/module/install.ts
  • packages/nuxt/src/pages/utils.ts
  • packages/vite/src/vite.ts
  • packages/kit/src/layers.ts
  • packages/kit/test/generate-types.spec.ts
  • packages/kit/src/template.ts
  • packages/nuxt/src/core/app.ts
  • packages/nuxt/src/core/nitro.ts
  • packages/schema/src/types/config.ts
  • packages/nuxt/src/core/nuxt.ts
  • packages/nuxt/src/pages/module.ts
**/*.{test,spec}.{ts,tsx,js,jsx}

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

Write unit tests for core functionality using vitest

Files:

  • packages/kit/src/ignore.test.ts
  • packages/kit/test/generate-types.spec.ts
🧠 Learnings (4)
📚 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/kit/src/index.ts
  • packages/nuxt/src/core/builder.ts
  • packages/nuxt/src/imports/module.ts
  • packages/nuxt/src/core/schema.ts
  • packages/kit/src/module/install.ts
  • packages/nuxt/src/pages/utils.ts
  • packages/vite/src/vite.ts
  • packages/kit/test/generate-types.spec.ts
  • packages/kit/src/template.ts
  • packages/nuxt/src/core/app.ts
  • packages/nuxt/src/core/nuxt.ts
  • packages/nuxt/src/pages/module.ts
📚 Learning: 2025-07-18T16:46:07.446Z
Learnt from: CR
PR: nuxt/nuxt#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-07-18T16:46:07.446Z
Learning: Applies to **/*.{test,spec}.{ts,tsx,js,jsx} : Write unit tests for core functionality using `vitest`

Applied to files:

  • packages/kit/src/ignore.test.ts
📚 Learning: 2025-07-18T16:46:07.446Z
Learnt from: CR
PR: nuxt/nuxt#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-07-18T16:46:07.446Z
Learning: Applies to **/e2e/**/*.{ts,js} : Write end-to-end tests using Playwright and `nuxt/test-utils`

Applied to files:

  • packages/kit/src/ignore.test.ts
📚 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:

  • packages/nuxt/src/pages/module.ts
🧬 Code graph analysis (2)
packages/kit/test/generate-types.spec.ts (1)
packages/kit/src/template.ts (1)
  • resolveLayerPaths (171-211)
packages/nuxt/src/core/app.ts (2)
packages/kit/src/resolve.ts (2)
  • findPath (68-86)
  • resolveFiles (277-285)
packages/nuxt/src/core/utils/names.ts (1)
  • getNameFromPath (6-14)
⏰ 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). (10)
  • GitHub Check: test-fixtures (ubuntu-latest, built, vite, async, manifest-off, 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, default, manifest-off, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, dev, vite, async, manifest-off, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, dev, vite, default, manifest-on, json, lts/-1)
  • GitHub Check: release-pkg-pr-new
  • GitHub Check: typecheck (windows-latest, bundler)
  • GitHub Check: typecheck (ubuntu-latest, bundler)
  • GitHub Check: test-benchmark
  • GitHub Check: code
🔇 Additional comments (28)
packages/kit/src/ignore.test.ts (1)

2-2: LGTM: Types import adjustment is correct.

Switching to Nuxt/NuxtConfig here aligns with current schema exports.

packages/schema/src/types/config.ts (1)

91-91: Confirm no runtime mutations on _layers

  • No calls to push/pop/splice/shift/unshift/sort/reverse on this array.
  • The only assignment to _layers is in packages/kit/src/loader/config.ts:110, where the array is initialised.
packages/kit/src/ignore.ts (1)

5-5: LGTM: Centralising layer discovery via getLayerDirectories.

This removes private _layers coupling.

packages/kit/src/module/install.ts (1)

17-17: LGTM: Using getLayerDirectories here reduces duplication and private API usage.

packages/nuxt/src/pages/utils.ts (1)

3-5: Imports update looks correct.
Switch to using getLayerDirectories and related utilities is aligned with the new API.

packages/nuxt/src/imports/module.ts (1)

2-2: Adopting getLayerDirectories for imports module.
Good move towards the public API.

packages/kit/test/generate-types.spec.ts (2)

8-8: Test import path update is fine.
Using the public entry ensures parity with consumers.


27-27: Layer mock includes rootDir and srcDir as expected.
Matches the new directory-centric API.

packages/nuxt/src/core/builder.ts (3)

4-4: Importing getLayerDirectories is appropriate here.
Aligns builder with the new layer API.


27-29: App/error invalidation across layers.
Using relative(layer.srcDir, path) ensures correct detection per layer. Looks good.


250-254: Using getLayerDirectories in resolvePathsToWatch.
Simple and correct; respects ignores.

packages/vite/src/vite.ts (2)

5-5: Vite now depends on getLayerDirectories.
Good consistency with the rest of the refactor.


41-41: Allow-listing layer rootDirs is correct.
Ensures external layer files are accessible to Vite.

packages/nuxt/src/core/schema.ts (1)

58-59: Adoption of getLayerDirectories is solid.

Using layer.rootDir with resolver.resolvePath(join(layer.rootDir, 'nuxt.schema')) is correct and improves clarity.

Also applies to: 110-112

packages/nuxt/src/core/app.ts (3)

160-173: Layouts discovery across layers looks correct.

Using per-layer dir.layouts and deriving names relative to that base is the right approach.


177-191: Middleware resolution order and patterns look good.

Reverse layering is preserved; patterns cover file and index variants.


221-226: Layered app.config resolution LGTM.

Scanning each srcDir for app.config maintains expected override semantics.

packages/kit/src/template.ts (2)

19-21: New layer utilities import is appropriate.


232-235: Types include/exclude logic works with LayerDirectories.

Conditionals and globs read well and preserve prior behaviour with clearer data flow.

Also applies to: 256-260

packages/nuxt/src/core/nuxt.ts (2)

9-9: Importing getLayerDirectories into core is fine

No issues with adding this import; matches the PR’s objective.


263-270: Nice: per-layer index.d.ts is now discovered via layer.rootDir

This simplifies type discovery and removes cwd assumptions.

packages/nuxt/src/core/nitro.ts (4)

11-11: Import looks correct

Adopting getLayerDirectories here aligns Nitro initialisation with the new abstraction.


36-41: Exclude list derivation: capture logic is fine

Extracting the tail after node_modules/.pnpm paths is sensible. See follow-up on Windows-safe regex below.


122-123: scanDirs via layer.serverDir: LGTM

This matches the new layer contract and removes per-config coupling.


140-140: appConfigFiles via layer.srcDir: LGTM

Consistent with new structure.

packages/nuxt/src/pages/module.ts (3)

3-3: Importing getLayerDirectories here is appropriate

Brings pages module in line with the new layer API.


22-22: Type imports look correct

Switch to importing Nuxt and NuxtPage only; consistent with usage below.


88-88: pagesDirs via getLayerDirectories: LGTM

This removes manual per-layer resolution and reads better.

Comment on lines 27 to 29
const isRoot = layer.config.rootDir === nuxt.options.rootDir
const config = isRoot ? nuxt.options : (layer.config as NuxtOptions)

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Root-layer detection can be brittle due to path normalisation differences.

Comparing config.rootDir with nuxt.options.rootDir can fail with trailing-slash or case/relative differences. Prefer comparing normalised cwd to nuxt.options.rootDir (also normalised).

-import { resolve } from 'pathe'
+import { resolve, normalize } from 'pathe'
@@
-    const isRoot = layer.config.rootDir === nuxt.options.rootDir
+    const isRoot = normalize(layer.cwd) === normalize(nuxt.options.rootDir)
     const config = isRoot ? nuxt.options : (layer.config as NuxtOptions)

Also applies to: 3-3

🤖 Prompt for AI Agents
In packages/kit/src/layers.ts around lines 27 to 29, the root-layer detection
compares layer.config.rootDir to nuxt.options.rootDir which is brittle across
trailing slashes, case or relative paths; change the check to compare a
normalized absolute cwd for the layer (e.g. path.resolve or fs.realpathSync of
layer.config.cwd or rootDir) against a normalized absolute nuxt.options.rootDir
(also resolved/realpathSync), ensuring both values are normalized
(path.resolve/path.normalize or realpath) before equality comparison; apply the
same normalization fix to the other occurrence noted (lines 3-3).

Comment on lines 30 to 36
const srcDir = withTrailingSlash(config.srcDir || layer.cwd)
const rootDir = withTrailingSlash(config.rootDir || layer.cwd)
const directories = {
srcDir,
rootDir,
serverDir: withTrailingSlash(resolve(layer.cwd, config?.serverDir || 'server')),
dir: {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Resolve rootDir/srcDir relative to the layer root and base serverDir off rootDir.

  • Today srcDir/rootDir accept relative paths; resolving them via layer.cwd makes outcomes stable.
  • serverDir should be relative to rootDir (not layer.cwd) to respect an overridden rootDir.
-    const srcDir = withTrailingSlash(config.srcDir || layer.cwd)
-    const rootDir = withTrailingSlash(config.rootDir || layer.cwd)
+    const rootDir = withTrailingSlash(resolve(layer.cwd, config.rootDir || '.'))
+    const srcDir = withTrailingSlash(resolve(rootDir, config.srcDir || '.'))
@@
-      serverDir: withTrailingSlash(resolve(layer.cwd, config?.serverDir || 'server')),
+      serverDir: withTrailingSlash(resolve(rootDir, config.serverDir || 'server')),
📝 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 srcDir = withTrailingSlash(config.srcDir || layer.cwd)
const rootDir = withTrailingSlash(config.rootDir || layer.cwd)
const directories = {
srcDir,
rootDir,
serverDir: withTrailingSlash(resolve(layer.cwd, config?.serverDir || 'server')),
dir: {
const rootDir = withTrailingSlash(resolve(layer.cwd, config.rootDir || '.'))
const srcDir = withTrailingSlash(resolve(rootDir, config.srcDir || '.'))
const directories = {
srcDir,
rootDir,
serverDir: withTrailingSlash(resolve(rootDir, config.serverDir || 'server')),
dir: {
🤖 Prompt for AI Agents
In packages/kit/src/layers.ts around lines 30–36, change how directories are
resolved: first resolve rootDir against layer.cwd (e.g. resolve(layer.cwd,
config.rootDir || '.' ) and then resolve srcDir against that resolved rootDir
(e.g. resolve(resolvedRootDir, config.srcDir || '.') ) so relative paths are
interpreted from the layer root; finally compute serverDir relative to the
resolved rootDir (not layer.cwd) so an overridden rootDir affects serverDir.
Ensure you still wrap results with withTrailingSlash.

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

Caution

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

⚠️ Outside diff range comments (2)
packages/nuxt/src/core/app.ts (2)

156-167: Layouts precedence is inverted; later layers (user) should override earlier ones (base).

Iterate reversed so first match comes from the user layer and keep it with ||=.

Apply this diff:

-  for (const layer of layerDirs) {
+  for (const layer of reversedLayerDirs) {
     const layoutFiles = await resolveFiles(layer.dir.layouts, `**/*{${extensionGlob}}`)
     for (const file of layoutFiles) {
       const name = getNameFromPath(file, layer.dir.layouts)
       if (!name) {
         // Ignore files like `~/layouts/index.vue` which end up not having a name at all
         logger.warn(`No layout name could be resolved for \`${resolveToAlias(file, nuxt)}\`. Bear in mind that \`index\` is ignored for the purpose of creating a layout name.`)
         continue
       }
       layouts[name] ||= { name, file }
     }
   }

217-222: Reverse app.configs before merging so user values override – currently we collect layerDirs base→user and call defuFn(cfg0, …, inlineConfig) (leftmost wins), causing base configs to take precedence. Invert the iteration (e.g. for (const layer of [...layerDirs].reverse())) or reverse app.configs before the defuFn call so user layers are merged first.

🧹 Nitpick comments (1)
packages/nuxt/src/core/app.ts (1)

188-194: Avoid positional coupling between two reversed arrays.

Indexing reversedLayers[i] alongside reversedLayerDirs[i] is brittle. Build a zipped structure for clarity and safety.

Example refactor:

-  for (let i = 0; i < reversedLayerDirs.length; i++) {
-    const config = reversedLayers[i]!.config
-    const layer = reversedLayerDirs[i]!
+  const reversed = reversedLayers.map((l, i) => ({ layer: reversedLayerDirs[i]!, config: l.config }))
+  for (const { layer, config } of reversed) {
📜 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 4db503d and e5ef603.

📒 Files selected for processing (2)
  • packages/kit/src/index.ts (1 hunks)
  • packages/nuxt/src/core/app.ts (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/kit/src/index.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:

  • packages/nuxt/src/core/app.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/core/app.ts
🧬 Code graph analysis (1)
packages/nuxt/src/core/app.ts (3)
packages/kit/src/layers.ts (1)
  • getLayerDirectories (21-50)
packages/kit/src/resolve.ts (2)
  • findPath (68-86)
  • resolveFiles (277-285)
packages/nuxt/src/core/utils/names.ts (1)
  • getNameFromPath (6-14)
⏰ 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). (16)
  • GitHub Check: test-fixtures (windows-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, dev, vite, default, manifest-on, json, lts/-1)
  • GitHub Check: test-fixtures (windows-latest, dev, vite, default, manifest-on, json, lts/-1)
  • GitHub Check: test-fixtures (windows-latest, dev, vite, async, manifest-on, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, built, rspack, default, manifest-on, json, lts/-1)
  • GitHub Check: test-fixtures (windows-latest, dev, vite, async, manifest-off, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, dev, vite, async, manifest-off, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, built, vite, async, manifest-on, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, built, vite, async, manifest-on, js, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, built, vite, async, 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, 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-benchmark
  • GitHub Check: typecheck (windows-latest, bundler)
🔇 Additional comments (3)
packages/nuxt/src/core/app.ts (3)

4-4: Imports look good.

The new kit utilities are correctly imported and used below.


138-141: LGTM: centralised, cached layer directory resolution.

Using getLayerDirectories(nuxt) once and deriving reversedLayerDirs is clean and avoids repeated normalisation.


196-199: Plugin scanning depth is intentional and matches documentation
Code and docs specify only top-level files and immediate subdirectory index.* files are auto-registered (no deeper nesting), so the current glob (*{…}, */index{…}) is correct.

join(layer.config.srcDir, 'app'),
]),
)
app.mainComponent ||= await findPath(layerDirs.flatMap(d => [join(d.srcDir, 'App'), join(d.srcDir, 'app')]),)
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 layer precedence for app.vue: user/project layer should win.

Current order checks base layers first. Reverse the search to honour user overrides.

Apply this diff:

-  app.mainComponent ||= await findPath(layerDirs.flatMap(d => [join(d.srcDir, 'App'), join(d.srcDir, 'app')]),)
+  app.mainComponent ||= await findPath(reversedLayerDirs.flatMap(d => [join(d.srcDir, 'App'), join(d.srcDir, 'app')]))

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In packages/nuxt/src/core/app.ts around line 143, the app.vue search currently
iterates layerDirs from base → user so base layers win; reverse the search order
so user/project layers take precedence by iterating layerDirs from last → first
(e.g. reverse a copy of layerDirs before flatMap) when building the candidate
paths for findPath, ensuring the user layer entries appear earlier in the list.

app.errorComponent ||= (await findPath(
nuxt.options._layers.map(layer => join(layer.config.srcDir, 'error')),
)) ?? resolve(nuxt.options.appDir, 'components/nuxt-error-page.vue')
app.errorComponent ||= (await findPath(layerDirs.map(d => join(d.srcDir, 'error')))) ?? resolve(nuxt.options.appDir, 'components/nuxt-error-page.vue')
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Prefer user error component over base layers.

Use reversedLayerDirs so the project’s error.vue takes precedence.

Apply this diff:

-  app.errorComponent ||= (await findPath(layerDirs.map(d => join(d.srcDir, 'error')))) ?? resolve(nuxt.options.appDir, 'components/nuxt-error-page.vue')
+  app.errorComponent ||= (await findPath(reversedLayerDirs.map(d => join(d.srcDir, 'error')))) ?? resolve(nuxt.options.appDir, 'components/nuxt-error-page.vue')
📝 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
app.errorComponent ||= (await findPath(layerDirs.map(d => join(d.srcDir, 'error')))) ?? resolve(nuxt.options.appDir, 'components/nuxt-error-page.vue')
app.errorComponent ||= (await findPath(reversedLayerDirs.map(d => join(d.srcDir, 'error')))) ?? resolve(nuxt.options.appDir, 'components/nuxt-error-page.vue')
🤖 Prompt for AI Agents
In packages/nuxt/src/core/app.ts around line 150, the error component resolution
uses layerDirs.map which gives base layers precedence; replace
layerDirs.map(...) with reversedLayerDirs.map(...) so the project's error.vue
(topmost layer) is preferred over base layers when calling
findPath(join(d.srcDir, 'error')), ensuring reversedLayerDirs is available in
scope or imported as used elsewhere in this file.

Comment on lines 171 to 175
for (const layer of reversedLayerDirs) {
const middlewareFiles = await resolveFiles(layer.dir.middleware, [
`*{${extensionGlob}}`,
`*/index{${extensionGlob}}`,
])
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Middleware precedence regression: “base wins” due to double-reverse.

Scanning with reversedLayerDirs then applying uniqueBy([...middleware].reverse()) keeps the base copy on conflict. Scan in base→user order to preserve “last wins” (user).

Apply this diff:

-  for (const layer of reversedLayerDirs) {
+  for (const layer of layerDirs) {
     const middlewareFiles = await resolveFiles(layer.dir.middleware, [
       `*{${extensionGlob}}`,
       `*/index{${extensionGlob}}`,
     ])
   }

No changes needed to the de-dupe line at 212; it already implements “last wins” for base→user scans.

Also applies to: 212-212

🤖 Prompt for AI Agents
In packages/nuxt/src/core/app.ts around lines 171 to 175, the loop currently
iterates over reversedLayerDirs which causes middleware scanning to be performed
in reverse and then later deduped with a reverse again, making base middleware
win; change the loop to iterate in the original layer order (e.g., layerDirs or
the non-reversed array) so scanning happens base→user and the existing
uniqueBy([...middleware].reverse()) at line 212 will correctly implement
last-wins (user overrides); do not change the de-dupe line at 212.

@codspeed-hq
Copy link

codspeed-hq bot commented Sep 1, 2025

CodSpeed Performance Report

Merging #33098 will not alter performance

Comparing feat/layer-dirs (3c5a6fa) with main (00ede5c)

Summary

✅ 10 untouched benchmarks

@TheAlexLichter
Copy link
Member

but I wonder if it would make sense at least to move modules/shared/public out of dir as they are relative to the root directory by default.

Or the other way around and move the rest "inside" dir? Either way that should probably be updated together with the main nuxt config types & options.

@danielroe
Copy link
Member Author

danielroe commented Sep 1, 2025

would it make sense for srcDir to become app, etc.?

export interface LayerDirectories {
  /** Nuxt rootDir (`/` by default) */
  root: string
  /** Nitro source directory (`/server` by default) */
  server: string
  /** Local modules directory (`/modules` by default) */
  modules: string
  /** Shared directory (`/shared` by default) */
  shared: string
  /** Public directory (`/public` by default) */
  public: string
  /** Nuxt srcDir (`/app/` by default) */
  app: string
  /** Layouts directory (`/app/layouts` by default) */
  appLayouts: string
  /** Middleware directory (`/app/middleware` by default) */
  appMiddleware: string
  /** Pages directory (`/app/pages` by default) */
  appPages: string
  /** Plugins directory (`/app/plugins` by default) */
  appPlugins: string
}

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

♻️ Duplicate comments (6)
packages/nuxt/src/core/app.ts (3)

150-150: Prefer user error component over base layers.

Use reversedLayerDirs so the project’s error.vue takes precedence.

Apply this diff:

-app.errorComponent ||= (await findPath(layerDirs.map(d => join(d.src, 'error')))) ?? resolve(nuxt.options.appDir, 'components/nuxt-error-page.vue')
+app.errorComponent ||= (await findPath(reversedLayerDirs.map(d => join(d.src, 'error')))) ?? resolve(nuxt.options.appDir, 'components/nuxt-error-page.vue')

171-175: Middleware precedence regression: double-reverse makes base win.

Scan base→user so the existing de-dupe at Line 212 preserves user copies.

Apply this diff:

-for (const dirs of reversedLayerDirs) {
+for (const dirs of layerDirs) {

143-143: Fix layer precedence for app.vue: user/project layer should win.

Search currently prefers base layers. Reverse the search order.

Apply this diff:

-app.mainComponent ||= await findPath(layerDirs.flatMap(d => [join(d.src, 'App'), join(d.src, 'app')]),)
+app.mainComponent ||= await findPath(reversedLayerDirs.flatMap(d => [join(d.src, 'App'), join(d.src, 'app')]))
packages/kit/src/layers.ts (3)

36-38: Fix brittle root-layer detection (normalise and compare absolute paths).

Comparing raw config.rootDir strings can mis-detect the root layer due to relative paths, trailing slashes, or case differences. Resolve against cwd and normalise both sides before comparing.

Apply this diff:

-    const isRoot = layer.config.rootDir === nuxt.options.rootDir
-    const config = isRoot ? nuxt.options : (layer.config as NuxtOptions)
+    const layerRootAbs = resolve(layer.cwd, layer.config.rootDir || '.')
+    const nuxtRootAbs = resolve(nuxt.options.rootDir || '.')
+    const isRoot = normalize(layerRootAbs) === normalize(nuxtRootAbs)
+    const config = isRoot ? nuxt.options : (layer.config as NuxtOptions)

39-41: Resolve rootDir relative to the layer root and derive srcDir from that.

Current code may treat relative rootDir/srcDir incorrectly and can also keep Windows backslashes in src/root. Resolve first to stabilise outcomes across platforms.

Apply this diff:

-    const src = withTrailingSlash(config.srcDir || layer.cwd)
-    const root = withTrailingSlash(config.rootDir || layer.cwd)
+    const root = withTrailingSlash(resolve(layer.cwd, config.rootDir || '.'))
+    const src = withTrailingSlash(resolve(root, config.srcDir || '.'))

1-3: Import normalize from pathe for the root-layer comparison.

Apply this diff:

-import { resolve } from 'pathe'
+import { resolve, normalize } from 'pathe'
🧹 Nitpick comments (4)
packages/kit/src/template.ts (1)

171-211: Exclude current layer’s server directory from app TS includes, and validate nested layer src defaults.

  • nitro globs don’t exclude dirs.server for the current layer; when server sits under src (v3-style), server files can leak into app TS. Add it explicitly.
  • Nested layer globs assume app/ as src; custom srcDir in nested layers may be missed.

Apply this diff to exclude the current layer’s server:

 export function resolveLayerPaths (dirs: LayerDirectories, projectBuildDir: string) {
   const relativeRootDir = relativeWithDot(projectBuildDir, dirs.root)
   const relativeSrcDir = relativeWithDot(projectBuildDir, dirs.src)
   const relativeModulesDir = relativeWithDot(projectBuildDir, dirs.modules)
   const relativeSharedDir = relativeWithDot(projectBuildDir, dirs.shared)
+  const relativeServerDir = relativeWithDot(projectBuildDir, dirs.server)
   return {
     nuxt: [
       join(relativeSrcDir, '**/*'),
       join(relativeModulesDir, `*/runtime/**/*`),
       join(relativeRootDir, `layers/*/app/**/*`),
       join(relativeRootDir, `layers/*/modules/*/runtime/**/*`),
     ],
     nitro: [
+      join(relativeServerDir, `**/*`),
       join(relativeModulesDir, `*/runtime/server/**/*`),
       join(relativeRootDir, `layers/*/server/**/*`),
       join(relativeRootDir, `layers/*/modules/*/runtime/server/**/*`),
     ],

If you want to support nested layers with custom srcDir in the globs, we can extend resolveLayerPaths or rely solely on getLayerDirectories in _generateTypes instead of hard-coded app paths.

packages/kit/src/layers.ts (3)

28-35: Cache invalidation for HMR/config changes.

WeakMap avoids leaks but cached paths can become stale if a layer’s config mutates in place (e.g. via hooks). Consider clearing cache on config extension or derive a cache key from resolved root/src.

Example addition (outside this hunk) to expose an invalidator:

// near layerMap
export function invalidateLayerDirectoriesCache () {
  layerMap && (layerMap as WeakMap<any, any>).clear?.()
}

And optionally hook once:

// when you have access to nuxt instance
nuxt.hooks.hook('config:extend', () => invalidateLayerDirectoriesCache())

62-64: Make withTrailingSlash simpler and robust post-normalisation.

After switching to pathe.resolve, inputs are POSIX-style. A fast endsWith is clearer.

Apply this diff:

-function withTrailingSlash (dir: string) {
-  return dir.replace(/[^/]$/, '$&/')
-}
+function withTrailingSlash (dir: string) {
+  return dir.endsWith('/') ? dir : (dir + '/')
+}

5-26: Naming shape: consider explicit app vs src to mirror config discussion.

To match the proposed clarity (root/server/modules/shared/public vs app-local), consider adding app and appLayouts/appMiddleware/appPages/appPlugins (keep src as alias for back-compat).

Possible type tweak:

 export interface LayerDirectories {
-  readonly src: string
+  /** App directory (`/app/` by default). Alias: `src` for back-compat. */
+  readonly app: string
+  /** Back-compat alias for `app`. */
+  readonly src: string
-  readonly layouts: string
-  readonly middleware: string
-  readonly pages: string
-  readonly plugins: string
+  readonly appLayouts: string
+  readonly appMiddleware: string
+  readonly appPages: string
+  readonly appPlugins: string
 }

And map properties accordingly while still populating the old names for transition.

📜 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 e5ef603 and 001c42f.

📒 Files selected for processing (15)
  • nuxt.config.ts (1 hunks)
  • packages/kit/src/ignore.ts (2 hunks)
  • packages/kit/src/index.ts (1 hunks)
  • packages/kit/src/layers.ts (1 hunks)
  • packages/kit/src/module/install.ts (2 hunks)
  • packages/kit/src/template.ts (4 hunks)
  • packages/nuxt/src/core/app.ts (5 hunks)
  • packages/nuxt/src/core/builder.ts (4 hunks)
  • packages/nuxt/src/core/nitro.ts (7 hunks)
  • packages/nuxt/src/core/nuxt.ts (5 hunks)
  • packages/nuxt/src/core/schema.ts (5 hunks)
  • packages/nuxt/src/imports/module.ts (2 hunks)
  • packages/nuxt/src/pages/module.ts (4 hunks)
  • packages/nuxt/src/pages/utils.ts (2 hunks)
  • packages/vite/src/vite.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (11)
  • packages/nuxt/src/pages/module.ts
  • packages/nuxt/src/core/builder.ts
  • packages/vite/src/vite.ts
  • packages/nuxt/src/core/nitro.ts
  • packages/kit/src/index.ts
  • packages/nuxt/src/core/schema.ts
  • packages/kit/src/module/install.ts
  • packages/nuxt/src/pages/utils.ts
  • packages/kit/src/ignore.ts
  • packages/nuxt/src/imports/module.ts
  • packages/nuxt/src/core/nuxt.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:

  • nuxt.config.ts
  • packages/nuxt/src/core/app.ts
  • packages/kit/src/layers.ts
  • packages/kit/src/template.ts
🧠 Learnings (4)
📚 Learning: 2025-07-18T16:46:07.446Z
Learnt from: CR
PR: nuxt/nuxt#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-07-18T16:46:07.446Z
Learning: Applies to **/*.{test,spec}.{ts,tsx,js,jsx} : Write unit tests for core functionality using `vitest`

Applied to files:

  • nuxt.config.ts
📚 Learning: 2025-07-18T16:46:07.446Z
Learnt from: CR
PR: nuxt/nuxt#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-07-18T16:46:07.446Z
Learning: Applies to **/*.vue : Use `<script setup lang="ts">` and the composition API when creating Vue components

Applied to files:

  • nuxt.config.ts
📚 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/core/app.ts
  • packages/kit/src/template.ts
📚 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:

  • packages/nuxt/src/core/app.ts
  • packages/kit/src/layers.ts
🧬 Code graph analysis (3)
packages/nuxt/src/core/app.ts (3)
packages/kit/src/layers.ts (1)
  • getLayerDirectories (30-60)
packages/kit/src/resolve.ts (2)
  • findPath (68-86)
  • resolveFiles (277-285)
packages/nuxt/src/core/utils/names.ts (1)
  • getNameFromPath (6-14)
packages/kit/src/layers.ts (1)
packages/schema/src/types/config.ts (2)
  • NuxtConfigLayer (68-74)
  • NuxtOptions (81-93)
packages/kit/src/template.ts (2)
packages/kit/src/layers.ts (2)
  • LayerDirectories (5-26)
  • getLayerDirectories (30-60)
packages/kit/src/index.ts (2)
  • LayerDirectories (15-15)
  • getLayerDirectories (14-14)
⏰ 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). (6)
  • GitHub Check: test-fixtures (windows-latest, built, vite, async, manifest-off, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, built, vite, async, manifest-on, js, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, built, webpack, default, manifest-on, json, lts/-1)
  • GitHub Check: release-pkg-pr-new
  • GitHub Check: test-size
  • GitHub Check: code
🔇 Additional comments (9)
nuxt.config.ts (1)

56-60: TS include for test sources looks good.

Including ./test/nuxt via fileURLToPath is fine and will be relativised later during type generation.

packages/nuxt/src/core/app.ts (4)

4-4: Importing getLayerDirectories is appropriate.

Good move away from private nuxt.options._layers access.


138-141: Using cached layer directories with a reversed copy is sensible.

Establishes a clear basis for precedence decisions.


188-199: Plugins order logic LGTM.

Iterating reversedLayerDirs with first-wins de-dupe correctly prioritises user plugins.


217-219: Confirm desired ordering for app.config layering.

You collect in base→user order. If downstream merge expects user-last, this is fine; if first-wins is used anywhere, reverse here.

Would you like me to add a small unit test asserting user app.config overrides base?

packages/kit/src/template.ts (3)

19-21: New layer API imports look correct.


232-235: Switch to getLayerDirectories for source dirs is a solid simplification.


257-260: Path selection condition merits a quick sanity check.

The guard includes the root layer and external layers; nested in-repo layers are skipped. Confirm this matches the intended include/exclude strategy for type scanning.

I can add tests covering: root layer, nested in-repo layer, external node_modules layer, and a layer with custom srcDir.

packages/kit/src/layers.ts (1)

48-50: Switch base of server, modules, and public to root – aligns with v4 semantics; downstream code contains no references to the old src-based directories.

Comment on lines 156 to 160
for (const dirs of layerDirs) {
const layoutFiles = await resolveFiles(dirs.layouts, `**/*{${extensionGlob}}`)
for (const file of layoutFiles) {
const name = getNameFromPath(file, resolve(config.srcDir, layoutDir))
const name = getNameFromPath(file, dirs.layouts)
if (!name) {
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Layouts precedence: ensure user overrides base.

Scanning base→user combined with ||= makes base win. Either scan user→base or overwrite unconditionally.

Apply one of:

  • Reverse the scan (minimal change):
-for (const dirs of layerDirs) {
+for (const dirs of reversedLayerDirs) {
  • Or keep order and overwrite:
-      layouts[name] ||= { name, file }
+      layouts[name] = { name, file }
📝 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
for (const dirs of layerDirs) {
const layoutFiles = await resolveFiles(dirs.layouts, `**/*{${extensionGlob}}`)
for (const file of layoutFiles) {
const name = getNameFromPath(file, resolve(config.srcDir, layoutDir))
const name = getNameFromPath(file, dirs.layouts)
if (!name) {
for (const dirs of reversedLayerDirs) {
const layoutFiles = await resolveFiles(dirs.layouts, `**/*{${extensionGlob}}`)
for (const file of layoutFiles) {
const name = getNameFromPath(file, dirs.layouts)
if (!name) {
🤖 Prompt for AI Agents
In packages/nuxt/src/core/app.ts around lines 156 to 160, the current layout
load loop scans layers in base→user order and uses a conditional assignment that
prevents user layouts from overriding base ones; change behavior so user
overrides win by either reversing the layerDirs iteration (scan user→base) or by
keeping the current iteration order but replace the conditional assignment with
an unconditional overwrite when a layout name is encountered, ensuring later
(user) layers override earlier (base) layers.

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 (2)
packages/nuxt/src/core/nuxt.ts (2)

262-269: Optional: also look for declarations in src as a fallback

Some layers may historically place index.d.ts under srcDir. Consider checking dirs.src as a fallback for smoother v3→v4 transitions.

Proposed change:

-    for (const dirs of layerDirs) {
-      const declaration = join(dirs.root, 'index.d.ts')
-      if (existsSync(declaration)) {
-        opts.references.push({ path: declaration })
-        opts.nodeReferences.push({ path: declaration })
-        opts.sharedReferences.push({ path: declaration })
-      }
-    }
+    for (const dirs of layerDirs) {
+      const candidates = [join(dirs.root, 'index.d.ts'), join(dirs.src, 'index.d.ts')]
+      for (const declaration of candidates) {
+        if (existsSync(declaration)) {
+          opts.references.push({ path: declaration })
+          opts.nodeReferences.push({ path: declaration })
+          opts.sharedReferences.push({ path: declaration })
+          break
+        }
+      }
+    }

681-696: Nit: reuse cached layerDirs and normalise string patterns

  • Reuse the already computed layerDirs instead of re-calling getLayerDirectories(nuxt); it’s cached, but avoiding the call is cleaner.
  • Normalise string watch patterns before comparing, to be resilient to path separator differences.

Suggested tweak:

-    const layerRelativePaths = new Set(getLayerDirectories(nuxt).map(l => relative(l.src, path)))
+    const normalizedPath = normalize(path)
+    const layerRelativePaths = new Set(layerDirs.map(l => relative(l.src, normalizedPath)))
     for (const pattern of nuxt.options.watch) {
       if (typeof pattern === 'string') {
-        // Test (normalized) strings against absolute path and relative path to any layer `srcDir`
-        if (pattern === path || layerRelativePaths.has(pattern)) { return nuxt.callHook('restart') }
+        const p = normalize(pattern)
+        // Test normalised strings against absolute path and relative path to any layer `srcDir`
+        if (p === normalizedPath || layerRelativePaths.has(p)) { return nuxt.callHook('restart') }
         continue
       }
📜 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 001c42f and c904bcd.

📒 Files selected for processing (3)
  • packages/kit/src/layers.ts (1 hunks)
  • packages/nuxt/src/core/app.ts (5 hunks)
  • packages/nuxt/src/core/nuxt.ts (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/nuxt/src/core/app.ts
  • packages/kit/src/layers.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:

  • packages/nuxt/src/core/nuxt.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/core/nuxt.ts
🧬 Code graph analysis (1)
packages/nuxt/src/core/nuxt.ts (1)
packages/kit/src/layers.ts (1)
  • getLayerDirectories (30-60)
⏰ 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). (11)
  • GitHub Check: test-fixtures (windows-latest, built, vite, async, manifest-off, json, lts/-1)
  • GitHub Check: test-fixtures (windows-latest, dev, vite, async, manifest-off, 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, default, manifest-off, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, built, vite, async, manifest-off, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, built, vite, async, manifest-on, js, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, dev, vite, async, manifest-off, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, dev, vite, default, manifest-off, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, dev, vite, async, manifest-on, js, lts/-1)
  • GitHub Check: test-benchmark
  • GitHub Check: typecheck (windows-latest, bundler)
🔇 Additional comments (3)
packages/nuxt/src/core/nuxt.ts (3)

9-9: LGTM: import of getLayerDirectories and related kit utilities

The expanded import set aligns with the new utility usage and the rest of this file. No issues spotted.


160-162: Good: centralised layer directory lookup

Initialising layerDirs via getLayerDirectories(nuxt) early is sound and matches the PR goal of avoiding direct _layers access.


418-419: LGTM: transpile node_modules-backed layers

Filtering layerDirs by i.root.includes('node_modules') and pushing the roots to build.transpile is correct; later normalisation (Line 712) keeps paths consistent across platforms.

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 (5)
docs/3.api/5.kit/16.layers.md (5)

103-125: Example 1: Prefer layer.app and minor ESM nit

Use layer.app (if that’s the final field name), and consider node:-prefixed built-ins elsewhere for consistency.

-      const files = await globby('**/*.vue', {
-        cwd: resolve(layer.src, 'components'),
+      const files = await globby('**/*.vue', {
+        cwd: resolve(layer.app, 'components'),
         absolute: true
       })

130-147: Example 2: Use layer.app and prefer node:fs

Align with the API and Node ESM import style.

-import { existsSync } from 'fs'
+import { existsSync } from 'node:fs'
@@
-      const configPath = resolve(layer.src, 'my-module.config.ts')
+      const configPath = resolve(layer.app, 'my-module.config.ts')

155-186: Example 3: Avoid mutating with reverse() and use app fields

  • reverse() mutates; prefer a non-mutating approach.
  • Update to layer.app and Node ESM imports.
-import { existsSync, readFileSync } from 'fs'
+import { existsSync, readFileSync } from 'node:fs'
@@
-      const configPath = resolve(layer.src, 'my-config.json')
+      const configPath = resolve(layer.app, 'my-config.json')
@@
-    for (const layer of layerDirs.reverse()) { // Process from lowest to highest priority
-      const configPath = resolve(layer.src, 'my-config.json')
+    for (const layer of [...layerDirs].reverse()) { // Process from lowest to highest priority
+      const configPath = resolve(layer.app, 'my-config.json')

191-207: Example 4: Use layer.app and prefer node:fs

-import { existsSync } from 'fs'
+import { existsSync } from 'node:fs'
@@
-    const layersWithAssets = layerDirs.filter(layer => {
-      return existsSync(resolve(layer.src, 'assets'))
-    })
+    const layersWithAssets = layerDirs.filter(layer => {
+      return existsSync(resolve(layer.app, 'assets'))
+    })

17-37: Add a brief note on call context

Clarify that getLayerDirectories should be called within a Nuxt context (e.g., inside defineNuxtModule setup) or with an explicit nuxt instance.

📜 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 c904bcd and d02766f.

📒 Files selected for processing (2)
  • docs/3.api/5.kit/16.layers.md (1 hunks)
  • packages/kit/src/layers.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/kit/src/layers.ts
🧰 Additional context used
🪛 LanguageTool
docs/3.api/5.kit/16.layers.md

[uncategorized] ~83-~83: Loose punctuation mark.
Context: ...rom base layers. LayerDirectories: An object containing the resolved direc...

(UNLIKELY_OPENING_PUNCTUATION)


[uncategorized] ~208-~208: Loose punctuation mark.
Context: ...ers with assets directory) } }) ``` ::note The getLayerDirectories` function...

(UNLIKELY_OPENING_PUNCTUATION)


[uncategorized] ~210-~210: Loose punctuation mark.
Context: ...performance when called multiple times. :: ::note Directory paths returned by th...

(UNLIKELY_OPENING_PUNCTUATION)


[uncategorized] ~212-~212: Loose punctuation mark.
Context: ...ormance when called multiple times. :: ::note Directory paths returned by this f...

(UNLIKELY_OPENING_PUNCTUATION)


[uncategorized] ~214-~214: Loose punctuation mark.
Context: ...clude a trailing slash for consistency. ::

(UNLIKELY_OPENING_PUNCTUATION)

⏰ 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). (1)
  • GitHub Check: codeql (javascript-typescript)
🔇 Additional comments (4)
docs/3.api/5.kit/16.layers.md (4)

70-82: Layer priority explanation reads clearly

The ordering and precedence rules are unambiguous and match Nuxt expectations.


209-215: Admonition syntax looks correct for MDC

The ::note blocks and closers (::) are consistent; LanguageTool’s “loose punctuation” warning can be ignored.


41-66: No renaming needed: docs reflect implementation. The LayerDirectories interface in packages/kit/src/layers.ts uses src, layouts, middleware, pages and plugins, matching the documentation.

Likely an incorrect or invalid review comment.


213-215: Trailing slash invariant confirmed — the withTrailingSlash helper (used for every directory in getLayerDirectories) guarantees a trailing slash, so the documentation note is accurate.

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 (2)
packages/nuxt/src/core/schema.ts (1)

72-77: Parcel watcher ignores are incorrect; filter events in callback

@parcel/watcher doesn’t support negated globs like '!nuxt.schema.*'. As written, it won’t narrow events to nuxt.schema.*, potentially causing excessive rebuilds. Also, the callback should filter by path.

Apply:

-          for (const dirs of layerDirs) {
-            const subscription = await subscribe(dirs.root, onChange, {
-              ignore: ['!nuxt.schema.*'],
-            })
+          for (const dirs of layerDirs) {
+            const subscription = await subscribe(dirs.root, (_err, events) => {
+              if (events?.some(e => /(?:^|\/)nuxt\.schema\.\w+$/.test(e.path))) {
+                onChange()
+              }
+            }, {
+              // keep noise down
+              ignore: [/[\\/]node_modules[\\/]/],
+            })
             nuxt.hook('close', () => subscription.unsubscribe())
           }
packages/kit/src/template.ts (1)

171-211: Exclude main server directory from app TS config and type the return.

nitro globs don’t exclude the current layer’s server dir (only layer/embedded module patterns), so src/server/**/* may leak into tsconfig.app via nuxt includes. Also, the function lacks an explicit return type, and node uses a shallow *.* for local modules while layer modules use a deep **/* (inconsistent).

  • Add relativeServerDir and exclude it in nitro.
  • Provide an explicit return type.
  • Make local modules glob deep to match layered modules.

Apply this diff:

-export function resolveLayerPaths (dirs: LayerDirectories, projectBuildDir: string) {
+export function resolveLayerPaths (dirs: LayerDirectories, projectBuildDir: string): {
+  nuxt: string[]
+  nitro: string[]
+  node: string[]
+  shared: string[]
+  sharedDeclarations: string[]
+  globalDeclarations: string[]
+} {
   const relativeRootDir = relativeWithDot(projectBuildDir, dirs.root)
   const relativeSrcDir = relativeWithDot(projectBuildDir, dirs.src)
   const relativeModulesDir = relativeWithDot(projectBuildDir, dirs.modules)
   const relativeSharedDir = relativeWithDot(projectBuildDir, dirs.shared)
+  const relativeServerDir = relativeWithDot(projectBuildDir, dirs.server)
   return {
     nuxt: [
       join(relativeSrcDir, '**/*'),
       join(relativeModulesDir, `*/runtime/**/*`),
       join(relativeRootDir, `layers/*/app/**/*`),
       join(relativeRootDir, `layers/*/modules/*/runtime/**/*`),
     ],
     nitro: [
+      join(relativeServerDir, `**/*`),
       join(relativeModulesDir, `*/runtime/server/**/*`),
       join(relativeRootDir, `layers/*/server/**/*`),
       join(relativeRootDir, `layers/*/modules/*/runtime/server/**/*`),
     ],
     node: [
-      join(relativeModulesDir, `*.*`),
+      join(relativeModulesDir, `**/*`),
       join(relativeRootDir, `nuxt.config.*`),
       join(relativeRootDir, `.config/nuxt.*`),
       join(relativeRootDir, `layers/*/nuxt.config.*`),
       join(relativeRootDir, `layers/*/.config/nuxt.*`),
       join(relativeRootDir, `layers/*/modules/**/*`),
     ],
♻️ Duplicate comments (3)
packages/nuxt/src/core/nuxt.ts (1)

422-430: Fix local layers detection and root comparison normalisation

Using all layers’ layers/ paths can misclassify non-local layers, and comparing dirs.root (trailing slash) to nuxt.options.rootDir can fail. Restrict to the project’s ./layers/ and normalise with trailing slashes.

-  const locallyScannedLayersDirs = layerDirs.map(l => join(l.root, 'layers/'))
-  for (const dirs of layerDirs) {
-    if (normalize(dirs.root) === normalize(nuxt.options.rootDir)) {
-      continue
-    }
-    if (locallyScannedLayersDirs.every(dir => !dirs.root.startsWith(dir))) {
-      nuxt.options.modulesDir.push(join(dirs.root, 'node_modules'))
-    }
-  }
+  const locallyScannedLayersDir = withTrailingSlash(join(nuxt.options.rootDir, 'layers'))
+  for (const dirs of layerDirs) {
+    // Skip the project root layer
+    if (withTrailingSlash(dirs.root) === withTrailingSlash(nuxt.options.rootDir)) {
+      continue
+    }
+    // Only add node_modules for layers not under the project's ./layers/*
+    if (!withTrailingSlash(dirs.root).startsWith(locallyScannedLayersDir)) {
+      nuxt.options.modulesDir.push(join(dirs.root, 'node_modules'))
+    }
+  }

Optional: deduplicate afterwards.

nuxt.options.modulesDir = Array.from(new Set(nuxt.options.modulesDir))
packages/kit/src/layers.ts (2)

49-51: Make root-layer detection robust (normalise cwd vs rootDir).

Comparing config.rootDir to nuxt.options.rootDir can be brittle; use layer.cwd normalised (and compare to nuxt.options.rootDir) to avoid trailing-slash/relative differences.

Apply this diff:

-    const isRoot = normalize(layer.config.rootDir) === normalize(nuxt.options.rootDir)
+    const isRoot = normalize(layer.cwd) === normalize(nuxt.options.rootDir)

52-54: Resolve rootDir/srcDir relative to the layer root.

Relative rootDir/srcDir should be resolved against layer.cwd. Also compute root before src so src can be relative to root.

Apply this diff:

-    const src = withTrailingSlash(config.srcDir || layer.cwd)
-    const root = withTrailingSlash(config.rootDir || layer.cwd)
+    const root = withTrailingSlash(resolve(layer.cwd, config.rootDir || '.'))
+    const src = withTrailingSlash(resolve(root, config.srcDir || '.'))
🧹 Nitpick comments (4)
docs/3.api/5.kit/16.layers.md (1)

85-97: Document resolution base nuances (v3 vs v4) for directories

Server/modules/public are resolved relative to srcDir for v3 back-compat in code; v4 leans root-relative. Add a note to reduce confusion.

 | `public`     | `string` | The public directory for static assets                                         |
 | `layouts`    | `string` | The layouts directory for Vue layout components                                |
 | `middleware` | `string` | The middleware directory for route middleware                                  |
 | `pages`      | `string` | The pages directory for file-based routing                                    |
 | `plugins`    | `string` | The plugins directory for Nuxt plugins                                        |
+
+> Note
+> For Nuxt 3 compatibility, `server`, `modules` and `public` are currently resolved relative to `srcDir`. In Nuxt 4 they are treated as root-relative by default. `getLayerDirectories` resolves paths accordingly for each layer.
packages/nuxt/src/core/nuxt.ts (1)

681-683: Optional: avoid recomputing layer dirs on every event

If layers don’t change during a watch session, reuse layerDirs for this handler. Not critical.

-    const layerRelativePaths = new Set(getLayerDirectories(nuxt).map(l => relative(l.src, path)))
+    const layerRelativePaths = new Set(layerDirs.map(l => relative(l.src, path)))
packages/kit/src/template.ts (1)

256-260: Remove unused rootGlob or include it intentionally.

rootGlob is computed but never added to any include/exclude set; the conditional if (path !== rootGlob) will always be true. Either drop the variable and condition or add rootGlob to paths.nuxt.

Apply this diff to simplify:

-    if (!dirs.src.startsWith(rootDirWithSlash) || normalize(dirs.root) === normalize(nuxt.options.rootDir) || dirs.src.includes('node_modules')) {
-      const rootGlob = join(relativeWithDot(nuxt.options.buildDir, dirs.root), '**/*')
+    if (!dirs.src.startsWith(rootDirWithSlash) || normalize(dirs.root) === normalize(nuxt.options.rootDir) || dirs.src.includes('node_modules')) {
       const paths = resolveLayerPaths(dirs, nuxt.options.buildDir)
       for (const path of paths.nuxt) {
         include.add(path)
         legacyInclude.add(path)
-        if (path !== rootGlob) {
-          nodeExclude.add(path)
-        }
+        nodeExclude.add(path)
       }
packages/kit/src/layers.ts (1)

75-77: Reuse ufo’s withTrailingSlash instead of a local helper.

The local regex doesn’t handle edge cases (e.g. empty strings, backslashes). ufo is already used elsewhere and is cross-platform.

Apply these diffs:

@@
-import type { NuxtConfigLayer, NuxtOptions } from '@nuxt/schema'
-import { normalize, resolve } from 'pathe'
+import type { NuxtConfigLayer, NuxtOptions } from '@nuxt/schema'
+import { normalize, resolve } from 'pathe'
+import { withTrailingSlash } from 'ufo'
@@
-function withTrailingSlash (dir: string) {
-  return dir.replace(/[^/]$/, '$&/')
-}
+// use `ufo`'s withTrailingSlash for consistent behaviour
📜 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 d02766f and bc6b5f9.

📒 Files selected for processing (5)
  • docs/3.api/5.kit/16.layers.md (1 hunks)
  • packages/kit/src/layers.ts (1 hunks)
  • packages/kit/src/template.ts (4 hunks)
  • packages/nuxt/src/core/nuxt.ts (6 hunks)
  • packages/nuxt/src/core/schema.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/kit/src/layers.ts
  • packages/nuxt/src/core/schema.ts
  • packages/kit/src/template.ts
  • packages/nuxt/src/core/nuxt.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:

  • packages/kit/src/layers.ts
  • packages/nuxt/src/core/nuxt.ts
📚 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/core/schema.ts
  • packages/kit/src/template.ts
  • packages/nuxt/src/core/nuxt.ts
🧬 Code graph analysis (4)
packages/kit/src/layers.ts (1)
packages/schema/src/types/config.ts (2)
  • NuxtConfigLayer (68-74)
  • NuxtOptions (81-93)
packages/nuxt/src/core/schema.ts (1)
packages/kit/src/layers.ts (1)
  • getLayerDirectories (43-73)
packages/kit/src/template.ts (2)
packages/kit/src/layers.ts (2)
  • LayerDirectories (7-28)
  • getLayerDirectories (43-73)
packages/kit/src/index.ts (2)
  • LayerDirectories (15-15)
  • getLayerDirectories (14-14)
packages/nuxt/src/core/nuxt.ts (2)
packages/kit/src/layers.ts (1)
  • getLayerDirectories (43-73)
packages/kit/src/resolve.ts (1)
  • resolveFiles (277-285)
🪛 LanguageTool
docs/3.api/5.kit/16.layers.md

[uncategorized] ~83-~83: Loose punctuation mark.
Context: ...rom base layers. LayerDirectories: An object containing the resolved direc...

(UNLIKELY_OPENING_PUNCTUATION)


[uncategorized] ~208-~208: Loose punctuation mark.
Context: ...ers with assets directory) } }) ``` ::note The getLayerDirectories` function...

(UNLIKELY_OPENING_PUNCTUATION)


[uncategorized] ~210-~210: Loose punctuation mark.
Context: ...performance when called multiple times. :: ::note Directory paths returned by th...

(UNLIKELY_OPENING_PUNCTUATION)


[uncategorized] ~212-~212: Loose punctuation mark.
Context: ...ormance when called multiple times. :: ::note Directory paths returned by this f...

(UNLIKELY_OPENING_PUNCTUATION)


[uncategorized] ~214-~214: Loose punctuation mark.
Context: ...clude a trailing slash for consistency. ::

(UNLIKELY_OPENING_PUNCTUATION)

🔇 Additional comments (12)
packages/nuxt/src/core/schema.ts (4)

8-8: LGTM: import change

Importing getLayerDirectories here is appropriate.


58-59: LGTM: use cached layer directories

Grabbing layerDirs once per module setup is fine (Nuxt restarts on layer add/remove).


85-97: LGTM: chokidar watcher targets only nuxt.schema. at layer roots*

The ignored predicate with SCHEMA_RE keeps this lightweight.


110-112: LGTM: per-layer schema resolution

Resolving nuxt.schema from each dirs.root matches the new layering API.

docs/3.api/5.kit/16.layers.md (1)

19-37: Confirm property names match the final public API

Examples use src/pages/layouts/plugins. The PR discussion mentions a possible rename to app/appPages/appLayouts/appPlugins. Ensure the docs reflect whatever ships in packages/kit/src/layers.ts at merge time.

Do you want me to update the examples automatically once the final naming is confirmed?

packages/nuxt/src/core/nuxt.ts (4)

9-9: LGTM: import getLayerDirectories

Consistent with broader refactor.


160-162: LGTM: compute layerDirs once in init

Matches usage pattern elsewhere.


262-269: LGTM: include per-layer index.d.ts

Good DX; references added across node/shared.


418-420: LGTM: transpile layers under node_modules

Normalised later via normalize, so OK cross-platform.

packages/kit/src/template.ts (2)

19-21: Imports look good.

Publicly importing getLayerDirectories and its type keeps usage consistent across kit. No concerns.


232-235: Good switch to getLayerDirectories.

Deriving sourceDirs from computed layer dirs de-dupes normalisation and aligns with the new API.

packages/kit/src/layers.ts (1)

7-28: Interface shape is clear and useful.

Readonly path strings with explicit comments are great for module authors. No issues.

Comment on lines 921 to 926
// Secondly automatically register modules from layer's module directory
const modulesDir = (config.rootDir === nuxt.options.rootDir ? nuxt.options.dir : config.dir)?.modules || 'modules'
const layerModules = await resolveFiles(config.srcDir, [
`${modulesDir}/*{${nuxt.options.extensions.join(',')}}`,
`${modulesDir}/*/index{${nuxt.options.extensions.join(',')}}`,
const modulesDir = resolve(config.srcDir, (config.rootDir === nuxt.options.rootDir ? nuxt.options.dir : config.dir)?.modules || 'modules')
const layerModules = await resolveFiles(modulesDir, [
`*{${nuxt.options.extensions.join(',')}}`,
`*/index{${nuxt.options.extensions.join(',')}}`,
])
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Normalise rootDir comparison when resolving layer modules directory

Comparing config.rootDir against nuxt.options.rootDir without normalisation can pick the wrong dir source and resolve the wrong path.

-    const modulesDir = resolve(config.srcDir, (config.rootDir === nuxt.options.rootDir ? nuxt.options.dir : config.dir)?.modules || 'modules')
+    const isRootLayer = normalize(config.rootDir) === normalize(nuxt.options.rootDir)
+    const baseDir = (isRootLayer ? nuxt.options.dir : config.dir)?.modules || 'modules'
+    const modulesDir = resolve(config.srcDir, baseDir)
📝 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
// Secondly automatically register modules from layer's module directory
const modulesDir = (config.rootDir === nuxt.options.rootDir ? nuxt.options.dir : config.dir)?.modules || 'modules'
const layerModules = await resolveFiles(config.srcDir, [
`${modulesDir}/*{${nuxt.options.extensions.join(',')}}`,
`${modulesDir}/*/index{${nuxt.options.extensions.join(',')}}`,
const modulesDir = resolve(config.srcDir, (config.rootDir === nuxt.options.rootDir ? nuxt.options.dir : config.dir)?.modules || 'modules')
const layerModules = await resolveFiles(modulesDir, [
`*{${nuxt.options.extensions.join(',')}}`,
`*/index{${nuxt.options.extensions.join(',')}}`,
])
// Secondly automatically register modules from layer's module directory
const isRootLayer = normalize(config.rootDir) === normalize(nuxt.options.rootDir)
const baseDir = (isRootLayer ? nuxt.options.dir : config.dir)?.modules || 'modules'
const modulesDir = resolve(config.srcDir, baseDir)
const layerModules = await resolveFiles(modulesDir, [
`*{${nuxt.options.extensions.join(',')}}`,
`*/index{${nuxt.options.extensions.join(',')}}`,
])
🤖 Prompt for AI Agents
In packages/nuxt/src/core/nuxt.ts around lines 921 to 926, the comparison of
config.rootDir to nuxt.options.rootDir is done without normalising paths which
can lead to choosing the wrong dir and resolving an incorrect modules path;
update the comparison to normalise/resolve both paths (e.g. use path.resolve or
path.normalize on both config.rootDir and nuxt.options.rootDir) before
comparing, so the ternary picks the correct dir value, then keep the rest of the
modulesDir resolution unchanged.

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)
packages/kit/src/template.ts (1)

171-211: Exclude each layer’s own server dir from app TS includes

Without this, server code from non-root layers can leak into app TS context.

   return {
     nuxt: [
       join(relativeSrcDir, '**/*'),
       join(relativeModulesDir, `*/runtime/**/*`),
       join(relativeRootDir, `layers/*/app/**/*`),
       join(relativeRootDir, `layers/*/modules/*/runtime/**/*`),
     ],
     nitro: [
+      join(relativeSrcDir, `server/**/*`),
       join(relativeModulesDir, `*/runtime/server/**/*`),
       join(relativeRootDir, `layers/*/server/**/*`),
       join(relativeRootDir, `layers/*/modules/*/runtime/server/**/*`),
     ],
♻️ Duplicate comments (5)
packages/nuxt/src/core/nuxt.ts (2)

422-430: Normalise root comparison and simplify local layers exclusion.
Use a single locally-scanned layers path and normalise with trailing slashes to skip the project root reliably.

-  const locallyScannedLayersDirs = layerDirs.map(l => join(l.root, 'layers/'))
-  for (const dirs of layerDirs) {
-    if (normalize(dirs.root) === normalize(nuxt.options.rootDir)) {
-      continue
-    }
-    if (locallyScannedLayersDirs.every(dir => !dirs.root.startsWith(dir))) {
-      nuxt.options.modulesDir.push(join(dirs.root, 'node_modules'))
-    }
-  }
+  const locallyScannedLayersDir = join(withTrailingSlash(nuxt.options.rootDir), 'layers/')
+  for (const dirs of layerDirs) {
+    if (withTrailingSlash(dirs.root) === withTrailingSlash(nuxt.options.rootDir)) { continue }
+    if (!dirs.root.startsWith(locallyScannedLayersDir)) {
+      nuxt.options.modulesDir.push(join(dirs.root, 'node_modules'))
+    }
+  }

921-926: Normalise root comparison when resolving modulesDir.
Prevents choosing the wrong base dir.

-    const modulesDir = resolve(config.srcDir, (config.rootDir === nuxt.options.rootDir ? nuxt.options.dir : config.dir)?.modules || 'modules')
+    const isRootLayer = normalize(config.rootDir) === normalize(nuxt.options.rootDir)
+    const baseDir = (isRootLayer ? nuxt.options.dir : config.dir)?.modules || 'modules'
+    const modulesDir = resolve(config.srcDir, baseDir)
packages/kit/src/layers.ts (3)

49-51: Root-layer detection should use cwd (normalised).
Comparing config.rootDir can be brittle; prefer layer.cwd vs nuxt.options.rootDir.

-    const isRoot = normalize(layer.config.rootDir) === normalize(nuxt.options.rootDir)
+    const isRoot = normalize(layer.cwd) === normalize(nuxt.options.rootDir)

52-54: Resolve root/src relative to the layer root.
Relative values should be resolved from layer.cwd, and src from the resolved root.

-    const src = withTrailingSlash(config.srcDir || layer.cwd)
-    const root = withTrailingSlash(config.rootDir || layer.cwd)
+    const root = withTrailingSlash(resolve(layer.cwd, config.rootDir || '.'))
+    const src = withTrailingSlash(resolve(root, config.srcDir || '.'))

55-63: Bug: server should be based on root (Nuxt v4 semantics).
Using src can misplace the server dir when srcDir ≠ rootDir.

-      // these are resolved relative to root in `@nuxt/schema` for v4+
-      // so resolving relative to `src` covers backward compatibility for v3
-      server: withTrailingSlash(resolve(src, resolveAlias(config.serverDir || 'server', nuxt.options.alias))),
+      // In v4, serverDir is resolved from root
+      server: withTrailingSlash(resolve(root, resolveAlias(config.serverDir || 'server', nuxt.options.alias))),
🧹 Nitpick comments (14)
packages/vite/src/vite.ts (2)

37-41: Normalise paths to avoid duplicate or missed pruning due to trailing slashes.

getLayerDirectories().map(d => d.root) returns paths with a trailing slash, while others (e.g. modulesDir, workspaceDir) may not. The subsequent startsWith pruning is order- and slash-sensitive and may leave near-duplicates (e.g. /a vs /a/). Normalise before pruning.

   let allowDirs = [
     nuxt.options.appDir,
     nuxt.options.workspaceDir,
     ...nuxt.options.modulesDir,
-    ...getLayerDirectories(nuxt).map(d => d.root),
+    ...getLayerDirectories(nuxt).map(d => d.root),
     ...Object.values(nuxt.apps).flatMap(app => [
       ...app.components.map(c => dirname(c.filePath)),
       ...app.plugins.map(p => dirname(p.src)),
       ...app.middleware.map(m => dirname(m.path)),
       ...Object.values(app.layouts || {}).map(l => dirname(l.file)),
       dirname(nuxt.apps.default!.rootComponent!),
       dirname(nuxt.apps.default!.errorComponent!),
     ]),
   ].filter(d => d && existsSync(d))
 
+  // Ensure consistent comparison (remove trailing slash, normalise separators)
+  allowDirs = allowDirs.map(d => normalize(d).replace(/\/$/, ''))
+
   for (const dir of allowDirs) {
     allowDirs = allowDirs.filter(d => !d.startsWith(dir) || d === dir)
   }

Also applies to: 50-55


169-177: Make root/app comparisons robust; drop redundant equality check.

dirs.app is normalised with a trailing slash; nuxt.options.srcDir typically is not. The equality check can be dropped; the startsWith(delimitedRootDir) guard already excludes the root layer. Also normalise both sides for cross-platform safety.

-  const delimitedRootDir = nuxt.options.rootDir + '/'
-  for (const dirs of getLayerDirectories(nuxt)) {
-    if (dirs.app !== nuxt.options.srcDir && !dirs.app.startsWith(delimitedRootDir)) {
-      layerDirs.push(dirs.app)
-    }
-  }
+  const delimitedRootDir = withTrailingSlash(normalize(nuxt.options.rootDir))
+  for (const dirs of getLayerDirectories(nuxt)) {
+    const appDir = withTrailingSlash(normalize(dirs.app))
+    if (!appDir.startsWith(delimitedRootDir)) {
+      layerDirs.push(appDir)
+    }
+  }
packages/nuxt/src/pages/module.ts (2)

88-89: Consider de-duplicating pagesDirs.

Multiple layers can point to the same appPages directory (e.g. symlinks). It’s harmless but easy to dedupe.

-    const pagesDirs = getLayerDirectories(nuxt).map(dirs => dirs.appPages)
+    const pagesDirs = Array.from(new Set(getLayerDirectories(nuxt).map(dirs => dirs.appPages)))

307-313: Use a Set to avoid redundant checks and tiny perf win.

Flattened array may include duplicates across layers. Using a Set reduces repeated startsWith checks during rebuilds.

-    const updateTemplatePaths = getLayerDirectories(nuxt)
-      .flatMap(dirs => [
-        dirs.appPages,
-        dirs.appLayouts,
-        dirs.appMiddleware,
-      ])
+    const updateTemplatePaths = new Set(
+      getLayerDirectories(nuxt).flatMap(dirs => [
+        dirs.appPages,
+        dirs.appLayouts,
+        dirs.appMiddleware,
+      ]),
+    )

And adjust the usage below:

-      if (shouldAlwaysRegenerate || updateTemplatePaths.some(dir => path.startsWith(dir))) {
+      if (shouldAlwaysRegenerate || Array.from(updateTemplatePaths).some(dir => path.startsWith(dir))) {
packages/nuxt/src/core/builder.ts (2)

27-29: Path check is fine; consider normalisation for safety.

relative(dirs.app, path) is robust, but given dirs.app has a trailing slash, normalising both occasionally avoids edge cases on Windows or unusual separators.

-          const relativePath = relative(dirs.app, path)
+          const relativePath = relative(normalize(dirs.app), normalize(path))

248-254: Skip ignored app dirs early and normalise paths.

Good to use isIgnored(dirs.app); add normalisation to avoid platform-specific mismatches and keep the set canonical.

-  for (const dirs of getLayerDirectories(nuxt)) {
-    if (!dirs.app || isIgnored(dirs.app)) { continue }
-    pathsToWatch.add(dirs.app)
+  for (const dirs of getLayerDirectories(nuxt)) {
+    const appDir = normalize(dirs.app)
+    if (!appDir || isIgnored(appDir)) { continue }
+    pathsToWatch.add(appDir)
   }
packages/nuxt/src/core/nitro.ts (3)

158-159: Reuse layerDirs instead of recomputing

Avoids extra calls (even if cached).

-          ...getLayerDirectories(nuxt).map(dirs => relativeWithDot(nuxt.options.buildDir, join(dirs.shared, '**/*.d.ts'))),
+          ...layerDirs.map(dirs => relativeWithDot(nuxt.options.buildDir, join(dirs.shared, '**/*.d.ts'))),

175-178: Reuse layerDirs here as well

Minor micro-optimisation.

-      ...getLayerDirectories(nuxt)
-        .filter(dirs => existsSync(dirs.public))
-        .map(dirs => ({ dir: dirs.public })),
+      ...layerDirs
+        .filter(dirs => existsSync(dirs.public))
+        .map(dirs => ({ dir: dirs.public })),

203-204: And reuse layerDirs here

Keeps things consistent.

-        ...getLayerDirectories(nuxt).map(dirs => join(dirs.app, 'app.config')),
+        ...layerDirs.map(dirs => join(dirs.app, 'app.config')),
packages/kit/src/template.ts (1)

255-259: Normalise paths for cross‑platform checks

startsWith/includes can misfire on Windows due to backslashes.

-  const rootDirWithSlash = withTrailingSlash(nuxt.options.rootDir)
+  const rootDirWithSlash = withTrailingSlash(normalize(nuxt.options.rootDir))
   for (const dirs of layerDirs) {
-    if (!dirs.app.startsWith(rootDirWithSlash) || normalize(dirs.root) === normalize(nuxt.options.rootDir) || dirs.app.includes('node_modules')) {
+    const appPosix = normalize(dirs.app)
+    if (!appPosix.startsWith(rootDirWithSlash) || normalize(dirs.root) === normalize(nuxt.options.rootDir) || appPosix.includes('/node_modules/')) {
       const rootGlob = join(relativeWithDot(nuxt.options.buildDir, dirs.root), '**/*')
       const paths = resolveLayerPaths(dirs, nuxt.options.buildDir)
packages/nuxt/src/core/nuxt.ts (2)

418-419: Harden node_modules detection to avoid false positives.
Use a normalised path and a bounded match.

-  ...layerDirs.filter(i => i.root.includes('node_modules')).map(i => i.root),
+  ...layerDirs
+    .filter(i => normalize(i.root).includes('/node_modules/'))
+    .map(i => i.root),

681-687: Avoid recomputing layer dirs in the watch callback.
Reuse the existing layerDirs; compute relatives against those.

-    const layerRelativePaths = new Set(getLayerDirectories(nuxt).map(l => relative(l.app, path)))
+    const layerRelativePaths = new Set(layerDirs.map(l => relative(l.app, path)))

If desired, precompute once:

const layerApps = layerDirs.map(l => l.app)
// …
const layerRelativePaths = new Set(layerApps.map(app => relative(app, path)))

Also applies to: 690-695

packages/kit/src/layers.ts (2)

61-63: Confirm intended base for modules/public.
PR text says these are root-relative by default; code resolves from src. Please confirm and align.

Possible adjustment (if root-relative intended):

-      modules: withTrailingSlash(resolve(src, resolveAlias(config.dir?.modules || 'modules', nuxt.options.alias))),
-      public: withTrailingSlash(resolve(src, resolveAlias(config.dir?.public || 'public', nuxt.options.alias))),
+      modules: withTrailingSlash(resolve(root, resolveAlias(config.dir?.modules || 'modules', nuxt.options.alias))),
+      public: withTrailingSlash(resolve(root, resolveAlias(config.dir?.public || 'public', nuxt.options.alias))),

75-77: Prefer shared helper rather than custom withTrailingSlash.
Use ufo.withTrailingSlash for consistency and edge cases.

-function withTrailingSlash (dir: string) {
-  return dir.replace(/[^/]$/, '$&/')
-}
+// Prefer: import { withTrailingSlash } from 'ufo'

Add import:

import { withTrailingSlash } from 'ufo'
📜 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 1cc8f21 and 4a92365.

📒 Files selected for processing (12)
  • docs/3.api/5.kit/16.layers.md (1 hunks)
  • packages/kit/src/layers.ts (1 hunks)
  • packages/kit/src/module/install.ts (2 hunks)
  • packages/kit/src/template.ts (4 hunks)
  • packages/nuxt/src/core/app.ts (5 hunks)
  • packages/nuxt/src/core/builder.ts (4 hunks)
  • packages/nuxt/src/core/nitro.ts (7 hunks)
  • packages/nuxt/src/core/nuxt.ts (6 hunks)
  • packages/nuxt/src/imports/module.ts (2 hunks)
  • packages/nuxt/src/pages/module.ts (4 hunks)
  • packages/nuxt/src/pages/utils.ts (2 hunks)
  • packages/vite/src/vite.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/nuxt/src/pages/utils.ts
  • packages/kit/src/module/install.ts
  • packages/nuxt/src/imports/module.ts
  • packages/nuxt/src/core/app.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:

  • packages/nuxt/src/pages/module.ts
  • packages/vite/src/vite.ts
  • packages/nuxt/src/core/builder.ts
  • packages/kit/src/layers.ts
  • packages/nuxt/src/core/nitro.ts
  • packages/kit/src/template.ts
  • packages/nuxt/src/core/nuxt.ts
🧠 Learnings (2)
📚 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/pages/module.ts
  • packages/vite/src/vite.ts
  • packages/nuxt/src/core/builder.ts
  • packages/kit/src/template.ts
  • packages/nuxt/src/core/nuxt.ts
📚 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:

  • packages/nuxt/src/pages/module.ts
  • packages/kit/src/layers.ts
  • packages/nuxt/src/core/nuxt.ts
🧬 Code graph analysis (7)
packages/nuxt/src/pages/module.ts (1)
packages/kit/src/layers.ts (1)
  • getLayerDirectories (43-73)
packages/vite/src/vite.ts (1)
packages/kit/src/layers.ts (1)
  • getLayerDirectories (43-73)
packages/nuxt/src/core/builder.ts (2)
packages/kit/src/layers.ts (1)
  • getLayerDirectories (43-73)
packages/kit/src/ignore.ts (1)
  • isIgnored (14-34)
packages/kit/src/layers.ts (1)
packages/schema/src/types/config.ts (2)
  • NuxtConfigLayer (68-74)
  • NuxtOptions (81-93)
packages/nuxt/src/core/nitro.ts (1)
packages/kit/src/layers.ts (1)
  • getLayerDirectories (43-73)
packages/kit/src/template.ts (1)
packages/kit/src/layers.ts (2)
  • LayerDirectories (7-28)
  • getLayerDirectories (43-73)
packages/nuxt/src/core/nuxt.ts (2)
packages/kit/src/layers.ts (1)
  • getLayerDirectories (43-73)
packages/kit/src/resolve.ts (1)
  • resolveFiles (277-285)
🪛 LanguageTool
docs/3.api/5.kit/16.layers.md

[uncategorized] ~86-~86: Loose punctuation mark.
Context: ...rom base layers. LayerDirectories: An object containing the resolved direc...

(UNLIKELY_OPENING_PUNCTUATION)


[uncategorized] ~213-~213: Loose punctuation mark.
Context: ...ers with assets directory) } }) ``` ::note The getLayerDirectories` function...

(UNLIKELY_OPENING_PUNCTUATION)


[uncategorized] ~215-~215: Loose punctuation mark.
Context: ...performance when called multiple times. :: ::note Directory paths returned by th...

(UNLIKELY_OPENING_PUNCTUATION)


[uncategorized] ~217-~217: Loose punctuation mark.
Context: ...ormance when called multiple times. :: ::note Directory paths returned by this f...

(UNLIKELY_OPENING_PUNCTUATION)


[uncategorized] ~219-~219: Loose punctuation mark.
Context: ...clude a trailing slash for consistency. ::

(UNLIKELY_OPENING_PUNCTUATION)

🪛 GitHub Actions: docs
docs/3.api/5.kit/16.layers.md

[error] 1-1: Rejected status code (Not Found) while fetching https://github.com/nuxt/nuxt/blob/main/packages/kit/src/layers.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 (16)
packages/vite/src/vite.ts (1)

5-5: Good move to public API.

Switching to getLayerDirectories avoids relying on private _layers and centralises normalisation/caching. No issues here.

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

3-3: Consistent usage of getLayerDirectories.

Importing the helper here aligns with the PR goal and removes ad‑hoc per-layer path resolution. Looks good.


22-22: Type import trim is OK.

Dropping NuxtOptions from imports is consistent with moving logic to @nuxt/kit where needed. No action required.

packages/nuxt/src/core/builder.ts (1)

105-110: Watching all layer apps via the public API: LGTM.

Switching to getLayerDirectories(nuxt).map(d => d.app) is the right abstraction and matches ignore handling. No further changes needed.

docs/3.api/5.kit/16.layers.md (3)

19-37: Usage snippet reads well and matches the final API

Property names align with the implemented LayerDirectories shape.


41-69: Type definition section looks consistent

Matches the exported interface and defaults explained elsewhere.


214-220: Good note on trailing slashes and caching

Clear and helpful operational details.

packages/nuxt/src/core/nitro.ts (3)

11-11: Importing getLayerDirectories here is correct

Keeps Nitro initialisation aligned with the new layer API.


122-122: Using per-layer server scan dirs is spot on

scanDirs: layerDirs.map(dirs => dirs.server) matches Nitro’s expectations.


139-140: Correct source for app config files

Reads app.config from each layer’s app dir as intended.

packages/kit/src/template.ts (2)

19-21: Importing and typing the layer helpers is correct

Sets up the file to use the new LayerDirectories model cleanly.


232-235: Good: switch to getLayerDirectories for source dirs

Removes reliance on private _layers.

packages/nuxt/src/core/nuxt.ts (3)

9-9: Import changes look good.


161-162: Good: centralised layer dirs via getLayerDirectories.


262-269: Correct: discover per-layer declarations from dirs.root.

packages/kit/src/layers.ts (1)

43-47: Cache hit path is fine.

Comment on lines 36 to 41
const layerDirs = getLayerDirectories(nuxt)
const excludePaths = layerDirs.flatMap(dirs => [
dirs.root.match(NODE_MODULES_RE)?.[1],
dirs.root.match(PNPM_NODE_MODULES_RE)?.[1],
].filter((dir): dir is string => Boolean(dir)).map(dir => escapeRE(dir)))

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Make node_modules exclusion robust across OS and pnpm layouts

Current regex relies on forward slashes and may miss Windows paths. Also, collapse the pnpm and classic cases into one extraction.

-  const layerDirs = getLayerDirectories(nuxt)
-  const excludePaths = layerDirs.flatMap(dirs => [
-    dirs.root.match(NODE_MODULES_RE)?.[1],
-    dirs.root.match(PNPM_NODE_MODULES_RE)?.[1],
-  ].filter((dir): dir is string => Boolean(dir)).map(dir => escapeRE(dir)))
+  const layerDirs = getLayerDirectories(nuxt)
+  const excludePaths = layerDirs.flatMap((dirs) => {
+    const root = dirs.root.replace(/\\/g, '/')
+    // supports: node_modules/<pkg>, node_modules/.pnpm/<...>/node_modules/<pkg>
+    const m = root.match(/\/node_modules\/(?:(?:\.pnpm\/[^/]+\/)?node_modules\/)?((?:@[^/]+\/)?[^/]+)/)
+    return m?.[1] ? [escapeRE(m[1])] : []
+  })

Additionally, adjust the exclude regex to handle both separators:

// outside the hunk above
const nmSegment = String.raw`node_modules[\\/]"
const excludePattern = excludePaths.length
  ? [new RegExp(`${nmSegment}(?!${excludePaths.join('|')})`)]
  : [new RegExp(nmSegment)]

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

♻️ Duplicate comments (2)
packages/nuxt/src/core/nuxt.ts (2)

922-926: Normalise rootDir comparison to resolve correct modules dir (or use directories.modules)

Raw string equality can mis-pick the base dir. Prefer a normalised comparison; alternatively, derive directly from the new directories API.

-    const modulesDir = resolve(config.srcDir, (config.rootDir === nuxt.options.rootDir ? nuxt.options.dir : config.dir)?.modules || 'modules')
+    const isRootLayer = normalize(config.rootDir) === normalize(nuxt.options.rootDir)
+    const baseDir = (isRootLayer ? nuxt.options.dir : config.dir)?.modules || 'modules'
+    const modulesDir = resolve(config.srcDir, baseDir)

422-430: Fix local layers detection; current logic misclassifies and pollutes modulesDir

Comparing against every <layer.root>/layers/ never matches; you only need the project’s ./layers/. Also normalise the root comparison. Optionally dedupe modulesDir afterwards.

-  const locallyScannedLayersDirs = layerDirs.map(l => join(l.root, 'layers/'))
-  for (const dirs of layerDirs) {
-    if (dirs.root === withTrailingSlash(nuxt.options.rootDir)) {
-      continue
-    }
-    if (locallyScannedLayersDirs.every(dir => !dirs.root.startsWith(dir))) {
-      nuxt.options.modulesDir.push(join(dirs.root, 'node_modules'))
-    }
-  }
+  const locallyScannedLayersDir = join(nuxt.options.rootDir, 'layers/')
+  for (const dirs of layerDirs) {
+    // Skip the project root layer (normalise both sides)
+    if (withTrailingSlash(dirs.root) === withTrailingSlash(nuxt.options.rootDir)) {
+      continue
+    }
+    // Only add node_modules for layers not under the project's ./layers/*
+    if (!dirs.root.startsWith(locallyScannedLayersDir)) {
+      nuxt.options.modulesDir.push(join(dirs.root, 'node_modules'))
+    }
+  }
+  // Optional: dedupe modulesDir
+  nuxt.options.modulesDir = Array.from(new Set(nuxt.options.modulesDir))
🧹 Nitpick comments (7)
packages/nuxt/src/core/nuxt.ts (4)

19-19: Avoid duplicating withTrailingSlash helpers

Either import it from ufo for consistency or keep the local helper but document why it diverges from ufo.

Apply this diff if you prefer reusing ufo:

-import { withoutLeadingSlash } from 'ufo'
+import { withoutLeadingSlash, withTrailingSlash } from 'ufo'

418-419: Be segment-aware when detecting node_modules and dedupe

includes('node_modules') can false-match; also avoid duplicate transpile entries.

-  ...layerDirs.filter(i => i.root.includes('node_modules')).map(i => i.root),
+  ...Array.from(new Set(
+    layerDirs
+      .filter(i => i.root.includes('/node_modules/'))
+      .map(i => i.root)
+  )),

681-683: Reuse precomputed layerDirs to avoid repeated helper calls

Minor nit; keeps things consistent.

-    const layerRelativePaths = new Set(getLayerDirectories(nuxt).map(l => relative(l.app, path)))
+    const layerRelativePaths = new Set(layerDirs.map(l => relative(l.app, path)))

990-992: Prefer using shared helper over bespoke withTrailingSlash

Avoids subtle divergences and keeps semantics consistent across the codebase.

If you adopt the ufo import above, remove this local helper:

-function withTrailingSlash (dir: string) {
-  return dir.replace(/[^/]$/, '$&/')
-}
packages/kit/src/template.ts (2)

254-259: Remove ineffective rootGlob comparison.

paths.nuxt never includes rootGlob, so if (path !== rootGlob) is always true. Simplify to unconditionally add to nodeExclude (or include rootGlob in paths.nuxt if that was intended).

Apply this diff:

-    if (!dirs.app.startsWith(rootDirWithSlash) || dirs.root === rootDirWithSlash || dirs.app.includes('node_modules')) {
-      const rootGlob = join(relativeWithDot(nuxt.options.buildDir, dirs.root), '**/*')
+    if (!dirs.app.startsWith(rootDirWithSlash) || dirs.root === rootDirWithSlash || dirs.app.includes('node_modules')) {
       const paths = resolveLayerPaths(dirs, nuxt.options.buildDir)
       for (const path of paths.nuxt) {
         include.add(path)
         legacyInclude.add(path)
-        if (path !== rootGlob) {
-          nodeExclude.add(path)
-        }
+        nodeExclude.add(path)
       }

662-664: Avoid duplicating withTrailingSlash implementations.

This local helper diverges easily from other places (e.g. layers.ts). Consider centralising it (e.g. utils/path.ts) and importing it here and in layers.ts. If exporting from layers.ts, import it here to keep semantics aligned across files.

packages/kit/test/generate-types.spec.ts (1)

127-129: Nice: exercising resolveLayerPaths via real Nuxt options.

Optional: Add a second case with an external layer (app dir outside rootDir) to cover the conditional inclusion logic in _generateTypes.

📜 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 4a92365 and 7d13650.

📒 Files selected for processing (4)
  • packages/kit/src/layers.ts (1 hunks)
  • packages/kit/src/template.ts (5 hunks)
  • packages/kit/test/generate-types.spec.ts (4 hunks)
  • packages/nuxt/src/core/nuxt.ts (8 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/kit/src/layers.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

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

Follow standard TypeScript conventions and best practices

Files:

  • packages/nuxt/src/core/nuxt.ts
  • packages/kit/test/generate-types.spec.ts
  • packages/kit/src/template.ts
**/*.{test,spec}.{ts,tsx,js,jsx}

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

Write unit tests for core functionality using vitest

Files:

  • packages/kit/test/generate-types.spec.ts
🧠 Learnings (2)
📚 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/core/nuxt.ts
  • packages/kit/test/generate-types.spec.ts
  • packages/kit/src/template.ts
📚 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:

  • packages/nuxt/src/core/nuxt.ts
🧬 Code graph analysis (3)
packages/nuxt/src/core/nuxt.ts (2)
packages/kit/src/layers.ts (1)
  • getLayerDirectories (43-73)
packages/kit/src/resolve.ts (1)
  • resolveFiles (277-285)
packages/kit/test/generate-types.spec.ts (2)
packages/kit/src/layers.ts (1)
  • getLayerDirectories (43-73)
packages/kit/src/template.ts (1)
  • resolveLayerPaths (170-210)
packages/kit/src/template.ts (1)
packages/kit/src/layers.ts (2)
  • LayerDirectories (7-28)
  • getLayerDirectories (43-73)
⏰ 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 (9)
packages/nuxt/src/core/nuxt.ts (3)

9-9: Good switch to centralised layer dirs via getLayerDirectories

This reduces private API usage and keeps path normalisation in one place.


160-162: Initialising layerDirs once is sensible

Memoised in kit; caching here improves readability and avoids scattered calls.


262-263: Including per-layer index.d.ts is correct

Pulling from dirs.root aligns with the new directories contract.

packages/kit/src/template.ts (3)

18-20: Good migration to layer-based API.

Importing getLayerDirectories and the LayerDirectories type here clarifies intent and decouples from private _layers.


170-209: resolveLayerPaths: API and glob shape look sound.

The new signature is clearer and the returned groups cover nuxt/nitro/node/shared/global declaration needs. No functional concerns.


231-234: Use of getLayerDirectories for sourceDirs is appropriate.

This removes direct dependence on _layers and keeps logic in one place. Nice.

packages/kit/test/generate-types.spec.ts (3)

27-27: Mock layer shape aligns with getLayerDirectories expectations.

Including config.rootDir and config.srcDir on _layers is sufficient for tests using the layer-based API.


60-69: Updated exclude snapshot matches new glob strategy.

The additional nitro/node/layers globs reflect the refactor correctly.


8-8: getLayerDirectories re-export verified.
packages/kit/src/index.ts exports getLayerDirectories from ./layers (line 14), so importing it from 'nuxt/kit' is valid.

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

♻️ Duplicate comments (2)
packages/nuxt/src/core/nuxt.ts (2)

422-431: Normalise root comparison and avoid O(n²) locals scan for modulesDir augmentation

This reintroduces two issues previously discussed: trailing-slash sensitivity for the root comparison and computing locallyScannedLayersDirs for every layer. Use a single project layers/ path and a normalised root comparison to prevent accidentally adding the project’s own node_modules and to simplify the logic.

-  const locallyScannedLayersDirs = layerDirs.map(l => join(l.root, 'layers/'))
-  const rootWithTrailingSlash = withTrailingSlash(nuxt.options.rootDir)
-  for (const dirs of layerDirs) {
-    if (dirs.root === rootWithTrailingSlash) {
-      continue
-    }
-    if (locallyScannedLayersDirs.every(dir => !dirs.root.startsWith(dir))) {
-      nuxt.options.modulesDir.push(join(dirs.root, 'node_modules'))
-    }
-  }
+  const locallyScannedLayersDir = join(nuxt.options.rootDir, 'layers/')
+  const rootWithTrailingSlash = withTrailingSlash(nuxt.options.rootDir)
+  for (const dirs of layerDirs) {
+    // skip project root
+    if (dirs.root === rootWithTrailingSlash) { continue }
+    // only add for layers not under ./layers/*
+    if (!dirs.root.startsWith(withTrailingSlash(locallyScannedLayersDir))) {
+      nuxt.options.modulesDir.push(join(dirs.root, 'node_modules'))
+    }
+  }

Optional: deduplicate modulesDir afterwards if necessary.


923-927: Root detection should be normalised; consider using getLayerDirectories here

The strict equality on rootDir can pick the wrong base dir and mis-resolve the layer modules path. At minimum, normalise both sides; ideally, use getLayerDirectories(nuxt) to read dirs.modules directly.

Minimal fix:

-    const modulesDir = resolve(config.srcDir, (config.rootDir === nuxt.options.rootDir ? nuxt.options.dir : config.dir)?.modules || 'modules')
+    const isRootLayer = normalize(withTrailingSlash(config.rootDir)) === normalize(withTrailingSlash(nuxt.options.rootDir))
+    const baseDir = (isRootLayer ? nuxt.options.dir : config.dir)?.modules || 'modules'
+    const modulesDir = resolve(config.srcDir, baseDir)

Optional refactor (preferred): map getLayerDirectories(nuxt) by dirs.root and use dirs.modules for the matching layer to avoid duplicating resolution rules.

🧹 Nitpick comments (3)
packages/nuxt/src/core/nuxt.ts (3)

19-19: Prefer reusing ufo's withTrailingSlash over a local helper

You already depend on ufo. To avoid subtle divergences, import withTrailingSlash instead of maintaining a local variant.

-import { withoutLeadingSlash } from 'ufo'
+import { withoutLeadingSlash, withTrailingSlash as ufoWithTrailingSlash } from 'ufo'

Apply corresponding replacements where withTrailingSlash is used, and remove the local helper at the end of the file. See also lines 991-993.


418-419: Tighten node_modules detection and simplify trailing-slash drop

Avoid false positives from paths that merely contain the substring and prefer a clearer trailing-slash trim.

-  ...layerDirs.filter(i => i.root.includes('node_modules')).map(i => i.root.replace(/\/$/, '')),
+  ...layerDirs
+    .filter(i => i.root.includes('/node_modules/'))
+    .map(i => i.root.slice(0, -1)),

991-993: Drop local withTrailingSlash helper

Use ufo’s withTrailingSlash to keep semantics aligned and avoid edge-case drift. See import adjustment on Line 19.

-function withTrailingSlash (dir: string) {
-  return dir.replace(/[^/]$/, '$&/')
-}
+// Use: ufoWithTrailingSlash imported from 'ufo'
📜 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 4affb25 and 3c5a6fa.

📒 Files selected for processing (1)
  • packages/nuxt/src/core/nuxt.ts (8 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/core/nuxt.ts
🧠 Learnings (2)
📚 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/core/nuxt.ts
📚 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:

  • packages/nuxt/src/core/nuxt.ts
🧬 Code graph analysis (1)
packages/nuxt/src/core/nuxt.ts (2)
packages/kit/src/layers.ts (1)
  • getLayerDirectories (43-73)
packages/kit/src/resolve.ts (1)
  • resolveFiles (277-285)
🔇 Additional comments (4)
packages/nuxt/src/core/nuxt.ts (4)

9-9: Good move: centralise on getLayerDirectories

Importing and using getLayerDirectories across core is the right direction and will reduce repeated normalisation logic.


160-162: LGTM: cacheable layer directory resolution

Capturing layerDirs early via getLayerDirectories(nuxt) is correct and leverages kit-side caching.


262-269: LGTM: include per-layer root-level index.d.ts

Switching to dirs.root from layer cwd is consistent with the new API and avoids cwd ambiguity.


682-697: LGTM: watch patterns relative to each layer srcDir

Computing layerRelativePaths against every layer’s app dir makes nuxt.options.watch more robust for multi-layer setups.

@danielroe danielroe merged commit 7234ae6 into main Sep 1, 2025
47 of 48 checks passed
@danielroe danielroe deleted the feat/layer-dirs branch September 1, 2025 21:38
@github-actions github-actions bot mentioned this pull request Sep 1, 2025
@github-actions github-actions bot mentioned this pull request Sep 2, 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