From 0b6c698e550301e92ee821a3cbec90eacda25526 Mon Sep 17 00:00:00 2001
From: Daniel Roe <daniel@roe.dev>
Date: Sat, 1 Mar 2025 12:37:06 +0000
Subject: [PATCH] feat(kit,nuxt): resolve template imports from originating
 module (#31175)

---
 packages/kit/src/template.ts                  | 19 ++++++++++++++++--
 packages/kit/src/utils.ts                     |  2 ++
 .../src/core/plugins/resolve-deep-imports.ts  | 20 ++++++++++++++++---
 packages/schema/src/config/experimental.ts    |  5 +++++
 packages/schema/src/types/nuxt.ts             |  5 +++++
 5 files changed, 46 insertions(+), 5 deletions(-)

diff --git a/packages/kit/src/template.ts b/packages/kit/src/template.ts
index d3249a7f2e..3a6946d421 100644
--- a/packages/kit/src/template.ts
+++ b/packages/kit/src/template.ts
@@ -1,5 +1,6 @@
 import { existsSync, promises as fsp } from 'node:fs'
-import { basename, isAbsolute, join, parse, relative, resolve } from 'pathe'
+import { fileURLToPath } from 'node:url'
+import { basename, isAbsolute, join, normalize, parse, relative, resolve } from 'pathe'
 import { hash } from 'ohash'
 import type { Nuxt, NuxtServerTemplate, NuxtTemplate, NuxtTypeTemplate, ResolvedNuxtTemplate, TSReference } from '@nuxt/schema'
 import { withTrailingSlash } from 'ufo'
@@ -8,8 +9,9 @@ import type { TSConfig } from 'pkg-types'
 import { gte } from 'semver'
 import { readPackageJSON } from 'pkg-types'
 import { resolveModulePath } from 'exsolve'
+import { captureStackTrace } from 'errx'
 
-import { filterInPlace } from './utils'
+import { distDirURL, filterInPlace } from './utils'
 import { directoryToURL } from './internal/esm'
 import { getDirectory } from './module/install'
 import { tryUseNuxt, useNuxt } from './context'
@@ -27,6 +29,19 @@ export function addTemplate<T> (_template: NuxtTemplate<T> | string) {
   // Remove any existing template with the same destination path
   filterInPlace(nuxt.options.build.templates, p => normalizeTemplate(p).dst !== template.dst)
 
+  try {
+    const distDir = distDirURL.toString()
+    const { source } = captureStackTrace().find(e => e.source && !e.source.startsWith(distDir)) ?? {}
+    if (source) {
+      const path = normalize(fileURLToPath(source))
+      if (existsSync(path)) {
+        template._path = path
+      }
+    }
+  } catch {
+    // ignore errors as this is an additive feature
+  }
+
   // Add to templates array
   nuxt.options.build.templates.push(template)
 
diff --git a/packages/kit/src/utils.ts b/packages/kit/src/utils.ts
index 0816bcf5cd..e675b46e23 100644
--- a/packages/kit/src/utils.ts
+++ b/packages/kit/src/utils.ts
@@ -19,3 +19,5 @@ export function filterInPlace<T> (array: T[], predicate: (item: T, index: number
 }
 
 export const MODE_RE = /\.(server|client)(\.\w+)*$/
+
+export const distDirURL = new URL('.', import.meta.url)
diff --git a/packages/nuxt/src/core/plugins/resolve-deep-imports.ts b/packages/nuxt/src/core/plugins/resolve-deep-imports.ts
index 01734c9c77..949573559d 100644
--- a/packages/nuxt/src/core/plugins/resolve-deep-imports.ts
+++ b/packages/nuxt/src/core/plugins/resolve-deep-imports.ts
@@ -1,6 +1,6 @@
 import { parseNodeModulePath } from 'mlly'
 import { resolveModulePath } from 'exsolve'
-import { isAbsolute, normalize } from 'pathe'
+import { isAbsolute, normalize, resolve } from 'pathe'
 import type { Plugin } from 'vite'
 import { directoryToURL, resolveAlias } from '@nuxt/kit'
 import type { Nuxt } from '@nuxt/schema'
@@ -8,6 +8,8 @@ import type { Nuxt } from '@nuxt/schema'
 import { pkgDir } from '../../dirs'
 import { logger } from '../../utils'
 
+const VIRTUAL_RE = /^\0?virtual:(?:nuxt:)?/
+
 export function resolveDeepImportsPlugin (nuxt: Nuxt): Plugin {
   const exclude: string[] = ['virtual:', '\0virtual:', '/__skip_vite', '@vitest/']
   let conditions: string[]
@@ -29,12 +31,24 @@ export function resolveDeepImportsPlugin (nuxt: Nuxt): Plugin {
       conditions = [...resolvedConditions]
     },
     async resolveId (id, importer) {
-      if (!importer || isAbsolute(id) || (!isAbsolute(importer) && !importer.startsWith('virtual:') && !importer.startsWith('\0virtual:')) || exclude.some(e => id.startsWith(e))) {
+      if (!importer || isAbsolute(id) || (!isAbsolute(importer) && !VIRTUAL_RE.test(importer)) || exclude.some(e => id.startsWith(e))) {
         return
       }
 
       const normalisedId = resolveAlias(normalize(id), nuxt.options.alias)
-      const normalisedImporter = importer.replace(/^\0?virtual:(?:nuxt:)?/, '')
+      const isNuxtTemplate = importer.startsWith('virtual:nuxt')
+      const normalisedImporter = (isNuxtTemplate ? decodeURIComponent(importer) : importer).replace(VIRTUAL_RE, '')
+
+      if (nuxt.options.experimental.templateImportResolution !== false && isNuxtTemplate) {
+        const template = nuxt.options.build.templates.find(t => resolve(nuxt.options.buildDir, t.filename!) === normalisedImporter)
+        if (template?._path) {
+          const res = await this.resolve?.(normalisedId, template._path, { skipSelf: true })
+          if (res !== undefined && res !== null) {
+            return res
+          }
+        }
+      }
+
       const dir = parseNodeModulePath(normalisedImporter).dir || pkgDir
 
       const res = await this.resolve?.(normalisedId, dir, { skipSelf: true })
diff --git a/packages/schema/src/config/experimental.ts b/packages/schema/src/config/experimental.ts
index 02e9fb8e81..38aa343f29 100644
--- a/packages/schema/src/config/experimental.ts
+++ b/packages/schema/src/config/experimental.ts
@@ -646,5 +646,10 @@ export default defineResolvers({
         return typeof val === 'boolean' ? val : true
       },
     },
+
+    /**
+     * Disable resolving imports into Nuxt templates from the path of the module that added the template.
+     */
+    templateImportResolution: true,
   },
 })
diff --git a/packages/schema/src/types/nuxt.ts b/packages/schema/src/types/nuxt.ts
index ed95dd6851..18368b13ca 100644
--- a/packages/schema/src/types/nuxt.ts
+++ b/packages/schema/src/types/nuxt.ts
@@ -43,6 +43,11 @@ export interface NuxtTemplate<Options = TemplateDefaultOptions> {
   getContents?: (data: { nuxt: Nuxt, app: NuxtApp, options: Options }) => string | Promise<string>
   /** Write to filesystem */
   write?: boolean
+  /**
+   * The source path of the template (to try resolving dependencies from).
+   * @internal
+   */
+  _path?: string
 }
 
 export interface NuxtServerTemplate {