From 798f050bc7aa15625a428e65ec126d58033a317f Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Wed, 26 Feb 2025 17:02:04 +0000 Subject: [PATCH] perf(nuxt): migrate to use `exsolve` for module resolution (#31124) --- packages/kit/package.json | 1 + packages/kit/src/internal/esm.ts | 21 +-- packages/kit/src/loader/config.ts | 11 +- packages/kit/src/module/install.ts | 121 ++++++++---------- packages/kit/src/plugin.ts | 8 +- packages/kit/src/resolve.ts | 10 +- packages/kit/src/template.ts | 5 +- packages/nuxt/package.json | 1 + packages/nuxt/src/components/module.ts | 5 +- packages/nuxt/src/core/app.ts | 19 ++- packages/nuxt/src/core/builder.ts | 62 +++++---- packages/nuxt/src/core/nuxt.ts | 21 ++- .../src/core/plugins/resolve-deep-imports.ts | 22 +++- packages/nuxt/src/core/schema.ts | 12 +- packages/nuxt/src/core/utils/types.ts | 7 +- packages/nuxt/src/head/module.ts | 7 +- pnpm-lock.yaml | 23 ++-- .../subpath/index.ts | 0 .../subpath/module.ts | 0 test/fixtures/basic/nuxt.config.ts | 2 +- 20 files changed, 188 insertions(+), 170 deletions(-) rename test/fixtures/basic/{modules => custom-modules}/subpath/index.ts (100%) rename test/fixtures/basic/{modules => custom-modules}/subpath/module.ts (100%) diff --git a/packages/kit/package.json b/packages/kit/package.json index 064d046e9c..e25f5a92ad 100644 --- a/packages/kit/package.json +++ b/packages/kit/package.json @@ -32,6 +32,7 @@ "defu": "^6.1.4", "destr": "^2.0.3", "errx": "^0.1.0", + "exsolve": "^0.4.4", "globby": "^14.1.0", "ignore": "^7.0.3", "jiti": "^2.4.2", diff --git a/packages/kit/src/internal/esm.ts b/packages/kit/src/internal/esm.ts index f69d9a276e..ccc0ffdbd9 100644 --- a/packages/kit/src/internal/esm.ts +++ b/packages/kit/src/internal/esm.ts @@ -1,5 +1,6 @@ import { fileURLToPath, pathToFileURL } from 'node:url' -import { interopDefault, resolvePath, resolvePathSync } from 'mlly' +import { interopDefault } from 'mlly' +import { resolveModulePath } from 'exsolve' import { createJiti } from 'jiti' import { captureStackTrace } from 'errx' @@ -21,17 +22,19 @@ export function directoryToURL (dir: string): URL { */ export async function tryResolveModule (id: string, url: URL | URL[]): Promise /** @deprecated pass URLs pointing at files */ -export async function tryResolveModule (id: string, url: string | string[]): Promise -export async function tryResolveModule (id: string, url: string | string[] | URL | URL[] = import.meta.url) { - try { - return await resolvePath(id, { url }) - } catch { - // intentionally empty as this is a `try-` function - } +export function tryResolveModule (id: string, url: string | string[]): Promise +export function tryResolveModule (id: string, url: string | string[] | URL | URL[] = import.meta.url) { + return Promise.resolve(resolveModulePath(id, { + from: url, + suffixes: ['', 'index'], + try: true, + })) } export function resolveModule (id: string, options?: ResolveModuleOptions) { - return resolvePathSync(id, { url: options?.url ?? options?.paths ?? [import.meta.url] }) + return resolveModulePath(id, { + from: options?.url ?? options?.paths ?? [import.meta.url], + }) } export interface ImportModuleOptions extends ResolveModuleOptions { diff --git a/packages/kit/src/loader/config.ts b/packages/kit/src/loader/config.ts index 8d3a6888f8..eb1450e114 100644 --- a/packages/kit/src/loader/config.ts +++ b/packages/kit/src/loader/config.ts @@ -8,8 +8,9 @@ import type { NuxtConfig, NuxtOptions } from '@nuxt/schema' import { globby } from 'globby' import defu from 'defu' import { basename, join, relative } from 'pathe' -import { isWindows } from 'std-env' -import { directoryToURL, tryResolveModule } from '../internal/esm' +import { resolveModuleURL } from 'exsolve' + +import { directoryToURL } from '../internal/esm' export interface LoadNuxtConfigOptions extends Omit, 'overrides'> { // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type @@ -110,10 +111,10 @@ export async function loadNuxtConfig (opts: LoadNuxtConfigOptions): Promise r.NuxtConfigSchema) + const schemaPath = resolveModuleURL('@nuxt/schema', { try: true, from: urls }) ?? '@nuxt/schema' + return await import(schemaPath).then(r => r.NuxtConfigSchema) } diff --git a/packages/kit/src/module/install.ts b/packages/kit/src/module/install.ts index b0e7305921..f18c588211 100644 --- a/packages/kit/src/module/install.ts +++ b/packages/kit/src/module/install.ts @@ -1,15 +1,15 @@ import { existsSync, promises as fsp, lstatSync } from 'node:fs' -import { fileURLToPath, pathToFileURL } from 'node:url' +import { fileURLToPath } from 'node:url' import type { ModuleMeta, Nuxt, NuxtConfig, NuxtModule } from '@nuxt/schema' -import { dirname, isAbsolute, join, resolve } from 'pathe' +import { dirname, isAbsolute, resolve } from 'pathe' import { defu } from 'defu' import { createJiti } from 'jiti' -import { parseNodeModulePath, resolve as resolveModule } from 'mlly' +import { parseNodeModulePath } from 'mlly' +import { resolveModuleURL } from 'exsolve' import { isRelative } from 'ufo' import { directoryToURL } from '../internal/esm' import { useNuxt } from '../context' -import { resolveAlias, resolvePath } from '../resolve' -import { logger } from '../logger' +import { resolveAlias } from '../resolve' const NODE_MODULES_RE = /[/\\]node_modules[/\\]/ @@ -81,84 +81,63 @@ export const normalizeModuleTranspilePath = (p: string) => { const MissingModuleMatcher = /Cannot find module\s+['"]?([^'")\s]+)['"]?/i -export async function loadNuxtModuleInstance (nuxtModule: string | NuxtModule, nuxt: Nuxt = useNuxt()) { +export async function loadNuxtModuleInstance (nuxtModule: string | NuxtModule, nuxt: Nuxt = useNuxt()): Promise<{ nuxtModule: NuxtModule, buildTimeModuleMeta: ModuleMeta, resolvedModulePath?: string }> { let buildTimeModuleMeta: ModuleMeta = {} - let resolvedModulePath: string | undefined + + if (typeof nuxtModule === 'function') { + return { + nuxtModule, + buildTimeModuleMeta, + } + } + + if (typeof nuxtModule !== 'string') { + throw new TypeError(`Nuxt module should be a function or a string to import. Received: ${nuxtModule}.`) + } const jiti = createJiti(nuxt.options.rootDir, { alias: nuxt.options.alias }) - let resolvedNuxtModule: NuxtModule | undefined - let hadNonFunctionPath = false // Import if input is string - if (typeof nuxtModule === 'string') { - const paths = new Set() - nuxtModule = resolveAlias(nuxtModule, nuxt.options.alias) + nuxtModule = resolveAlias(nuxtModule, nuxt.options.alias) - if (isRelative(nuxtModule)) { - nuxtModule = resolve(nuxt.options.rootDir, nuxtModule) - } - - paths.add(nuxtModule) - paths.add(join(nuxtModule, 'module')) - paths.add(join(nuxtModule, 'nuxt')) - - for (const path of paths) { - try { - const src = isAbsolute(path) - ? pathToFileURL(await resolvePath(path, { fallbackToOriginal: false, extensions: nuxt.options.extensions })).href - : await resolveModule(path, { - url: nuxt.options.modulesDir.map(m => directoryToURL(m.replace(/\/node_modules\/?$/, '/'))), - extensions: nuxt.options.extensions, - }) - resolvedModulePath = fileURLToPath(new URL(src)) - if (!existsSync(resolvedModulePath)) { - continue - } - const instance = await jiti.import(src, { default: true }) as NuxtModule - // ignore possible barrel exports - if (typeof instance !== 'function') { - hadNonFunctionPath = true - continue - } - resolvedNuxtModule = instance - - // nuxt-module-builder generates a module.json with metadata including the version - const moduleMetadataPath = new URL('module.json', src) - if (existsSync(moduleMetadataPath)) { - buildTimeModuleMeta = JSON.parse(await fsp.readFile(moduleMetadataPath, 'utf-8')) - } - break - } catch (error: unknown) { - const code = (error as Error & { code?: string }).code - if (code === 'ERR_PACKAGE_PATH_NOT_EXPORTED' || code === 'ERR_UNSUPPORTED_DIR_IMPORT' || code === 'ENOTDIR') { - continue - } - if (code === 'MODULE_NOT_FOUND' || code === 'ERR_MODULE_NOT_FOUND') { - const module = MissingModuleMatcher.exec((error as Error).message)?.[1] - // verify that it's missing the nuxt module otherwise it may be a sub dependency of the module itself - // i.e module is importing a module that is missing - if (!module || module.includes(nuxtModule as string)) { - continue - } - } - logger.error(`Error while importing module \`${nuxtModule}\`: ${error}`) - throw error - } - } - } else { - resolvedNuxtModule = nuxtModule + if (isRelative(nuxtModule)) { + nuxtModule = resolve(nuxt.options.rootDir, nuxtModule) } - if (!resolvedNuxtModule) { - // Module was resolvable but returned a non-function - if (hadNonFunctionPath) { + try { + const src = resolveModuleURL(nuxtModule, { + from: nuxt.options.modulesDir.map(m => directoryToURL(m.replace(/\/node_modules\/?$/, '/'))), + suffixes: ['nuxt', 'nuxt/index', 'module', 'module/index', '', 'index'], + extensions: ['.js', '.mjs', '.cjs', '.ts', '.mts', '.cts'], + }) + const resolvedModulePath = fileURLToPath(src) + const resolvedNuxtModule = await jiti.import>(src, { default: true }) + + if (typeof resolvedNuxtModule !== 'function') { throw new TypeError(`Nuxt module should be a function: ${nuxtModule}.`) } - // Throw error if module could not be found - if (typeof nuxtModule === 'string') { + + // nuxt-module-builder generates a module.json with metadata including the version + const moduleMetadataPath = new URL('module.json', src) + if (existsSync(moduleMetadataPath)) { + buildTimeModuleMeta = JSON.parse(await fsp.readFile(moduleMetadataPath, 'utf-8')) + } + + return { nuxtModule: resolvedNuxtModule, buildTimeModuleMeta, resolvedModulePath } + } catch (error: unknown) { + const code = (error as Error & { code?: string }).code + if (code === 'ERR_PACKAGE_PATH_NOT_EXPORTED' || code === 'ERR_UNSUPPORTED_DIR_IMPORT' || code === 'ENOTDIR') { throw new TypeError(`Could not load \`${nuxtModule}\`. Is it installed?`) } + if (code === 'MODULE_NOT_FOUND' || code === 'ERR_MODULE_NOT_FOUND') { + const module = MissingModuleMatcher.exec((error as Error).message)?.[1] + // verify that it's missing the nuxt module otherwise it may be a sub dependency of the module itself + // i.e module is importing a module that is missing + if (module && !module.includes(nuxtModule as string)) { + throw new TypeError(`Error while importing module \`${nuxtModule}\`: ${error}`) + } + } } - return { nuxtModule: resolvedNuxtModule, buildTimeModuleMeta, resolvedModulePath } as { nuxtModule: NuxtModule, buildTimeModuleMeta: ModuleMeta, resolvedModulePath?: string } + throw new TypeError(`Could not load \`${nuxtModule}\`. Is it installed?`) } diff --git a/packages/kit/src/plugin.ts b/packages/kit/src/plugin.ts index 26ae9b70dc..bdaa5ffb59 100644 --- a/packages/kit/src/plugin.ts +++ b/packages/kit/src/plugin.ts @@ -1,10 +1,8 @@ import { existsSync } from 'node:fs' import { isAbsolute } from 'node:path' -import { pathToFileURL } from 'node:url' import { normalize } from 'pathe' import type { NuxtPlugin, NuxtPluginTemplate } from '@nuxt/schema' -import { resolvePathSync } from 'mlly' -import { isWindows } from 'std-env' +import { resolveModulePath } from 'exsolve' import { MODE_RE, filterInPlace } from './utils' import { tryUseNuxt, useNuxt } from './context' import { addTemplate } from './template' @@ -35,7 +33,9 @@ export function normalizePlugin (plugin: NuxtPlugin | string): NuxtPlugin { if (!existsSync(plugin.src) && isAbsolute(plugin.src)) { try { - plugin.src = resolvePathSync(isWindows ? pathToFileURL(plugin.src).href : plugin.src, { extensions: tryUseNuxt()?.options.extensions }) + plugin.src = resolveModulePath(plugin.src, { + extensions: tryUseNuxt()?.options.extensions ?? ['.js', '.mjs', '.cjs', '.ts', '.tsx', '.mts', '.cts'], + }) } catch { // ignore errors as the file may be in the nuxt vfs } diff --git a/packages/kit/src/resolve.ts b/packages/kit/src/resolve.ts index 79b83a2146..d65a4e4d30 100644 --- a/packages/kit/src/resolve.ts +++ b/packages/kit/src/resolve.ts @@ -2,7 +2,7 @@ import { promises as fsp } from 'node:fs' import { fileURLToPath } from 'node:url' import { basename, dirname, isAbsolute, join, normalize, resolve } from 'pathe' import { globby } from 'globby' -import { resolvePath as _resolvePath } from 'mlly' +import { resolveModulePath } from 'exsolve' import { resolveAlias as _resolveAlias } from 'pathe/utils' import { directoryToURL } from './internal/esm' import { tryUseNuxt } from './context' @@ -172,7 +172,7 @@ async function _resolvePathGranularly (path: string, opts: ResolvePathOptions = const modulesDir = nuxt ? nuxt.options.modulesDir : [] // Resolve aliases - path = resolveAlias(path) + path = _resolveAlias(path, opts.alias ?? nuxt?.options.alias ?? {}) // Resolve relative to cwd if (!isAbsolute(path)) { @@ -200,7 +200,11 @@ async function _resolvePathGranularly (path: string, opts: ResolvePathOptions = } // Try to resolve as module id - const resolvedModulePath = await _resolvePath(_path, { url: [cwd, ...modulesDir].map(d => directoryToURL(d)) }).catch(() => null) + const resolvedModulePath = resolveModulePath(_path, { + try: true, + suffixes: ['', 'index'], + from: [cwd, ...modulesDir].map(d => directoryToURL(d)), + }) if (resolvedModulePath) { return { path: resolvedModulePath, diff --git a/packages/kit/src/template.ts b/packages/kit/src/template.ts index 0f88dc9576..d0f85b731a 100644 --- a/packages/kit/src/template.ts +++ b/packages/kit/src/template.ts @@ -7,9 +7,10 @@ import { defu } from 'defu' import type { TSConfig } from 'pkg-types' import { gte } from 'semver' import { readPackageJSON } from 'pkg-types' +import { resolveModulePath } from 'exsolve' import { filterInPlace } from './utils' -import { directoryToURL, tryResolveModule } from './internal/esm' +import { directoryToURL } from './internal/esm' import { getDirectory } from './module/install' import { tryUseNuxt, useNuxt } from './context' import { resolveNuxtModule } from './resolve' @@ -250,7 +251,7 @@ export async function _generateTypes (nuxt: Nuxt) { let absolutePath = resolve(basePath, aliases[alias]!) let stats = await fsp.stat(absolutePath).catch(() => null /* file does not exist */) if (!stats) { - const resolvedModule = await tryResolveModule(aliases[alias]!, importPaths) + const resolvedModule = resolveModulePath(aliases[alias]!, { try: true, from: importPaths }) if (resolvedModule) { absolutePath = resolvedModule stats = await fsp.stat(resolvedModule).catch(() => null) diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index 88d3d93a10..8bf92f9b19 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -89,6 +89,7 @@ "esbuild": "^0.25.0", "escape-string-regexp": "^5.0.0", "estree-walker": "^3.0.3", + "exsolve": "^0.4.4", "globby": "^14.1.0", "h3": "npm:h3-nightly@1.15.1-20250222-111608-d1c00fc", "hookable": "^5.5.3", diff --git a/packages/nuxt/src/components/module.ts b/packages/nuxt/src/components/module.ts index ebaad05622..7d68f0393c 100644 --- a/packages/nuxt/src/components/module.ts +++ b/packages/nuxt/src/components/module.ts @@ -1,8 +1,9 @@ import { existsSync, statSync, writeFileSync } from 'node:fs' import { isAbsolute, join, normalize, relative, resolve } from 'pathe' -import { addBuildPlugin, addPluginTemplate, addTemplate, addTypeTemplate, addVitePlugin, defineNuxtModule, findPath, resolveAlias, resolvePath } from '@nuxt/kit' +import { addBuildPlugin, addPluginTemplate, addTemplate, addTypeTemplate, addVitePlugin, defineNuxtModule, findPath, resolveAlias } from '@nuxt/kit' import type { Component, ComponentsDir, ComponentsOptions } from 'nuxt/schema' +import { resolveModulePath } from 'exsolve' import { distDir } from '../dirs' import { logger } from '../utils' import { componentNamesTemplate, componentsIslandsTemplate, componentsMetadataTemplate, componentsPluginTemplate, componentsTypeTemplate } from './templates' @@ -175,7 +176,7 @@ export default defineNuxtModule({ for (const component of newComponents) { if (!(component as any /* untyped internal property */)._scanned && !(component.filePath in nuxt.vfs) && isAbsolute(component.filePath) && !existsSync(component.filePath)) { // attempt to resolve component path - component.filePath = await resolvePath(component.filePath, { fallbackToOriginal: true }) + component.filePath = resolveModulePath(resolveAlias(component.filePath), { try: true, extensions: nuxt.options.extensions }) ?? component.filePath } if (component.mode === 'client' && !newComponents.some(c => c.pascalName === component.pascalName && c.mode === 'server')) { newComponents.push({ diff --git a/packages/nuxt/src/core/app.ts b/packages/nuxt/src/core/app.ts index 76cabddf90..7a65ed2dbb 100644 --- a/packages/nuxt/src/core/app.ts +++ b/packages/nuxt/src/core/app.ts @@ -1,7 +1,7 @@ import { promises as fsp, mkdirSync, writeFileSync } from 'node:fs' import { dirname, join, relative, resolve } from 'pathe' import { defu } from 'defu' -import { findPath, normalizePlugin, normalizeTemplate, resolveAlias, resolveFiles, resolvePath } from '@nuxt/kit' +import { findPath, normalizePlugin, normalizeTemplate, resolveFiles, resolvePath } from '@nuxt/kit' import type { Nuxt, NuxtApp, NuxtPlugin, NuxtTemplate, ResolvedNuxtTemplate } from 'nuxt/schema' import type { PluginMeta } from 'nuxt/app' @@ -215,8 +215,8 @@ export async function resolveApp (nuxt: Nuxt, app: NuxtApp) { } // Normalize and de-duplicate plugins and middleware - app.middleware = uniqueBy(await resolvePaths([...app.middleware].reverse(), 'path'), 'name').reverse() - app.plugins = uniqueBy(await resolvePaths(app.plugins, 'src'), 'src') + app.middleware = uniqueBy(await resolvePaths(nuxt, [...app.middleware].reverse(), 'path'), 'name').reverse() + app.plugins = uniqueBy(await resolvePaths(nuxt, app.plugins, 'src'), 'src') // Resolve app.config app.configs = [] @@ -231,16 +231,21 @@ export async function resolveApp (nuxt: Nuxt, app: NuxtApp) { await nuxt.callHook('app:resolve', app) // Normalize and de-duplicate plugins and middleware - app.middleware = uniqueBy(await resolvePaths(app.middleware, 'path'), 'name') - app.plugins = uniqueBy(await resolvePaths(app.plugins, 'src'), 'src') + app.middleware = uniqueBy(await resolvePaths(nuxt, app.middleware, 'path'), 'name') + app.plugins = uniqueBy(await resolvePaths(nuxt, app.plugins, 'src'), 'src') } -function resolvePaths> (items: Item[], key: { [K in keyof Item]: Item[K] extends string ? K : never }[keyof Item]) { +function resolvePaths> (nuxt: Nuxt, items: Item[], key: { [K in keyof Item]: Item[K] extends string ? K : never }[keyof Item]) { return Promise.all(items.map(async (item) => { if (!item[key]) { return item } return { ...item, - [key]: await resolvePath(resolveAlias(item[key])), + [key]: await resolvePath(item[key], { + alias: nuxt.options.alias, + extensions: nuxt.options.extensions, + fallbackToOriginal: true, + virtual: true, + }), } })) } diff --git a/packages/nuxt/src/core/builder.ts b/packages/nuxt/src/core/builder.ts index a0ce6aca04..98648c3d2d 100644 --- a/packages/nuxt/src/core/builder.ts +++ b/packages/nuxt/src/core/builder.ts @@ -1,7 +1,7 @@ import type { EventType } from '@parcel/watcher' import type { FSWatcher } from 'chokidar' import { watch as chokidarWatch } from 'chokidar' -import { createIsIgnored, directoryToURL, importModule, isIgnored, tryResolveModule, useNuxt } from '@nuxt/kit' +import { createIsIgnored, directoryToURL, importModule, isIgnored, useNuxt } from '@nuxt/kit' import { debounce } from 'perfect-debounce' import { normalize, relative, resolve } from 'pathe' import type { Nuxt, NuxtBuilder } from 'nuxt/schema' @@ -193,36 +193,35 @@ async function createParcelWatcher () { // eslint-disable-next-line no-console console.time('[nuxt] builder:parcel:watch') } - const watcherPath = await tryResolveModule('@parcel/watcher', [nuxt.options.rootDir, ...nuxt.options.modulesDir].map(d => directoryToURL(d))) - if (!watcherPath) { + try { + const { subscribe } = await importModule('@parcel/watcher', { url: [nuxt.options.rootDir, ...nuxt.options.modulesDir].map(d => directoryToURL(d)) }) + for (const layer of nuxt.options._layers) { + if (!layer.config.srcDir) { continue } + const watcher = subscribe(layer.config.srcDir, (err, events) => { + if (err) { return } + for (const event of events) { + if (isIgnored(event.path)) { continue } + nuxt.callHook('builder:watch', watchEvents[event.type], normalize(event.path)) + } + }, { + ignore: [ + ...nuxt.options.ignore, + 'node_modules', + ], + }) + watcher.then((subscription) => { + if (nuxt.options.debug && nuxt.options.debug.watchers) { + // eslint-disable-next-line no-console + console.timeEnd('[nuxt] builder:parcel:watch') + } + nuxt.hook('close', () => subscription.unsubscribe()) + }) + } + return true + } catch { logger.warn('Falling back to `chokidar-granular` as `@parcel/watcher` cannot be resolved in your project.') return false } - - const { subscribe } = await importModule(watcherPath) - for (const layer of nuxt.options._layers) { - if (!layer.config.srcDir) { continue } - const watcher = subscribe(layer.config.srcDir, (err, events) => { - if (err) { return } - for (const event of events) { - if (isIgnored(event.path)) { continue } - nuxt.callHook('builder:watch', watchEvents[event.type], normalize(event.path)) - } - }, { - ignore: [ - ...nuxt.options.ignore, - 'node_modules', - ], - }) - watcher.then((subscription) => { - if (nuxt.options.debug && nuxt.options.debug.watchers) { - // eslint-disable-next-line no-console - console.timeEnd('[nuxt] builder:parcel:watch') - } - nuxt.hook('close', () => subscription.unsubscribe()) - }) - } - return true } async function bundle (nuxt: Nuxt) { @@ -244,10 +243,9 @@ async function bundle (nuxt: Nuxt) { } async function loadBuilder (nuxt: Nuxt, builder: string): Promise { - const builderPath = await tryResolveModule(builder, [directoryToURL(nuxt.options.rootDir), new URL(import.meta.url)]) - - if (!builderPath) { + try { + return await importModule(builder, { url: [directoryToURL(nuxt.options.rootDir), new URL(import.meta.url)] }) + } catch { throw new Error(`Loading \`${builder}\` builder failed. You can read more about the nuxt \`builder\` option at: \`https://nuxt.com/docs/api/nuxt-config#builder\``) } - return importModule(builderPath) } diff --git a/packages/nuxt/src/core/nuxt.ts b/packages/nuxt/src/core/nuxt.ts index 013f151fe1..e853cf7694 100644 --- a/packages/nuxt/src/core/nuxt.ts +++ b/packages/nuxt/src/core/nuxt.ts @@ -6,7 +6,7 @@ import { join, normalize, relative, resolve } from 'pathe' import { createDebugger, createHooks } from 'hookable' import ignore from 'ignore' import type { LoadNuxtOptions } from '@nuxt/kit' -import { addBuildPlugin, addComponent, addPlugin, addPluginTemplate, addRouteMiddleware, addServerPlugin, addTypeTemplate, addVitePlugin, addWebpackPlugin, directoryToURL, installModule, loadNuxtConfig, nuxtCtx, resolveAlias, resolveFiles, resolveIgnorePatterns, resolvePath, runWithNuxtContext, tryResolveModule, useNitro } from '@nuxt/kit' +import { addBuildPlugin, addComponent, addPlugin, addPluginTemplate, addRouteMiddleware, addServerPlugin, addTypeTemplate, addVitePlugin, addWebpackPlugin, directoryToURL, installModule, loadNuxtConfig, nuxtCtx, resolveAlias, resolveFiles, resolveIgnorePatterns, runWithNuxtContext, useNitro } from '@nuxt/kit' import type { Nuxt, NuxtHooks, NuxtModule, NuxtOptions } from 'nuxt/schema' import type { PackageJson } from 'pkg-types' import { readPackageJSON } from 'pkg-types' @@ -24,6 +24,7 @@ import defu from 'defu' import { gt, satisfies } from 'semver' import { hasTTY, isCI } from 'std-env' import { genImport } from 'knitwork' +import { resolveModulePath, resolveModuleURL } from 'exsolve' import { installNuxtModule } from '../core/features' import pagesModule from '../pages/module' @@ -390,14 +391,14 @@ async function initNuxt (nuxt: Nuxt) { })) } - nuxt.hook('modules:done', async () => { + nuxt.hook('modules:done', () => { const importPaths = nuxt.options.modulesDir.map(dir => directoryToURL((dir))) // Add unctx transform addBuildPlugin(UnctxTransformPlugin({ sourcemap: !!nuxt.options.sourcemap.server || !!nuxt.options.sourcemap.client, transformerOptions: { ...nuxt.options.optimization.asyncTransforms, - helperModule: await tryResolveModule('unctx', importPaths) ?? 'unctx', + helperModule: resolveModuleURL('unctx', { try: true, from: importPaths }) ?? 'unctx', }, })) @@ -482,8 +483,14 @@ async function initNuxt (nuxt: Nuxt) { for (const _mod of nuxt.options.modules) { const mod = Array.isArray(_mod) ? _mod[0] : _mod if (typeof mod !== 'string') { continue } - const modPath = await resolvePath(resolveAlias(mod), { fallbackToOriginal: true }) - specifiedModules.add(modPath) + const modAlias = resolveAlias(mod) + const modPath = resolveModulePath(modAlias, { + try: true, + from: nuxt.options.modulesDir.map(m => directoryToURL(m.replace(/\/node_modules\/?$/, '/'))), + suffixes: ['nuxt', 'nuxt/index', 'module', 'module/index', '', 'index'], + extensions: ['.js', '.mjs', '.cjs', '.ts', '.mts', '.cts'], + }) + specifiedModules.add(modPath || modAlias) } // Automatically register user modules @@ -920,9 +927,9 @@ export async function loadNuxt (opts: LoadNuxtOptions): Promise { } export async function checkDependencyVersion (name: string, nuxtVersion: string): Promise { - const path = await resolvePath(name, { fallbackToOriginal: true }).catch(() => null) + const path = resolveModulePath(name, { try: true }) - if (!path || path === name) { return } + if (!path) { return } const { version } = await readPackageJSON(path) if (version && gt(nuxtVersion, version)) { diff --git a/packages/nuxt/src/core/plugins/resolve-deep-imports.ts b/packages/nuxt/src/core/plugins/resolve-deep-imports.ts index abc281830e..01734c9c77 100644 --- a/packages/nuxt/src/core/plugins/resolve-deep-imports.ts +++ b/packages/nuxt/src/core/plugins/resolve-deep-imports.ts @@ -1,4 +1,5 @@ -import { parseNodeModulePath, resolvePath } from 'mlly' +import { parseNodeModulePath } from 'mlly' +import { resolveModulePath } from 'exsolve' import { isAbsolute, normalize } from 'pathe' import type { Plugin } from 'vite' import { directoryToURL, resolveAlias } from '@nuxt/kit' @@ -36,13 +37,24 @@ export function resolveDeepImportsPlugin (nuxt: Nuxt): Plugin { const normalisedImporter = importer.replace(/^\0?virtual:(?:nuxt:)?/, '') const dir = parseNodeModulePath(normalisedImporter).dir || pkgDir - return await this.resolve?.(normalisedId, dir, { skipSelf: true }) ?? await resolvePath(id, { - url: [dir, ...nuxt.options.modulesDir].map(d => directoryToURL(d)), + const res = await this.resolve?.(normalisedId, dir, { skipSelf: true }) + if (res !== undefined && res !== null) { + return res + } + + const path = resolveModulePath(id, { + from: [dir, ...nuxt.options.modulesDir].map(d => directoryToURL(d)), + suffixes: ['', 'index'], conditions, - }).catch(() => { + try: true, + }) + + if (!path) { logger.debug('Could not resolve id', id, importer) return null - }) + } + + return normalize(path) }, } } diff --git a/packages/nuxt/src/core/schema.ts b/packages/nuxt/src/core/schema.ts index f8e000870d..0c45b456e2 100644 --- a/packages/nuxt/src/core/schema.ts +++ b/packages/nuxt/src/core/schema.ts @@ -5,7 +5,7 @@ import { resolve } from 'pathe' import { watch } from 'chokidar' import { defu } from 'defu' import { debounce } from 'perfect-debounce' -import { createIsIgnored, createResolver, defineNuxtModule, directoryToURL, importModule, tryResolveModule } from '@nuxt/kit' +import { createIsIgnored, createResolver, defineNuxtModule, directoryToURL, importModule } from '@nuxt/kit' import { generateTypes, resolveSchema as resolveUntypedSchema } from 'untyped' import type { Schema, SchemaDefinition } from 'untyped' import untypedPlugin from 'untyped/babel-plugin' @@ -54,9 +54,10 @@ export default defineNuxtModule({ }) if (nuxt.options.experimental.watcher === 'parcel') { - const watcherPath = await tryResolveModule('@parcel/watcher', [nuxt.options.rootDir, ...nuxt.options.modulesDir].map(dir => directoryToURL(dir))) - if (watcherPath) { - const { subscribe } = await importModule(watcherPath) + try { + const { subscribe } = await importModule('@parcel/watcher', { + url: [nuxt.options.rootDir, ...nuxt.options.modulesDir].map(dir => directoryToURL(dir)), + }) for (const layer of nuxt.options._layers) { const subscription = await subscribe(layer.config.rootDir, onChange, { ignore: ['!nuxt.schema.*'], @@ -64,8 +65,9 @@ export default defineNuxtModule({ nuxt.hook('close', () => subscription.unsubscribe()) } return + } catch { + logger.warn('Falling back to `chokidar` as `@parcel/watcher` cannot be resolved in your project.') } - logger.warn('Falling back to `chokidar` as `@parcel/watcher` cannot be resolved in your project.') } const isIgnored = createIsIgnored(nuxt) diff --git a/packages/nuxt/src/core/utils/types.ts b/packages/nuxt/src/core/utils/types.ts index 504962b989..6a86e99db1 100644 --- a/packages/nuxt/src/core/utils/types.ts +++ b/packages/nuxt/src/core/utils/types.ts @@ -1,11 +1,14 @@ import { resolvePackageJSON } from 'pkg-types' -import { resolvePath as _resolvePath } from 'mlly' +import { resolveModulePath } from 'exsolve' import { dirname } from 'pathe' import { directoryToURL, tryUseNuxt } from '@nuxt/kit' export async function resolveTypePath (path: string, subpath: string, searchPaths = tryUseNuxt()?.options.modulesDir) { try { - const r = await _resolvePath(path, { url: searchPaths?.map(d => directoryToURL(d)), conditions: ['types', 'import', 'require'] }) + const r = resolveModulePath(path, { + from: searchPaths?.map(d => directoryToURL(d)), + conditions: ['types', 'import', 'require'], + }) if (subpath) { return r.replace(/(?:\.d)?\.[mc]?[jt]s$/, '') } diff --git a/packages/nuxt/src/head/module.ts b/packages/nuxt/src/head/module.ts index 05acfc7aa1..95d047f120 100644 --- a/packages/nuxt/src/head/module.ts +++ b/packages/nuxt/src/head/module.ts @@ -1,6 +1,7 @@ import { resolve } from 'pathe' -import { addComponent, addImportsSources, addPlugin, addTemplate, defineNuxtModule, directoryToURL, tryResolveModule } from '@nuxt/kit' +import { addComponent, addImportsSources, addPlugin, addTemplate, defineNuxtModule, directoryToURL } from '@nuxt/kit' import type { NuxtOptions } from '@nuxt/schema' +import { resolveModulePath } from 'exsolve' import { distDir } from '../dirs' const components = ['NoScript', 'Link', 'Base', 'Title', 'Meta', 'Style', 'Head', 'Html', 'Body'] @@ -10,7 +11,7 @@ export default defineNuxtModule({ name: 'nuxt:meta', configKey: 'unhead', }, - async setup (options, nuxt) { + setup (options, nuxt) { const runtimeDir = resolve(distDir, 'head/runtime') // Transpile @unhead/vue @@ -53,7 +54,7 @@ export default defineNuxtModule({ // Opt-out feature allowing dependencies using @vueuse/head to work const importPaths = nuxt.options.modulesDir.map(d => directoryToURL(d)) - const unheadVue = await tryResolveModule('@unhead/vue', importPaths) || '@unhead/vue' + const unheadVue = resolveModulePath('@unhead/vue', { try: true, from: importPaths }) || '@unhead/vue' addTemplate({ filename: 'unhead-plugins.mjs', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6fa7e0b470..35b2377b85 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -247,6 +247,9 @@ importers: errx: specifier: ^0.1.0 version: 0.1.0 + exsolve: + specifier: ^0.4.4 + version: 0.4.4 globby: specifier: ^14.1.0 version: 14.1.0 @@ -398,6 +401,9 @@ importers: estree-walker: specifier: ^3.0.3 version: 3.0.3 + exsolve: + specifier: ^0.4.4 + version: 0.4.4 globby: specifier: ^14.1.0 version: 14.1.0 @@ -3808,9 +3814,6 @@ packages: uWebSockets.js: optional: true - crossws@0.3.2: - resolution: {integrity: sha512-S2PpQHRcgYABOS2465b34wqTOn5dbLL+iSvyweJYGGFLDsKq88xrjDXUiEhfYkhWZq1HuS6of3okRHILbkrqxw==} - crossws@0.3.4: resolution: {integrity: sha512-uj0O1ETYX1Bh6uSgktfPvwDiPYGQ3aI4qVsaC/LWpkIzGj1nUYm5FK3K+t11oOlpN01lGbprFCH4wBlKdJjVgw==} @@ -4388,8 +4391,8 @@ packages: resolution: {integrity: sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==} engines: {node: '>=12.0.0'} - exsolve@0.4.2: - resolution: {integrity: sha512-sLGq+3R6ISovHNxuENnZ69paPIQm8h2bfeSefXIKvBLNCgh4pc+luHtgTLC4ij2zN6KFm1aMG8efQXcjyNy3lw==} + exsolve@0.4.4: + resolution: {integrity: sha512-74RiT9i1G0eyFyE9n5f6mdX8+AicZFnhJ0CHB9VrkIl3Sy8vmW49ODbpwevdLswST7fhp3jvfPzD4DArTfjnsA==} extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -10519,7 +10522,7 @@ snapshots: confbox: 0.1.8 defu: 6.1.4 dotenv: 16.4.7 - exsolve: 0.4.2 + exsolve: 0.4.4 giget: 2.0.0 jiti: 2.4.2 ohash: 2.0.5 @@ -10810,10 +10813,6 @@ snapshots: crossws@0.2.4: {} - crossws@0.3.2: - dependencies: - uncrypto: 0.1.3 - crossws@0.3.4: dependencies: uncrypto: 0.1.3 @@ -11550,7 +11549,7 @@ snapshots: expect-type@1.1.0: {} - exsolve@0.4.2: {} + exsolve@0.4.4: {} extend@3.0.2: {} @@ -12506,7 +12505,7 @@ snapshots: citty: 0.1.6 clipboardy: 4.0.0 consola: 3.4.0 - crossws: 0.3.2 + crossws: 0.3.4 defu: 6.1.4 get-port-please: 3.1.2 h3: h3-nightly@1.15.1-20250222-111608-d1c00fc diff --git a/test/fixtures/basic/modules/subpath/index.ts b/test/fixtures/basic/custom-modules/subpath/index.ts similarity index 100% rename from test/fixtures/basic/modules/subpath/index.ts rename to test/fixtures/basic/custom-modules/subpath/index.ts diff --git a/test/fixtures/basic/modules/subpath/module.ts b/test/fixtures/basic/custom-modules/subpath/module.ts similarity index 100% rename from test/fixtures/basic/modules/subpath/module.ts rename to test/fixtures/basic/custom-modules/subpath/module.ts diff --git a/test/fixtures/basic/nuxt.config.ts b/test/fixtures/basic/nuxt.config.ts index 4944bf60c4..f30d2247b4 100644 --- a/test/fixtures/basic/nuxt.config.ts +++ b/test/fixtures/basic/nuxt.config.ts @@ -35,7 +35,7 @@ export default defineNuxtConfig({ } }) }, - '~/modules/subpath', + '~/custom-modules/subpath', './modules/test', '~/modules/example', function (_, nuxt) {