feat(kit,nuxt,vite): directoryToURL to normalise paths (#30986)

This commit is contained in:
Daniel Roe 2025-02-14 12:14:28 +01:00
parent 9ba51dba6f
commit 62cb4a6ec1
No known key found for this signature in database
GPG Key ID: CBC814C393D93268
15 changed files with 60 additions and 41 deletions

View File

@ -32,6 +32,6 @@ export { addTemplate, addServerTemplate, addTypeTemplate, normalizeTemplate, upd
export { logger, useLogger } from './logger'
// Internal Utils
export { resolveModule, tryResolveModule, importModule, tryImportModule, requireModule, tryRequireModule } from './internal/esm'
export { directoryToURL, resolveModule, tryResolveModule, importModule, tryImportModule, requireModule, tryRequireModule } from './internal/esm'
export type { ImportModuleOptions, ResolveModuleOptions } from './internal/esm'
export * from './internal/template'

View File

@ -3,7 +3,13 @@ import { interopDefault, resolvePath, resolvePathSync } from 'mlly'
import { createJiti } from 'jiti'
export interface ResolveModuleOptions {
/** @deprecated use `url` with URLs pointing at a file - never a directory */
paths?: string | string[]
url?: URL | URL[]
}
export function directoryToURL (dir: string): URL {
return pathToFileURL(dir + '/')
}
/**
@ -12,7 +18,10 @@ export interface ResolveModuleOptions {
*
* @internal
*/
export async function tryResolveModule (id: string, url: string | string[] = import.meta.url) {
export async function tryResolveModule (id: string, url: URL | URL[]): Promise<string | undefined>
/** @deprecated pass URLs pointing at files */
export async function tryResolveModule (id: string, url: string | string[]): Promise<string | undefined>
export async function tryResolveModule (id: string, url: string | string[] | URL | URL[] = import.meta.url) {
try {
return await resolvePath(id, { url })
} catch {
@ -21,7 +30,7 @@ export async function tryResolveModule (id: string, url: string | string[] = imp
}
export function resolveModule (id: string, options?: ResolveModuleOptions) {
return resolvePathSync(id, { url: options?.paths ?? [import.meta.url] })
return resolvePathSync(id, { url: options?.url ?? options?.paths ?? [import.meta.url] })
}
export interface ImportModuleOptions extends ResolveModuleOptions {
@ -30,8 +39,8 @@ export interface ImportModuleOptions extends ResolveModuleOptions {
}
export async function importModule<T = unknown> (id: string, opts?: ImportModuleOptions) {
const resolvedPath = await resolveModule(id, opts)
return import(pathToFileURL(resolvedPath).href).then(r => opts?.interopDefault !== false ? interopDefault(r) : r) as Promise<T>
const resolvedPath = resolveModule(id, opts)
return await import(pathToFileURL(resolvedPath).href).then(r => opts?.interopDefault !== false ? interopDefault(r) : r) as Promise<T>
}
export function tryImportModule<T = unknown> (id: string, opts?: ImportModuleOptions) {

View File

@ -9,7 +9,7 @@ import { globby } from 'globby'
import defu from 'defu'
import { basename, join, relative } from 'pathe'
import { isWindows } from 'std-env'
import { tryResolveModule } from '../internal/esm'
import { directoryToURL, tryResolveModule } from '../internal/esm'
export interface LoadNuxtConfigOptions extends Omit<LoadConfigOptions<NuxtConfig>, 'overrides'> {
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
@ -108,11 +108,12 @@ export async function loadNuxtConfig (opts: LoadNuxtConfigOptions): Promise<Nuxt
}
async function loadNuxtSchema (cwd: string) {
const paths = [cwd]
const nuxtPath = await tryResolveModule('nuxt', cwd) ?? await tryResolveModule('nuxt-nightly', cwd)
const url = directoryToURL(cwd)
const urls = [url]
const nuxtPath = await tryResolveModule('nuxt', url) ?? await tryResolveModule('nuxt-nightly', url)
if (nuxtPath) {
paths.unshift(nuxtPath)
urls.unshift(pathToFileURL(nuxtPath))
}
const schemaPath = await tryResolveModule('@nuxt/schema', paths) ?? '@nuxt/schema'
const schemaPath = await tryResolveModule('@nuxt/schema', urls) ?? '@nuxt/schema'
return await import(isWindows ? pathToFileURL(schemaPath).href : schemaPath).then(r => r.NuxtConfigSchema)
}

View File

@ -1,8 +1,6 @@
import { pathToFileURL } from 'node:url'
import { readPackageJSON, resolvePackageJSON } from 'pkg-types'
import type { Nuxt } from '@nuxt/schema'
import { withTrailingSlash } from 'ufo'
import { importModule, tryImportModule } from '../internal/esm'
import { directoryToURL, importModule, tryImportModule } from '../internal/esm'
import { runWithNuxtContext } from '../context'
import type { LoadNuxtConfigOptions } from './config'
@ -28,10 +26,10 @@ export async function loadNuxt (opts: LoadNuxtOptions): Promise<Nuxt> {
// Apply dev as config override
opts.overrides.dev = !!opts.dev
const rootDir = withTrailingSlash(pathToFileURL(opts.cwd!).href)
const rootURL = directoryToURL(opts.cwd!)
const nearestNuxtPkg = await Promise.all(['nuxt-nightly', 'nuxt3', 'nuxt', 'nuxt-edge']
.map(pkg => resolvePackageJSON(pkg, { url: rootDir }).catch(() => null)))
.map(pkg => resolvePackageJSON(pkg, { url: rootURL }).catch(() => null)))
.then(r => (r.filter(Boolean) as string[]).sort((a, b) => b.length - a.length)[0])
if (!nearestNuxtPkg) {
throw new Error(`Cannot find any nuxt version from ${opts.cwd}`)
@ -41,13 +39,13 @@ export async function loadNuxt (opts: LoadNuxtOptions): Promise<Nuxt> {
// Nuxt 3
if (majorVersion && majorVersion >= 3) {
const { loadNuxt } = await importModule<typeof import('nuxt')>((pkg as any)._name || pkg.name, { paths: rootDir })
const { loadNuxt } = await importModule<typeof import('nuxt')>((pkg as any)._name || pkg.name, { url: rootURL })
const nuxt = await loadNuxt(opts)
return nuxt
}
// Nuxt 2
const { loadNuxt } = await tryImportModule<{ loadNuxt: any }>('nuxt-edge', { paths: rootDir }) || await importModule<{ loadNuxt: any }>('nuxt', { paths: rootDir })
const { loadNuxt } = await tryImportModule<{ loadNuxt: any }>('nuxt-edge', { url: rootURL }) || await importModule<{ loadNuxt: any }>('nuxt', { url: rootURL })
const nuxt = await loadNuxt({
rootDir: opts.cwd,
for: opts.dev ? 'dev' : 'build',
@ -73,15 +71,15 @@ export async function loadNuxt (opts: LoadNuxtOptions): Promise<Nuxt> {
}
export async function buildNuxt (nuxt: Nuxt): Promise<any> {
const rootDir = withTrailingSlash(pathToFileURL(nuxt.options.rootDir).href)
const rootURL = directoryToURL(nuxt.options.rootDir)
// Nuxt 3
if (nuxt.options._majorVersion === 3) {
const { build } = await tryImportModule<typeof import('nuxt')>('nuxt-nightly', { paths: rootDir }) || await tryImportModule<typeof import('nuxt')>('nuxt3', { paths: rootDir }) || await importModule<typeof import('nuxt')>('nuxt', { paths: rootDir })
const { build } = await tryImportModule<typeof import('nuxt')>('nuxt-nightly', { url: rootURL }) || await tryImportModule<typeof import('nuxt')>('nuxt3', { url: rootURL }) || await importModule<typeof import('nuxt')>('nuxt', { url: rootURL })
return runWithNuxtContext(nuxt, () => build(nuxt))
}
// Nuxt 2
const { build } = await tryImportModule<{ build: any }>('nuxt-edge', { paths: rootDir }) || await importModule<{ build: any }>('nuxt', { paths: rootDir })
const { build } = await tryImportModule<{ build: any }>('nuxt-edge', { url: rootURL }) || await importModule<{ build: any }>('nuxt', { url: rootURL })
return runWithNuxtContext(nuxt, () => build(nuxt))
}

View File

@ -7,6 +7,7 @@ import { createJiti } from 'jiti'
import { parseNodeModulePath, resolve as resolveModule } from 'mlly'
import { isRelative } from 'ufo'
import { isNuxt2 } from '../compatibility'
import { directoryToURL } from '../internal/esm'
import { useNuxt } from '../context'
import { resolveAlias, resolvePath } from '../resolve'
import { logger } from '../logger'
@ -107,7 +108,10 @@ export async function loadNuxtModuleInstance (nuxtModule: string | NuxtModule, n
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 => pathToFileURL(m.replace(/\/node_modules\/?$/, ''))), extensions: nuxt.options.extensions })
: await resolveModule(path, {
url: nuxt.options.modulesDir.map(m => directoryToURL(m.replace(/\/node_modules\/?$/, '/'))),
extensions: nuxt.options.extensions,
})
nuxtModule = await jiti.import(src, { default: true }) as NuxtModule
resolvedModulePath = fileURLToPath(new URL(src))

View File

@ -4,6 +4,7 @@ import { basename, dirname, isAbsolute, join, normalize, resolve } from 'pathe'
import { globby } from 'globby'
import { resolvePath as _resolvePath } from 'mlly'
import { resolveAlias as _resolveAlias } from 'pathe/utils'
import { directoryToURL } from './internal/esm'
import { tryUseNuxt } from './context'
import { isIgnored } from './ignore'
import { toArray } from './utils'
@ -201,7 +202,7 @@ async function _resolvePathGranularly (path: string, opts: ResolvePathOptions =
}
// Try to resolve as module id
const resolvedModulePath = await _resolvePath(_path, { url: [cwd, ...modulesDir] }).catch(() => null)
const resolvedModulePath = await _resolvePath(_path, { url: [cwd, ...modulesDir].map(d => directoryToURL(d)) }).catch(() => null)
if (resolvedModulePath) {
return {
path: resolvedModulePath,

View File

@ -9,7 +9,7 @@ import { gte } from 'semver'
import { readPackageJSON } from 'pkg-types'
import { filterInPlace } from './utils'
import { tryResolveModule } from './internal/esm'
import { directoryToURL, tryResolveModule } from './internal/esm'
import { getDirectory } from './module/install'
import { tryUseNuxt, useNuxt } from './context'
import { resolveNuxtModule } from './resolve'
@ -244,6 +244,8 @@ export async function _generateTypes (nuxt: Nuxt) {
tsConfig.compilerOptions.paths ||= {}
tsConfig.include ||= []
const importPaths = nuxt.options.modulesDir.map(d => directoryToURL(d))
for (const alias in aliases) {
if (excludedAlias.some(re => re.test(alias))) {
continue
@ -251,7 +253,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]!, nuxt.options.modulesDir)
const resolvedModule = await tryResolveModule(aliases[alias]!, importPaths)
if (resolvedModule) {
absolutePath = resolvedModule
stats = await fsp.stat(resolvedModule).catch(() => null)

View File

@ -1,7 +1,7 @@
import type { EventType } from '@parcel/watcher'
import type { FSWatcher } from 'chokidar'
import { watch as chokidarWatch } from 'chokidar'
import { createIsIgnored, importModule, isIgnored, tryResolveModule, useNuxt } from '@nuxt/kit'
import { createIsIgnored, directoryToURL, importModule, isIgnored, tryResolveModule, useNuxt } from '@nuxt/kit'
import { debounce } from 'perfect-debounce'
import { normalize, relative, resolve } from 'pathe'
import type { Nuxt, NuxtBuilder } from 'nuxt/schema'
@ -196,7 +196,7 @@ 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])
const watcherPath = await tryResolveModule('@parcel/watcher', [nuxt.options.rootDir, ...nuxt.options.modulesDir].map(d => directoryToURL(d)))
if (!watcherPath) {
logger.warn('Falling back to `chokidar-granular` as `@parcel/watcher` cannot be resolved in your project.')
return false
@ -248,7 +248,7 @@ async function bundle (nuxt: Nuxt) {
}
async function loadBuilder (nuxt: Nuxt, builder: string): Promise<NuxtBuilder> {
const builderPath = await tryResolveModule(builder, [nuxt.options.rootDir, import.meta.url])
const builderPath = await tryResolveModule(builder, [directoryToURL(nuxt.options.rootDir), new URL(import.meta.url)])
if (!builderPath) {
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\``)

View File

@ -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, 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, resolvePath, runWithNuxtContext, tryResolveModule, useNitro } from '@nuxt/kit'
import type { Nuxt, NuxtHooks, NuxtModule, NuxtOptions } from 'nuxt/schema'
import type { PackageJson } from 'pkg-types'
import { readPackageJSON } from 'pkg-types'
@ -390,12 +390,13 @@ async function initNuxt (nuxt: Nuxt) {
}
nuxt.hook('modules:done', async () => {
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', nuxt.options.modulesDir) ?? 'unctx',
helperModule: await tryResolveModule('unctx', importPaths) ?? 'unctx',
},
}))

View File

@ -1,7 +1,7 @@
import { parseNodeModulePath, resolvePath } from 'mlly'
import { isAbsolute, normalize } from 'pathe'
import type { Plugin } from 'vite'
import { resolveAlias } from '@nuxt/kit'
import { directoryToURL, resolveAlias } from '@nuxt/kit'
import type { Nuxt } from '@nuxt/schema'
import { pkgDir } from '../../dirs'
@ -37,7 +37,7 @@ export function resolveDeepImportsPlugin (nuxt: Nuxt): Plugin {
const dir = parseNodeModulePath(normalisedImporter).dir || pkgDir
return await this.resolve?.(normalisedId, dir, { skipSelf: true }) ?? await resolvePath(id, {
url: [dir, ...nuxt.options.modulesDir],
url: [dir, ...nuxt.options.modulesDir].map(d => directoryToURL(d)),
conditions,
}).catch(() => {
logger.debug('Could not resolve id', id, importer)

View File

@ -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, importModule, tryResolveModule } from '@nuxt/kit'
import { createIsIgnored, createResolver, defineNuxtModule, directoryToURL, importModule, tryResolveModule } from '@nuxt/kit'
import { generateTypes, resolveSchema as resolveUntypedSchema } from 'untyped'
import type { Schema, SchemaDefinition } from 'untyped'
import untypedPlugin from 'untyped/babel-plugin'
@ -57,7 +57,7 @@ export default defineNuxtModule({
})
if (nuxt.options.experimental.watcher === 'parcel') {
const watcherPath = await tryResolveModule('@parcel/watcher', [nuxt.options.rootDir, ...nuxt.options.modulesDir])
const watcherPath = await tryResolveModule('@parcel/watcher', [nuxt.options.rootDir, ...nuxt.options.modulesDir].map(dir => directoryToURL(dir)))
if (watcherPath) {
const { subscribe } = await importModule<typeof import('@parcel/watcher')>(watcherPath)
for (const layer of nuxt.options._layers) {

View File

@ -1,11 +1,11 @@
import { resolvePackageJSON } from 'pkg-types'
import { resolvePath as _resolvePath } from 'mlly'
import { dirname } from 'pathe'
import { tryUseNuxt } from '@nuxt/kit'
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, conditions: ['types', 'import', 'require'] })
const r = await _resolvePath(path, { url: searchPaths?.map(d => directoryToURL(d)), conditions: ['types', 'import', 'require'] })
if (subpath) {
return r.replace(/(?:\.d)?\.[mc]?[jt]s$/, '')
}

View File

@ -1,5 +1,5 @@
import { resolve } from 'pathe'
import { addComponent, addImportsSources, addPlugin, addTemplate, defineNuxtModule, tryResolveModule } from '@nuxt/kit'
import { addComponent, addImportsSources, addPlugin, addTemplate, defineNuxtModule, directoryToURL, tryResolveModule } from '@nuxt/kit'
import type { NuxtOptions } from '@nuxt/schema'
import { distDir } from '../dirs'
@ -52,7 +52,8 @@ export default defineNuxtModule<NuxtOptions['unhead']>({
})
// Opt-out feature allowing dependencies using @vueuse/head to work
const unheadVue = await tryResolveModule('@unhead/vue', nuxt.options.modulesDir) || '@unhead/vue'
const importPaths = nuxt.options.modulesDir.map(d => directoryToURL(d))
const unheadVue = await tryResolveModule('@unhead/vue', importPaths) || '@unhead/vue'
if (nuxt.options.experimental.polyfillVueUseHead) {
// backwards compatibility
nuxt.options.alias['@vueuse/head'] = unheadVue

View File

@ -1,5 +1,5 @@
import { existsSync } from 'node:fs'
import { addBuildPlugin, addTemplate, addTypeTemplate, createIsIgnored, defineNuxtModule, resolveAlias, tryResolveModule, updateTemplates, useNuxt } from '@nuxt/kit'
import { addBuildPlugin, addTemplate, addTypeTemplate, createIsIgnored, defineNuxtModule, directoryToURL, resolveAlias, tryResolveModule, updateTemplates, useNuxt } from '@nuxt/kit'
import { isAbsolute, join, normalize, relative, resolve } from 'pathe'
import type { Import, Unimport } from 'unimport'
import { createUnimport, scanDirExports, toExports } from 'unimport'
@ -181,6 +181,8 @@ function addDeclarationTemplates (ctx: Unimport, options: Partial<ImportsOptions
const SUPPORTED_EXTENSION_RE = new RegExp(`\\.(${nuxt.options.extensions.map(i => i.replace('.', '')).join('|')})$`)
const importPaths = nuxt.options.modulesDir.map(dir => directoryToURL(dir))
async function cacheImportPaths (imports: Import[]) {
const importSource = Array.from(new Set(imports.map(i => i.from)))
// skip relative import paths for node_modules that are explicitly installed
@ -190,7 +192,7 @@ function addDeclarationTemplates (ctx: Unimport, options: Partial<ImportsOptions
}
let path = resolveAlias(from)
if (!isAbsolute(path)) {
path = await tryResolveModule(from, nuxt.options.modulesDir).then(async (r) => {
path = await tryResolveModule(from, importPaths).then(async (r) => {
if (!r) { return r }
const { dir, name } = parseNodeModulePath(r)

View File

@ -2,7 +2,7 @@ import { resolve } from 'pathe'
import * as vite from 'vite'
import vuePlugin from '@vitejs/plugin-vue'
import viteJsxPlugin from '@vitejs/plugin-vue-jsx'
import { logger, resolvePath, tryImportModule } from '@nuxt/kit'
import { directoryToURL, logger, resolvePath, tryImportModule } from '@nuxt/kit'
import { joinURL, withTrailingSlash, withoutLeadingSlash } from 'ufo'
import type { ViteConfig } from '@nuxt/schema'
import type { PackageJson } from 'pkg-types'
@ -117,7 +117,7 @@ export async function buildServer (ctx: ViteBuildContext) {
if (!ctx.nuxt.options.dev) {
const runtimeDependencies = await tryImportModule<PackageJson>('nitropack/package.json', {
paths: ctx.nuxt.options.modulesDir,
url: ctx.nuxt.options.modulesDir.map(d => directoryToURL(d)),
})?.then(r => r?.dependencies ? Object.keys(r.dependencies) : []).catch(() => []) || []
if (Array.isArray(serverConfig.ssr!.external)) {
serverConfig.ssr!.external.push(