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 (_template: NuxtTemplate | 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 (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 { getContents?: (data: { nuxt: Nuxt, app: NuxtApp, options: Options }) => string | Promise /** Write to filesystem */ write?: boolean + /** + * The source path of the template (to try resolving dependencies from). + * @internal + */ + _path?: string } export interface NuxtServerTemplate {