From d2b30bb3b28ad503cfc93ba4988fb0443f6cc416 Mon Sep 17 00:00:00 2001 From: patak Date: Fri, 1 Dec 2023 20:47:11 +0100 Subject: [PATCH 1/9] perf: async fs calls in resolve and package handling --- .../vite/src/node/__tests__/build.spec.ts | 151 ++++++++---------- packages/vite/src/node/build.ts | 75 +++++---- packages/vite/src/node/config.ts | 54 ++++--- packages/vite/src/node/optimizer/index.ts | 2 +- packages/vite/src/node/optimizer/resolve.ts | 14 +- packages/vite/src/node/packages.ts | 29 ++-- .../src/node/plugins/assetImportMetaUrl.ts | 2 +- .../vite/src/node/plugins/importAnalysis.ts | 2 +- packages/vite/src/node/plugins/resolve.ts | 150 +++++++++-------- .../src/node/plugins/workerImportMetaUrl.ts | 2 +- packages/vite/src/node/ssr/ssrExternal.ts | 57 +++---- packages/vite/src/node/ssr/ssrModuleLoader.ts | 8 +- packages/vite/src/node/utils.ts | 44 ++--- 13 files changed, 311 insertions(+), 279 deletions(-) diff --git a/packages/vite/src/node/__tests__/build.spec.ts b/packages/vite/src/node/__tests__/build.spec.ts index 1b632a8b20fdcf..25cd4577dfdb7a 100644 --- a/packages/vite/src/node/__tests__/build.spec.ts +++ b/packages/vite/src/node/__tests__/build.spec.ts @@ -4,7 +4,7 @@ import colors from 'picocolors' import { describe, expect, test, vi } from 'vitest' import type { OutputChunk, OutputOptions, RollupOutput } from 'rollup' import type { LibraryFormats, LibraryOptions } from '../build' -import { build, resolveBuildOutputs, resolveLibFilename } from '../build' +import { build, createResolveLibFilename, resolveBuildOutputs } from '../build' import type { Logger } from '../logger' import { createLogger } from '../logger' @@ -205,75 +205,71 @@ describe('resolveBuildOutputs', () => { }) describe('resolveLibFilename', () => { - test('custom filename function', () => { - const filename = resolveLibFilename( + test('custom filename function', async () => { + const resolveLibFilename = await createResolveLibFilename( { fileName: (format) => `custom-filename-function.${format}.js`, entry: 'mylib.js', }, 'es', - 'myLib', resolve(__dirname, 'packages/name'), ) - + const filename = resolveLibFilename({ name: 'myLib' }) expect(filename).toBe('custom-filename-function.es.js') }) - test('custom filename string', () => { - const filename = resolveLibFilename( + test('custom filename string', async () => { + const resolveLibFilename = await createResolveLibFilename( { fileName: 'custom-filename', entry: 'mylib.js', }, 'es', - 'myLib', resolve(__dirname, 'packages/name'), ) - + const filename = resolveLibFilename({ name: 'myLib' }) expect(filename).toBe('custom-filename.mjs') }) - test('package name as filename', () => { - const filename = resolveLibFilename( + test('package name as filename', async () => { + const resolveLibFilename = await createResolveLibFilename( { entry: 'mylib.js', }, 'es', - 'myLib', resolve(__dirname, 'packages/name'), ) - + const filename = resolveLibFilename({ name: 'myLib' }) expect(filename).toBe('mylib.mjs') }) - test('custom filename and no package name', () => { - const filename = resolveLibFilename( + test('custom filename and no package name', async () => { + const resolveLibFilename = await createResolveLibFilename( { fileName: 'custom-filename', entry: 'mylib.js', }, 'es', - 'myLib', resolve(__dirname, 'packages/noname'), ) - + const filename = resolveLibFilename({ name: 'myLib' }) expect(filename).toBe('custom-filename.mjs') }) - test('missing filename', () => { + test('missing filename', async () => { + const resolveLibFilename = await createResolveLibFilename( + { + entry: 'mylib.js', + }, + 'es', + resolve(__dirname, 'packages/noname'), + ) expect(() => { - resolveLibFilename( - { - entry: 'mylib.js', - }, - 'es', - 'myLib', - resolve(__dirname, 'packages/noname'), - ) + resolveLibFilename({ name: 'myLib' }) }).toThrow() }) - test('commonjs package extensions', () => { + test('commonjs package extensions', async () => { const formatsToFilenames: FormatsToFileNames = [ ['es', 'my-lib.mjs'], ['umd', 'my-lib.umd.js'], @@ -282,18 +278,17 @@ describe('resolveLibFilename', () => { ] for (const [format, expectedFilename] of formatsToFilenames) { - const filename = resolveLibFilename( + const resolveLibFilename = await createResolveLibFilename( baseLibOptions, format, - 'myLib', resolve(__dirname, 'packages/noname'), ) - + const filename = resolveLibFilename({ name: 'myLib' }) expect(filename).toBe(expectedFilename) } }) - test('module package extensions', () => { + test('module package extensions', async () => { const formatsToFilenames: FormatsToFileNames = [ ['es', 'my-lib.js'], ['umd', 'my-lib.umd.cjs'], @@ -302,39 +297,37 @@ describe('resolveLibFilename', () => { ] for (const [format, expectedFilename] of formatsToFilenames) { - const filename = resolveLibFilename( + const resolveLibFilename = await createResolveLibFilename( baseLibOptions, format, - 'myLib', resolve(__dirname, 'packages/module'), ) - + const filename = resolveLibFilename({ name: 'myLib' }) expect(expectedFilename).toBe(filename) } }) - test('multiple entries with aliases', () => { + test('multiple entries with aliases', async () => { const libOptions: LibraryOptions = { entry: { entryA: 'entryA.js', entryB: 'entryB.js', }, } - + const resolveLibFilename = await createResolveLibFilename( + libOptions, + 'es', + resolve(__dirname, 'packages/name'), + ) const [fileName1, fileName2] = ['entryA', 'entryB'].map((entryAlias) => - resolveLibFilename( - libOptions, - 'es', - entryAlias, - resolve(__dirname, 'packages/name'), - ), + resolveLibFilename({ name: entryAlias }), ) expect(fileName1).toBe('entryA.mjs') expect(fileName2).toBe('entryB.mjs') }) - test('multiple entries with aliases: custom filename function', () => { + test('multiple entries with aliases: custom filename function', async () => { const libOptions: LibraryOptions = { entry: { entryA: 'entryA.js', @@ -343,21 +336,20 @@ describe('resolveLibFilename', () => { fileName: (format, entryAlias) => `custom-filename-function.${entryAlias}.${format}.js`, } - + const resolveLibFilename = await createResolveLibFilename( + libOptions, + 'es', + resolve(__dirname, 'packages/name'), + ) const [fileName1, fileName2] = ['entryA', 'entryB'].map((entryAlias) => - resolveLibFilename( - libOptions, - 'es', - entryAlias, - resolve(__dirname, 'packages/name'), - ), + resolveLibFilename({ name: entryAlias }), ) expect(fileName1).toBe('custom-filename-function.entryA.es.js') expect(fileName2).toBe('custom-filename-function.entryB.es.js') }) - test('multiple entries with aliases: custom filename string', () => { + test('multiple entries with aliases: custom filename string', async () => { const libOptions: LibraryOptions = { entry: { entryA: 'entryA.js', @@ -365,71 +357,68 @@ describe('resolveLibFilename', () => { }, fileName: 'custom-filename', } - + const resolveLibFilename = await createResolveLibFilename( + libOptions, + 'es', + resolve(__dirname, 'packages/name'), + ) const [fileName1, fileName2] = ['entryA', 'entryB'].map((entryAlias) => - resolveLibFilename( - libOptions, - 'es', - entryAlias, - resolve(__dirname, 'packages/name'), - ), + resolveLibFilename({ name: entryAlias }), ) expect(fileName1).toBe('custom-filename.mjs') expect(fileName2).toBe('custom-filename.mjs') }) - test('multiple entries as array', () => { + test('multiple entries as array', async () => { const libOptions: LibraryOptions = { entry: ['entryA.js', 'entryB.js'], } - + const resolveLibFilename = await createResolveLibFilename( + libOptions, + 'es', + resolve(__dirname, 'packages/name'), + ) const [fileName1, fileName2] = ['entryA', 'entryB'].map((entryAlias) => - resolveLibFilename( - libOptions, - 'es', - entryAlias, - resolve(__dirname, 'packages/name'), - ), + resolveLibFilename({ name: entryAlias }), ) expect(fileName1).toBe('entryA.mjs') expect(fileName2).toBe('entryB.mjs') }) - test('multiple entries as array: custom filename function', () => { + test('multiple entries as array: custom filename function', async () => { const libOptions: LibraryOptions = { entry: ['entryA.js', 'entryB.js'], fileName: (format, entryAlias) => `custom-filename-function.${entryAlias}.${format}.js`, } - + const resolveLibFilename = await createResolveLibFilename( + libOptions, + 'es', + resolve(__dirname, 'packages/name'), + ) const [fileName1, fileName2] = ['entryA', 'entryB'].map((entryAlias) => - resolveLibFilename( - libOptions, - 'es', - entryAlias, - resolve(__dirname, 'packages/name'), - ), + resolveLibFilename({ name: entryAlias }), ) expect(fileName1).toBe('custom-filename-function.entryA.es.js') expect(fileName2).toBe('custom-filename-function.entryB.es.js') }) - test('multiple entries as array: custom filename string', () => { + test('multiple entries as array: custom filename string', async () => { const libOptions: LibraryOptions = { entry: ['entryA.js', 'entryB.js'], fileName: 'custom-filename', } + const resolveLibFilename = await createResolveLibFilename( + libOptions, + 'es', + resolve(__dirname, 'packages/name'), + ) const [fileName1, fileName2] = ['entryA', 'entryB'].map((entryAlias) => - resolveLibFilename( - libOptions, - 'es', - entryAlias, - resolve(__dirname, 'packages/name'), - ), + resolveLibFilename({ name: entryAlias }), ) expect(fileName1).toBe('custom-filename.mjs') diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 402eed8b7c8399..35947e1e6b1431 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -557,7 +557,9 @@ export async function build( let bundle: RollupBuild | undefined try { - const buildOutputOptions = (output: OutputOptions = {}): OutputOptions => { + const buildOutputOptions = async ( + output: OutputOptions = {}, + ): Promise => { // @ts-expect-error See https://github.com/vitejs/vite/issues/5812#issuecomment-984345618 if (output.output) { config.logger.warn( @@ -589,10 +591,21 @@ export async function build( ssrNodeBuild || libOptions ? resolveOutputJsExtension( format, - findNearestPackageData(config.root, config.packageCache)?.data - .type, + (await findNearestPackageData(config.root, config.packageCache)) + ?.data.type, ) : 'js' + + const resolveLibFilename = libOptions + ? await createResolveLibFilename( + libOptions, + format, + config.root, + jsExt, + config.packageCache, + ) + : undefined + return { dir: outDir, // Default format is 'es' for regular and for SSR builds @@ -607,15 +620,7 @@ export async function build( entryFileNames: ssr ? `[name].${jsExt}` : libOptions - ? ({ name }) => - resolveLibFilename( - libOptions, - format, - name, - config.root, - jsExt, - config.packageCache, - ) + ? resolveLibFilename : path.posix.join(options.assetsDir, `[name]-[hash].${jsExt}`), chunkFileNames: libOptions ? `[name]-[hash].${jsExt}` @@ -642,10 +647,10 @@ export async function build( if (Array.isArray(outputs)) { for (const resolvedOutput of outputs) { - normalizedOutputs.push(buildOutputOptions(resolvedOutput)) + normalizedOutputs.push(await buildOutputOptions(resolvedOutput)) } } else { - normalizedOutputs.push(buildOutputOptions(outputs)) + normalizedOutputs.push(await buildOutputOptions(outputs)) } const outDirs = normalizedOutputs.map(({ dir }) => resolve(dir!)) @@ -792,37 +797,37 @@ function resolveOutputJsExtension( } } -export function resolveLibFilename( +export async function createResolveLibFilename( libOptions: LibraryOptions, format: ModuleFormat, - entryName: string, root: string, extension?: JsExt, packageCache?: PackageCache, -): string { - if (typeof libOptions.fileName === 'function') { - return libOptions.fileName(format, entryName) - } +): Promise<(entry: { name: string }) => string> { + const packageJson = (await findNearestPackageData(root, packageCache))?.data + return (entry: { name: string }) => { + if (typeof libOptions.fileName === 'function') { + return libOptions.fileName(format, entry.name) + } + const name = + libOptions.fileName || + (packageJson && typeof libOptions.entry === 'string' + ? getPkgName(packageJson.name) + : entry.name) - const packageJson = findNearestPackageData(root, packageCache)?.data - const name = - libOptions.fileName || - (packageJson && typeof libOptions.entry === 'string' - ? getPkgName(packageJson.name) - : entryName) + if (!name) + throw new Error( + 'Name in package.json is required if option "build.lib.fileName" is not provided.', + ) - if (!name) - throw new Error( - 'Name in package.json is required if option "build.lib.fileName" is not provided.', - ) + extension ??= resolveOutputJsExtension(format, packageJson?.type) - extension ??= resolveOutputJsExtension(format, packageJson?.type) + if (format === 'cjs' || format === 'es') { + return `${name}.${extension}` + } - if (format === 'cjs' || format === 'es') { - return `${name}.${extension}` + return `${name}.${format}.${extension}` } - - return `${name}.${format}.${extension}` } export function resolveBuildOutputs( diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index 39de230daa4d2d..a9f7db943285a0 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -586,7 +586,7 @@ export async function resolveConfig( ) // resolve cache directory - const pkgDir = findNearestPackageData(resolvedRoot, packageCache)?.dir + const pkgDir = (await findNearestPackageData(resolvedRoot, packageCache))?.dir const cacheDir = normalizePath( config.cacheDir ? path.resolve(resolvedRoot, config.cacheDir) @@ -969,7 +969,7 @@ export async function loadConfigFromFile( return null } - const isESM = isFilePathESM(resolvedPath) + const isESM = await isFilePathESM(resolvedPath) try { const bundled = await bundleConfigFile(resolvedPath, isESM) @@ -1028,30 +1028,32 @@ async function bundleConfigFile( name: 'externalize-deps', setup(build) { const packageCache = new Map() - const resolveByViteResolver = ( + const resolveByViteResolver = async ( id: string, importer: string, isRequire: boolean, ) => { - return tryNodeResolve( - id, - importer, - { - root: path.dirname(fileName), - isBuild: true, - isProduction: true, - preferRelative: false, - tryIndex: true, - mainFields: [], - conditions: [], - overrideConditions: ['node'], - dedupe: [], - extensions: DEFAULT_EXTENSIONS, - preserveSymlinks: false, - packageCache, - isRequire, - }, - false, + return ( + await tryNodeResolve( + id, + importer, + { + root: path.dirname(fileName), + isBuild: true, + isProduction: true, + preferRelative: false, + tryIndex: true, + mainFields: [], + conditions: [], + overrideConditions: ['node'], + dedupe: [], + extensions: DEFAULT_EXTENSIONS, + preserveSymlinks: false, + packageCache, + isRequire, + }, + false, + ) )?.id } @@ -1077,16 +1079,16 @@ async function bundleConfigFile( const isImport = isESM || kind === 'dynamic-import' let idFsPath: string | undefined try { - idFsPath = resolveByViteResolver(id, importer, !isImport) + idFsPath = await resolveByViteResolver(id, importer, !isImport) } catch (e) { if (!isImport) { let canResolveWithImport = false try { - canResolveWithImport = !!resolveByViteResolver( + canResolveWithImport = !!(await resolveByViteResolver( id, importer, false, - ) + )) } catch {} if (canResolveWithImport) { throw new Error( @@ -1104,7 +1106,7 @@ async function bundleConfigFile( if ( idFsPath && !isImport && - isFilePathESM(idFsPath, packageCache) + (await isFilePathESM(idFsPath, packageCache)) ) { throw new Error( `${JSON.stringify( diff --git a/packages/vite/src/node/optimizer/index.ts b/packages/vite/src/node/optimizer/index.ts index 12e40fef5ff14f..25935c77d3302c 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -851,7 +851,7 @@ export async function addManuallyIncludedOptimizeDeps( for (let i = 0; i < includes.length; i++) { const id = includes[i] if (glob.isDynamicPattern(id)) { - const globIds = expandGlobIds(id, config) + const globIds = await expandGlobIds(id, config) includes.splice(i, 1, ...globIds) i += globIds.length - 1 } diff --git a/packages/vite/src/node/optimizer/resolve.ts b/packages/vite/src/node/optimizer/resolve.ts index f03d69a5c3697e..ed82d53c935695 100644 --- a/packages/vite/src/node/optimizer/resolve.ts +++ b/packages/vite/src/node/optimizer/resolve.ts @@ -25,7 +25,7 @@ export function createOptimizeDepsIncludeResolver( // 'foo > bar > baz' => 'foo > bar' & 'baz' const nestedRoot = id.substring(0, lastArrowIndex).trim() const nestedPath = id.substring(lastArrowIndex + 1).trim() - const basedir = nestedResolveBasedir( + const basedir = await nestedResolveBasedir( nestedRoot, config.root, config.resolve.preserveSymlinks, @@ -42,11 +42,14 @@ export function createOptimizeDepsIncludeResolver( /** * Expand the glob syntax in `optimizeDeps.include` to proper import paths */ -export function expandGlobIds(id: string, config: ResolvedConfig): string[] { +export async function expandGlobIds( + id: string, + config: ResolvedConfig, +): Promise { const pkgName = getNpmPackageName(id) if (!pkgName) return [] - const pkgData = resolvePackageData( + const pkgData = await resolvePackageData( pkgName, config.root, config.resolve.preserveSymlinks, @@ -160,14 +163,15 @@ function getFirstExportStringValue( /** * Continuously resolve the basedir of packages separated by '>' */ -function nestedResolveBasedir( +async function nestedResolveBasedir( id: string, basedir: string, preserveSymlinks = false, ) { const pkgs = id.split('>').map((pkg) => pkg.trim()) for (const pkg of pkgs) { - basedir = resolvePackageData(pkg, basedir, preserveSymlinks)?.dir || basedir + basedir = + (await resolvePackageData(pkg, basedir, preserveSymlinks))?.dir || basedir } return basedir } diff --git a/packages/vite/src/node/packages.ts b/packages/vite/src/node/packages.ts index 9b35ecc3b82e9c..2af7b4d627824a 100644 --- a/packages/vite/src/node/packages.ts +++ b/packages/vite/src/node/packages.ts @@ -1,10 +1,11 @@ import fs from 'node:fs' +import fsp from 'node:fs/promises' import path from 'node:path' import { createRequire } from 'node:module' import { createFilter, isInNodeModules, - safeRealpathSync, + safeRealpath, tryStatSync, } from './utils' import type { Plugin } from './plugin' @@ -52,12 +53,12 @@ function invalidatePackageData( }) } -export function resolvePackageData( +export async function resolvePackageData( pkgName: string, basedir: string, preserveSymlinks = false, packageCache?: PackageCache, -): PackageData | null { +): Promise { if (pnp) { const cacheKey = getRpdCacheKey(pkgName, basedir, preserveSymlinks) if (packageCache?.has(cacheKey)) return packageCache.get(cacheKey)! @@ -68,7 +69,7 @@ export function resolvePackageData( }) if (!pkg) return null - const pkgData = loadPackageData(path.join(pkg, 'package.json')) + const pkgData = await loadPackageData(path.join(pkg, 'package.json')) packageCache?.set(cacheKey, pkgData) return pkgData } catch { @@ -92,8 +93,8 @@ export function resolvePackageData( const pkg = path.join(basedir, 'node_modules', pkgName, 'package.json') try { if (fs.existsSync(pkg)) { - const pkgPath = preserveSymlinks ? pkg : safeRealpathSync(pkg) - const pkgData = loadPackageData(pkgPath) + const pkgPath = preserveSymlinks ? pkg : await safeRealpath(pkg) + const pkgData = await loadPackageData(pkgPath) if (packageCache) { setRpdCache( @@ -118,10 +119,10 @@ export function resolvePackageData( return null } -export function findNearestPackageData( +export async function findNearestPackageData( basedir: string, packageCache?: PackageCache, -): PackageData | null { +): Promise { const originalBasedir = basedir while (basedir) { if (packageCache) { @@ -132,7 +133,7 @@ export function findNearestPackageData( const pkgPath = path.join(basedir, 'package.json') if (tryStatSync(pkgPath)?.isFile()) { try { - const pkgData = loadPackageData(pkgPath) + const pkgData = await loadPackageData(pkgPath) if (packageCache) { setFnpdCache(packageCache, pkgData, basedir, originalBasedir) @@ -151,11 +152,11 @@ export function findNearestPackageData( } // Finds the nearest package.json with a `name` field -export function findNearestMainPackageData( +export async function findNearestMainPackageData( basedir: string, packageCache?: PackageCache, -): PackageData | null { - const nearestPackage = findNearestPackageData(basedir, packageCache) +): Promise { + const nearestPackage = await findNearestPackageData(basedir, packageCache) return ( nearestPackage && (nearestPackage.data.name @@ -167,8 +168,8 @@ export function findNearestMainPackageData( ) } -export function loadPackageData(pkgPath: string): PackageData { - const data = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) +export async function loadPackageData(pkgPath: string): Promise { + const data = JSON.parse(await fsp.readFile(pkgPath, 'utf-8')) const pkgDir = path.dirname(pkgPath) const { sideEffects } = data let hasSideEffects: (id: string) => boolean diff --git a/packages/vite/src/node/plugins/assetImportMetaUrl.ts b/packages/vite/src/node/plugins/assetImportMetaUrl.ts index f57db10321befb..1996ed3faff376 100644 --- a/packages/vite/src/node/plugins/assetImportMetaUrl.ts +++ b/packages/vite/src/node/plugins/assetImportMetaUrl.ts @@ -109,7 +109,7 @@ export function assetImportMetaUrlPlugin(config: ResolvedConfig): Plugin { let file: string | undefined if (url[0] === '.') { file = slash(path.resolve(path.dirname(id), url)) - file = tryFsResolve(file, fsResolveOptions) ?? file + file = (await tryFsResolve(file, fsResolveOptions)) ?? file } else { assetResolver ??= config.createResolver({ extensions: [], diff --git a/packages/vite/src/node/plugins/importAnalysis.ts b/packages/vite/src/node/plugins/importAnalysis.ts index 08b827d4d8fb75..54348ce21a2a6a 100644 --- a/packages/vite/src/node/plugins/importAnalysis.ts +++ b/packages/vite/src/node/plugins/importAnalysis.ts @@ -488,7 +488,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { } // skip ssr external if (ssr) { - if (shouldExternalizeForSSR(specifier, importer, config)) { + if (await shouldExternalizeForSSR(specifier, importer, config)) { return } if (isBuiltin(specifier)) { diff --git a/packages/vite/src/node/plugins/resolve.ts b/packages/vite/src/node/plugins/resolve.ts index 0c93628f6cfc5a..34ff6a644ce875 100644 --- a/packages/vite/src/node/plugins/resolve.ts +++ b/packages/vite/src/node/plugins/resolve.ts @@ -1,4 +1,5 @@ import fs from 'node:fs' +import fsp from 'node:fs/promises' import path from 'node:path' import colors from 'picocolors' import type { PartialResolvedId } from 'rollup' @@ -34,7 +35,7 @@ import { isTsRequest, isWindows, normalizePath, - safeRealpathSync, + safeRealpath, slash, tryStatSync, withTrailingSlash, @@ -114,7 +115,10 @@ export interface InternalResolveOptions extends Required { ssrOptimizeCheck?: boolean // Resolve using esbuild deps optimization getDepsOptimizer?: (ssr: boolean) => DepsOptimizer | undefined - shouldExternalize?: (id: string, importer?: string) => boolean | undefined + shouldExternalize?: ( + id: string, + importer?: string, + ) => Promise /** * Set by createResolver, we only care about the resolved id. moduleSideEffects @@ -184,7 +188,7 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin { conditions: ssr ? ssrConditions : resolveOptions.conditions, } - const resolvedImports = resolveSubpathImports( + const resolvedImports = await resolveSubpathImports( id, importer, options, @@ -240,7 +244,7 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin { (rootInRoot || !id.startsWith(withTrailingSlash(root))) ) { const fsPath = path.resolve(root, id.slice(1)) - if ((res = tryFsResolve(fsPath, options))) { + if ((res = await tryFsResolve(fsPath, options))) { debug?.(`[url] ${colors.cyan(id)} -> ${colors.dim(res)}`) return ensureVersionQuery(res, id, options, depsOptimizer) } @@ -279,12 +283,17 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin { if ( targetWeb && options.mainFields.includes('browser') && - (res = tryResolveBrowserMapping(fsPath, importer, options, true)) + (res = await tryResolveBrowserMapping( + fsPath, + importer, + options, + true, + )) ) { return res } - if ((res = tryFsResolve(fsPath, options))) { + if ((res = await tryFsResolve(fsPath, options))) { res = ensureVersionQuery(res, id, options, depsOptimizer) debug?.(`[relative] ${colors.cyan(id)} -> ${colors.dim(res)}`) @@ -296,7 +305,7 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin { options.isBuild && !importer?.endsWith('.html') ) { - const resPkg = findNearestPackageData( + const resPkg = await findNearestPackageData( path.dirname(res), options.packageCache, ) @@ -315,7 +324,7 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin { if (isWindows && id[0] === '/') { const basedir = importer ? path.dirname(importer) : process.cwd() const fsPath = path.resolve(basedir, id) - if ((res = tryFsResolve(fsPath, options))) { + if ((res = await tryFsResolve(fsPath, options))) { debug?.(`[drive-relative] ${colors.cyan(id)} -> ${colors.dim(res)}`) return ensureVersionQuery(res, id, options, depsOptimizer) } @@ -324,7 +333,7 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin { // absolute fs paths if ( isNonDriveRelativeAbsolutePath(id) && - (res = tryFsResolve(id, options)) + (res = await tryFsResolve(id, options)) ) { debug?.(`[fs] ${colors.cyan(id)} -> ${colors.dim(res)}`) return ensureVersionQuery(res, id, options, depsOptimizer) @@ -343,7 +352,7 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin { // bare package imports, perform node resolve if (bareImportRE.test(id)) { - const external = options.shouldExternalize?.(id, importer) + const external = await options.shouldExternalize?.(id, importer) if ( !external && asSrc && @@ -363,7 +372,7 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin { if ( targetWeb && options.mainFields.includes('browser') && - (res = tryResolveBrowserMapping( + (res = await tryResolveBrowserMapping( id, importer, options, @@ -375,7 +384,7 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin { } if ( - (res = tryNodeResolve( + (res = await tryNodeResolve( id, importer, options, @@ -453,7 +462,7 @@ export default new Proxy({}, { } } -function resolveSubpathImports( +async function resolveSubpathImports( id: string, importer: string | undefined, options: InternalResolveOptions, @@ -461,7 +470,7 @@ function resolveSubpathImports( ) { if (!importer || !id.startsWith(subpathImportsPrefix)) return const basedir = path.dirname(importer) - const pkgData = findNearestPackageData(basedir, options.packageCache) + const pkgData = await findNearestPackageData(basedir, options.packageCache) if (!pkgData) return let importsPath = resolveExportsOrImports( @@ -516,13 +525,13 @@ function splitFileAndPostfix(path: string) { return { file, postfix: path.slice(file.length) } } -export function tryFsResolve( +export async function tryFsResolve( fsPath: string, options: InternalResolveOptions, tryIndex = true, targetWeb = true, skipPackageJson = false, -): string | undefined { +): Promise { // Dependencies like es5-ext use `#` in their paths. We don't support `#` in user // source code so we only need to perform the check for dependencies. // We don't support `?` in node_modules paths, so we only need to check in this branch. @@ -532,7 +541,7 @@ export function tryFsResolve( // We only need to check foo#bar?baz and foo#bar, ignore foo?bar#baz if (queryIndex < 0 || queryIndex > hashIndex) { const file = queryIndex > hashIndex ? fsPath.slice(0, queryIndex) : fsPath - const res = tryCleanFsResolve( + const res = await tryCleanFsResolve( file, options, tryIndex, @@ -544,7 +553,7 @@ export function tryFsResolve( } const { file, postfix } = splitFileAndPostfix(fsPath) - const res = tryCleanFsResolve( + const res = await tryCleanFsResolve( file, options, tryIndex, @@ -557,13 +566,13 @@ export function tryFsResolve( const knownTsOutputRE = /\.(?:js|mjs|cjs|jsx)$/ const isPossibleTsOutput = (url: string): boolean => knownTsOutputRE.test(url) -function tryCleanFsResolve( +async function tryCleanFsResolve( file: string, options: InternalResolveOptions, tryIndex = true, targetWeb = true, skipPackageJson = false, -): string | undefined { +): Promise { const { tryPrefix, extensions, preserveSymlinks } = options const fileStat = tryStatSync(file) @@ -584,7 +593,7 @@ function tryCleanFsResolve( const fileExt = path.extname(file) const fileName = file.slice(0, -fileExt.length) if ( - (res = tryResolveRealFile( + (res = await tryResolveRealFile( fileName + fileExt.replace('js', 'ts'), preserveSymlinks, )) @@ -593,13 +602,13 @@ function tryCleanFsResolve( // for .js, also try .tsx if ( fileExt === '.js' && - (res = tryResolveRealFile(fileName + '.tsx', preserveSymlinks)) + (res = await tryResolveRealFile(fileName + '.tsx', preserveSymlinks)) ) return res } if ( - (res = tryResolveRealFileWithExtensions( + (res = await tryResolveRealFileWithExtensions( file, extensions, preserveSymlinks, @@ -610,10 +619,11 @@ function tryCleanFsResolve( if (tryPrefix) { const prefixed = `${dirPath}/${options.tryPrefix}${path.basename(file)}` - if ((res = tryResolveRealFile(prefixed, preserveSymlinks))) return res + if ((res = await tryResolveRealFile(prefixed, preserveSymlinks))) + return res if ( - (res = tryResolveRealFileWithExtensions( + (res = await tryResolveRealFileWithExtensions( prefixed, extensions, preserveSymlinks, @@ -633,11 +643,11 @@ function tryCleanFsResolve( try { if (fs.existsSync(pkgPath)) { if (!options.preserveSymlinks) { - pkgPath = safeRealpathSync(pkgPath) + pkgPath = await safeRealpath(pkgPath) } // path points to a node package - const pkg = loadPackageData(pkgPath) - return resolvePackageEntry(dirPath, pkg, targetWeb, options) + const pkg = await loadPackageData(pkgPath) + return await resolvePackageEntry(dirPath, pkg, targetWeb, options) } } catch (e) { // This check is best effort, so if an entry is not found, skip error for now @@ -647,7 +657,7 @@ function tryCleanFsResolve( } if ( - (res = tryResolveRealFileWithExtensions( + (res = await tryResolveRealFileWithExtensions( `${dirPath}/index`, extensions, preserveSymlinks, @@ -657,7 +667,7 @@ function tryCleanFsResolve( if (tryPrefix) { if ( - (res = tryResolveRealFileWithExtensions( + (res = await tryResolveRealFileWithExtensions( `${dirPath}/${options.tryPrefix}index`, extensions, preserveSymlinks, @@ -668,21 +678,21 @@ function tryCleanFsResolve( } } -function tryResolveRealFile( +async function tryResolveRealFile( file: string, preserveSymlinks: boolean, -): string | undefined { +): Promise { const stat = tryStatSync(file) if (stat?.isFile()) return getRealPath(file, preserveSymlinks) } -function tryResolveRealFileWithExtensions( +async function tryResolveRealFileWithExtensions( filePath: string, extensions: string[], preserveSymlinks: boolean, -): string | undefined { +): Promise { for (const ext of extensions) { - const res = tryResolveRealFile(filePath + ext, preserveSymlinks) + const res = await tryResolveRealFile(filePath + ext, preserveSymlinks) if (res) return res } } @@ -695,7 +705,7 @@ export type InternalResolveOptionsWithOverrideConditions = overrideConditions?: string[] } -export function tryNodeResolve( +export async function tryNodeResolve( id: string, importer: string | null | undefined, options: InternalResolveOptionsWithOverrideConditions, @@ -704,7 +714,7 @@ export function tryNodeResolve( ssr: boolean = false, externalize?: boolean, allowLinkedExternal: boolean = true, -): PartialResolvedId | undefined { +): Promise { const { root, dedupe, isBuild, preserveSymlinks, packageCache } = options // check for deep import, e.g. "my-lib/foo" @@ -727,7 +737,12 @@ export function tryNodeResolve( basedir = root } - const pkg = resolvePackageData(pkgId, basedir, preserveSymlinks, packageCache) + const pkg = await resolvePackageData( + pkgId, + basedir, + preserveSymlinks, + packageCache, + ) if (!pkg) { // if import can't be found, check if it's an optional peer dep. // if so, we can resolve to a special id that errors only when imported. @@ -737,7 +752,8 @@ export function tryNodeResolve( !id.includes('\0') && bareImportRE.test(id) ) { - const mainPkg = findNearestMainPackageData(basedir, packageCache)?.data + const mainPkg = (await findNearestMainPackageData(basedir, packageCache)) + ?.data if (mainPkg) { const pkgName = getNpmPackageName(id) if ( @@ -759,14 +775,14 @@ export function tryNodeResolve( let resolved: string | undefined try { - resolved = resolveId(unresolvedId, pkg, targetWeb, options) + resolved = await resolveId(unresolvedId, pkg, targetWeb, options) } catch (err) { if (!options.tryEsmOnly) { throw err } } if (!resolved && options.tryEsmOnly) { - resolved = resolveId(unresolvedId, pkg, targetWeb, { + resolved = await resolveId(unresolvedId, pkg, targetWeb, { ...options, isRequire: false, mainFields: DEFAULT_MAIN_FIELDS, @@ -857,7 +873,7 @@ export function tryNodeResolve( (!options.ssrOptimizeCheck && !isBuild && ssr) || // Only optimize non-external CJS deps during SSR by default (ssr && - isFilePathESM(resolved, options.packageCache) && + (await isFilePathESM(resolved, options.packageCache)) && !(include?.includes(pkgId) || include?.includes(id))) if (options.ssrOptimizeCheck) { @@ -935,11 +951,13 @@ export async function tryOptimizedResolve( if (idPkgDir == null) { const pkgName = getNpmPackageName(id) if (!pkgName) break - idPkgDir = resolvePackageData( - pkgName, - importer, - preserveSymlinks, - packageCache, + idPkgDir = ( + await resolvePackageData( + pkgName, + importer, + preserveSymlinks, + packageCache, + ) )?.dir // if still null, it likely means that this id isn't a dep for importer. // break to bail early @@ -954,12 +972,12 @@ export async function tryOptimizedResolve( } } -export function resolvePackageEntry( +export async function resolvePackageEntry( id: string, { dir, data, setResolvedCache, getResolvedCache }: PackageData, targetWeb: boolean, options: InternalResolveOptions, -): string | undefined { +): Promise { const { file: idWithoutPostfix, postfix } = splitFileAndPostfix(id) const cached = getResolvedCache('.', targetWeb) @@ -987,7 +1005,7 @@ export function resolvePackageEntry( for (const field of options.mainFields) { if (field === 'browser') { if (targetWeb) { - entryPoint = tryResolveBrowserEntry(dir, data, options) + entryPoint = await tryResolveBrowserEntry(dir, data, options) if (entryPoint) { break } @@ -1028,7 +1046,7 @@ export function resolvePackageEntry( } const entryPointPath = path.join(dir, entry) - const resolvedEntryPoint = tryFsResolve( + const resolvedEntryPoint = await tryFsResolve( entryPointPath, options, true, @@ -1097,7 +1115,7 @@ function resolveExportsOrImports( return result ? result[0] : undefined } -function resolveDeepImport( +async function resolveDeepImport( id: string, { webResolvedImports, @@ -1108,7 +1126,7 @@ function resolveDeepImport( }: PackageData, targetWeb: boolean, options: InternalResolveOptions, -): string | undefined { +): Promise { const cache = getResolvedCache(id, targetWeb) if (cache) { return cache @@ -1160,7 +1178,7 @@ function resolveDeepImport( } if (relativeId) { - const resolved = tryFsResolve( + const resolved = await tryFsResolve( path.join(dir, relativeId), options, !exportsField, // try index only if no exports field @@ -1176,7 +1194,7 @@ function resolveDeepImport( } } -function tryResolveBrowserMapping( +async function tryResolveBrowserMapping( id: string, importer: string | undefined, options: InternalResolveOptions, @@ -1186,15 +1204,16 @@ function tryResolveBrowserMapping( let res: string | undefined const pkg = importer && - findNearestPackageData(path.dirname(importer), options.packageCache) + (await findNearestPackageData(path.dirname(importer), options.packageCache)) if (pkg && isObject(pkg.data.browser)) { const mapId = isFilePath ? './' + slash(path.relative(pkg.dir, id)) : id const browserMappedPath = mapWithBrowserField(mapId, pkg.data.browser) if (browserMappedPath) { if ( (res = bareImportRE.test(browserMappedPath) - ? tryNodeResolve(browserMappedPath, importer, options, true)?.id - : tryFsResolve(path.join(pkg.dir, browserMappedPath), options)) + ? (await tryNodeResolve(browserMappedPath, importer, options, true)) + ?.id + : await tryFsResolve(path.join(pkg.dir, browserMappedPath), options)) ) { debug?.(`[browser mapped] ${colors.cyan(id)} -> ${colors.dim(res)}`) let result: PartialResolvedId = { id: res } @@ -1202,7 +1221,7 @@ function tryResolveBrowserMapping( return result } if (!options.scan && options.isBuild) { - const resPkg = findNearestPackageData( + const resPkg = await findNearestPackageData( path.dirname(res), options.packageCache, ) @@ -1221,7 +1240,7 @@ function tryResolveBrowserMapping( } } -function tryResolveBrowserEntry( +async function tryResolveBrowserEntry( dir: string, data: PackageData['data'], options: InternalResolveOptions, @@ -1248,12 +1267,12 @@ function tryResolveBrowserEntry( // the heuristics here is to actually read the browser entry when // possible and check for hints of ESM. If it is not ESM, prefer "module" // instead; Otherwise, assume it's ESM and use it. - const resolvedBrowserEntry = tryFsResolve( + const resolvedBrowserEntry = await tryFsResolve( path.join(dir, browserEntry), options, ) if (resolvedBrowserEntry) { - const content = fs.readFileSync(resolvedBrowserEntry, 'utf-8') + const content = await fsp.readFile(resolvedBrowserEntry, 'utf-8') if (hasESMSyntax(content)) { // likely ESM, prefer browser return browserEntry @@ -1298,9 +1317,12 @@ function equalWithoutSuffix(path: string, key: string, suffix: string) { return key.endsWith(suffix) && key.slice(0, -suffix.length) === path } -function getRealPath(resolved: string, preserveSymlinks?: boolean): string { +async function getRealPath( + resolved: string, + preserveSymlinks?: boolean, +): Promise { if (!preserveSymlinks && browserExternalId !== resolved) { - resolved = safeRealpathSync(resolved) + resolved = await safeRealpath(resolved) } return normalizePath(resolved) } diff --git a/packages/vite/src/node/plugins/workerImportMetaUrl.ts b/packages/vite/src/node/plugins/workerImportMetaUrl.ts index e88d1ac3362a43..1f456916eb87df 100644 --- a/packages/vite/src/node/plugins/workerImportMetaUrl.ts +++ b/packages/vite/src/node/plugins/workerImportMetaUrl.ts @@ -166,7 +166,7 @@ export function workerImportMetaUrlPlugin(config: ResolvedConfig): Plugin { let file: string | undefined if (url[0] === '.') { file = path.resolve(path.dirname(id), url) - file = tryFsResolve(file, fsResolveOptions) ?? file + file = (await tryFsResolve(file, fsResolveOptions)) ?? file } else { workerResolver ??= config.createResolver({ extensions: [], diff --git a/packages/vite/src/node/ssr/ssrExternal.ts b/packages/vite/src/node/ssr/ssrExternal.ts index 3035fa972b3d19..b64da95dc3aca0 100644 --- a/packages/vite/src/node/ssr/ssrExternal.ts +++ b/packages/vite/src/node/ssr/ssrExternal.ts @@ -12,16 +12,17 @@ import type { ResolvedConfig } from '..' const debug = createDebugger('vite:ssr-external') -const isSsrExternalCache = new WeakMap< - ResolvedConfig, - (id: string, importer?: string) => boolean | undefined ->() +type IsSsrExternalFunction = ( + id: string, + importer?: string, +) => Promise +const isSsrExternalCache = new WeakMap() export function shouldExternalizeForSSR( id: string, importer: string | undefined, config: ResolvedConfig, -): boolean | undefined { +): Promise { let isSsrExternal = isSsrExternalCache.get(config) if (!isSsrExternal) { isSsrExternal = createIsSsrExternal(config) @@ -32,7 +33,7 @@ export function shouldExternalizeForSSR( export function createIsConfiguredAsSsrExternal( config: ResolvedConfig, -): (id: string, importer?: string) => boolean { +): (id: string, importer?: string) => Promise { const { ssr, root } = config const noExternal = ssr?.noExternal const noExternalFilter = @@ -50,30 +51,32 @@ export function createIsConfiguredAsSsrExternal( conditions: targetConditions, } - const isExternalizable = ( + const isExternalizable = async ( id: string, importer?: string, configuredAsExternal?: boolean, - ): boolean => { + ): Promise => { if (!bareImportRE.test(id) || id.includes('\0')) { return false } try { - return !!tryNodeResolve( - id, - // Skip passing importer in build to avoid externalizing non-hoisted dependencies - // unresolvable from root (which would be unresolvable from output bundles also) - config.command === 'build' ? undefined : importer, - resolveOptions, - ssr?.target === 'webworker', - undefined, - true, - // try to externalize, will return undefined or an object without - // a external flag if it isn't externalizable - true, - // Allow linked packages to be externalized if they are explicitly - // configured as external - !!configuredAsExternal, + return !!( + await tryNodeResolve( + id, + // Skip passing importer in build to avoid externalizing non-hoisted dependencies + // unresolvable from root (which would be unresolvable from output bundles also) + config.command === 'build' ? undefined : importer, + resolveOptions, + ssr?.target === 'webworker', + undefined, + true, + // try to externalize, will return undefined or an object without + // a external flag if it isn't externalizable + true, + // Allow linked packages to be externalized if they are explicitly + // configured as external + !!configuredAsExternal, + ) )?.external } catch (e) { debug?.( @@ -86,7 +89,7 @@ export function createIsConfiguredAsSsrExternal( // Returns true if it is configured as external, false if it is filtered // by noExternal and undefined if it isn't affected by the explicit config - return (id: string, importer?: string) => { + return async (id: string, importer?: string) => { const { ssr } = config if (ssr) { if ( @@ -120,18 +123,18 @@ export function createIsConfiguredAsSsrExternal( function createIsSsrExternal( config: ResolvedConfig, -): (id: string, importer?: string) => boolean | undefined { +): (id: string, importer?: string) => Promise { const processedIds = new Map() const isConfiguredAsExternal = createIsConfiguredAsSsrExternal(config) - return (id: string, importer?: string) => { + return async (id: string, importer?: string) => { if (processedIds.has(id)) { return processedIds.get(id) } let external = false if (id[0] !== '.' && !path.isAbsolute(id)) { - external = isBuiltin(id) || isConfiguredAsExternal(id, importer) + external = isBuiltin(id) || (await isConfiguredAsExternal(id, importer)) } processedIds.set(id, external) return external diff --git a/packages/vite/src/node/ssr/ssrModuleLoader.ts b/packages/vite/src/node/ssr/ssrModuleLoader.ts index c48d0751df60bf..0464a195533973 100644 --- a/packages/vite/src/node/ssr/ssrModuleLoader.ts +++ b/packages/vite/src/node/ssr/ssrModuleLoader.ts @@ -295,7 +295,7 @@ async function nodeImport( if (isRuntimeHandled) { url = id } else { - const resolved = tryNodeResolve( + const resolved = await tryNodeResolve( id, importer, { ...resolveOptions, tryEsmOnly: true }, @@ -320,7 +320,7 @@ async function nodeImport( } else if (isRuntimeHandled) { return mod } else { - analyzeImportedModDifference( + await analyzeImportedModDifference( mod, url, id, @@ -362,7 +362,7 @@ function isPrimitive(value: any) { * Top-level imports and dynamic imports work slightly differently in Node.js. * This function normalizes the differences so it matches prod behaviour. */ -function analyzeImportedModDifference( +async function analyzeImportedModDifference( mod: any, filePath: string, rawId: string, @@ -372,7 +372,7 @@ function analyzeImportedModDifference( // No normalization needed if the user already dynamic imports this module if (metadata?.isDynamicImport) return // If file path is ESM, everything should be fine - if (isFilePathESM(filePath, packageCache)) return + if (await isFilePathESM(filePath, packageCache)) return // For non-ESM, named imports is done via static analysis with cjs-module-lexer in Node.js. // If the user named imports a specifier that can't be analyzed, error. diff --git a/packages/vite/src/node/utils.ts b/packages/vite/src/node/utils.ts index 4e893968c44444..ec0d6bfe0851c9 100644 --- a/packages/vite/src/node/utils.ts +++ b/packages/vite/src/node/utils.ts @@ -1,4 +1,5 @@ import fs from 'node:fs' +import fsp from 'node:fs/promises' import os from 'node:os' import path from 'node:path' import { exec } from 'node:child_process' @@ -432,10 +433,10 @@ export function lookupFile( } } -export function isFilePathESM( +export async function isFilePathESM( filePath: string, packageCache?: PackageCache, -): boolean { +): Promise { if (/\.m[jt]s$/.test(filePath)) { return true } else if (/\.c[jt]s$/.test(filePath)) { @@ -443,7 +444,10 @@ export function isFilePathESM( } else { // check package.json for type: "module" try { - const pkg = findNearestPackageData(path.dirname(filePath), packageCache) + const pkg = await findNearestPackageData( + path.dirname(filePath), + packageCache, + ) return pkg?.data.type === 'module' } catch { return false @@ -625,15 +629,17 @@ export function copyDir(srcDir: string, destDir: string): void { // `fs.realpathSync.native` resolves differently in Windows network drive, // causing file read errors. skip for now. // https://github.com/nodejs/node/issues/37737 -export let safeRealpathSync = isWindows - ? windowsSafeRealPathSync - : fs.realpathSync.native +export let safeRealpath = isWindows ? windowsSafeRealPath : fsp.realpath + +async function realpathFallback(path: string) { + return fs.realpathSync(path) +} // Based on https://github.com/larrybahr/windows-network-drive // MIT License, Copyright (c) 2017 Larry Bahr const windowsNetworkMap = new Map() -function windowsMappedRealpathSync(path: string) { - const realPath = fs.realpathSync.native(path) +async function windowsMappedRealpath(path: string) { + const realPath = await fsp.realpath(path) if (realPath.startsWith('\\\\')) { for (const [network, volume] of windowsNetworkMap) { if (realPath.startsWith(network)) return realPath.replace(network, volume) @@ -642,31 +648,31 @@ function windowsMappedRealpathSync(path: string) { return realPath } const parseNetUseRE = /^(\w+)? +(\w:) +([^ ]+)\s/ -let firstSafeRealPathSyncRun = false +let firstSafeRealPathRun = false -function windowsSafeRealPathSync(path: string): string { - if (!firstSafeRealPathSyncRun) { - optimizeSafeRealPathSync() - firstSafeRealPathSyncRun = true +async function windowsSafeRealPath(path: string): Promise { + if (!firstSafeRealPathRun) { + await optimizeSafeRealPath() + firstSafeRealPathRun = true } return fs.realpathSync(path) } -function optimizeSafeRealPathSync() { +async function optimizeSafeRealPath() { // Skip if using Node <18.10 due to MAX_PATH issue: https://github.com/vitejs/vite/issues/12931 const nodeVersion = process.versions.node.split('.').map(Number) if (nodeVersion[0] < 18 || (nodeVersion[0] === 18 && nodeVersion[1] < 10)) { - safeRealpathSync = fs.realpathSync + safeRealpath = realpathFallback return } // Check the availability `fs.realpathSync.native` // in Windows virtual and RAM disks that bypass the Volume Mount Manager, in programs such as imDisk // get the error EISDIR: illegal operation on a directory try { - fs.realpathSync.native(path.resolve('./')) + await fsp.realpath(path.resolve('./')) } catch (error) { if (error.message.includes('EISDIR: illegal operation on a directory')) { - safeRealpathSync = fs.realpathSync + safeRealpath = realpathFallback return } } @@ -680,9 +686,9 @@ function optimizeSafeRealPathSync() { if (m) windowsNetworkMap.set(m[3], m[2]) } if (windowsNetworkMap.size === 0) { - safeRealpathSync = fs.realpathSync.native + safeRealpath = fsp.realpath } else { - safeRealpathSync = windowsMappedRealpathSync + safeRealpath = windowsMappedRealpath } }) } From 0faf06c0cb4f9abb5ada86f2b1eee59cf33d900e Mon Sep 17 00:00:00 2001 From: patak Date: Sat, 2 Dec 2023 22:37:02 +0100 Subject: [PATCH 2/9] refactor: avoid creating promises in tryCleanFsResolve --- packages/vite/src/node/plugins/resolve.ts | 71 +++++++---------------- 1 file changed, 20 insertions(+), 51 deletions(-) diff --git a/packages/vite/src/node/plugins/resolve.ts b/packages/vite/src/node/plugins/resolve.ts index 34ff6a644ce875..83dc8e22384bfb 100644 --- a/packages/vite/src/node/plugins/resolve.ts +++ b/packages/vite/src/node/plugins/resolve.ts @@ -573,7 +573,7 @@ async function tryCleanFsResolve( targetWeb = true, skipPackageJson = false, ): Promise { - const { tryPrefix, extensions, preserveSymlinks } = options + const { tryPrefix, extensions } = options const fileStat = tryStatSync(file) @@ -592,44 +592,24 @@ async function tryCleanFsResolve( // try resolve .js, .mjs, .cjs or .jsx import to typescript file const fileExt = path.extname(file) const fileName = file.slice(0, -fileExt.length) - if ( - (res = await tryResolveRealFile( - fileName + fileExt.replace('js', 'ts'), - preserveSymlinks, - )) - ) - return res + if ((res = tryResolveFile(fileName + fileExt.replace('js', 'ts')))) + return getRealPath(res, options.preserveSymlinks) // for .js, also try .tsx - if ( - fileExt === '.js' && - (res = await tryResolveRealFile(fileName + '.tsx', preserveSymlinks)) - ) - return res + if (fileExt === '.js' && (res = tryResolveFile(fileName + '.tsx'))) + return getRealPath(res, options.preserveSymlinks) } - if ( - (res = await tryResolveRealFileWithExtensions( - file, - extensions, - preserveSymlinks, - )) - ) - return res + if ((res = tryResolveFileWithExtensions(file, extensions))) + return getRealPath(res, options.preserveSymlinks) if (tryPrefix) { const prefixed = `${dirPath}/${options.tryPrefix}${path.basename(file)}` - if ((res = await tryResolveRealFile(prefixed, preserveSymlinks))) - return res + if ((res = tryResolveFile(prefixed))) + return getRealPath(res, options.preserveSymlinks) - if ( - (res = await tryResolveRealFileWithExtensions( - prefixed, - extensions, - preserveSymlinks, - )) - ) - return res + if ((res = tryResolveFileWithExtensions(prefixed, extensions))) + return getRealPath(res, options.preserveSymlinks) } } } @@ -656,43 +636,32 @@ async function tryCleanFsResolve( } } - if ( - (res = await tryResolveRealFileWithExtensions( - `${dirPath}/index`, - extensions, - preserveSymlinks, - )) - ) - return res + if ((res = tryResolveFileWithExtensions(`${dirPath}/index`, extensions))) + return getRealPath(res, options.preserveSymlinks) if (tryPrefix) { if ( - (res = await tryResolveRealFileWithExtensions( + (res = tryResolveFileWithExtensions( `${dirPath}/${options.tryPrefix}index`, extensions, - preserveSymlinks, )) ) - return res + return getRealPath(res, options.preserveSymlinks) } } } -async function tryResolveRealFile( - file: string, - preserveSymlinks: boolean, -): Promise { +function tryResolveFile(file: string): string | undefined { const stat = tryStatSync(file) - if (stat?.isFile()) return getRealPath(file, preserveSymlinks) + if (stat?.isFile()) return file } -async function tryResolveRealFileWithExtensions( +function tryResolveFileWithExtensions( filePath: string, extensions: string[], - preserveSymlinks: boolean, -): Promise { +): string | undefined { for (const ext of extensions) { - const res = await tryResolveRealFile(filePath + ext, preserveSymlinks) + const res = tryResolveFile(filePath + ext) if (res) return res } } From 06462a8d66d704a68c5e8325c1ae728912e4b5a4 Mon Sep 17 00:00:00 2001 From: patak Date: Sat, 2 Dec 2023 22:43:24 +0100 Subject: [PATCH 3/9] test: check if windows in CI is happy forcing fs.realpathSync --- packages/vite/src/node/utils.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/vite/src/node/utils.ts b/packages/vite/src/node/utils.ts index ec0d6bfe0851c9..546d075e53fa06 100644 --- a/packages/vite/src/node/utils.ts +++ b/packages/vite/src/node/utils.ts @@ -2,7 +2,7 @@ import fs from 'node:fs' import fsp from 'node:fs/promises' import os from 'node:os' import path from 'node:path' -import { exec } from 'node:child_process' +// import { exec } from 'node:child_process' import { createHash } from 'node:crypto' import { URL, URLSearchParams, fileURLToPath } from 'node:url' import { builtinModules, createRequire } from 'node:module' @@ -629,12 +629,13 @@ export function copyDir(srcDir: string, destDir: string): void { // `fs.realpathSync.native` resolves differently in Windows network drive, // causing file read errors. skip for now. // https://github.com/nodejs/node/issues/37737 -export let safeRealpath = isWindows ? windowsSafeRealPath : fsp.realpath +export const safeRealpath = isWindows ? realpathFallback : fsp.realpath async function realpathFallback(path: string) { return fs.realpathSync(path) } +/* // Based on https://github.com/larrybahr/windows-network-drive // MIT License, Copyright (c) 2017 Larry Bahr const windowsNetworkMap = new Map() @@ -692,6 +693,7 @@ async function optimizeSafeRealPath() { } }) } +*/ export function ensureWatchedFile( watcher: FSWatcher, From 664284b39b0fa1ee56d09f207d54d0f753d47888 Mon Sep 17 00:00:00 2001 From: patak Date: Sat, 2 Dec 2023 22:50:56 +0100 Subject: [PATCH 4/9] Revert "test: check if windows in CI is happy forcing fs.realpathSync" This reverts commit 06462a8d66d704a68c5e8325c1ae728912e4b5a4. --- packages/vite/src/node/utils.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/vite/src/node/utils.ts b/packages/vite/src/node/utils.ts index 546d075e53fa06..ec0d6bfe0851c9 100644 --- a/packages/vite/src/node/utils.ts +++ b/packages/vite/src/node/utils.ts @@ -2,7 +2,7 @@ import fs from 'node:fs' import fsp from 'node:fs/promises' import os from 'node:os' import path from 'node:path' -// import { exec } from 'node:child_process' +import { exec } from 'node:child_process' import { createHash } from 'node:crypto' import { URL, URLSearchParams, fileURLToPath } from 'node:url' import { builtinModules, createRequire } from 'node:module' @@ -629,13 +629,12 @@ export function copyDir(srcDir: string, destDir: string): void { // `fs.realpathSync.native` resolves differently in Windows network drive, // causing file read errors. skip for now. // https://github.com/nodejs/node/issues/37737 -export const safeRealpath = isWindows ? realpathFallback : fsp.realpath +export let safeRealpath = isWindows ? windowsSafeRealPath : fsp.realpath async function realpathFallback(path: string) { return fs.realpathSync(path) } -/* // Based on https://github.com/larrybahr/windows-network-drive // MIT License, Copyright (c) 2017 Larry Bahr const windowsNetworkMap = new Map() @@ -693,7 +692,6 @@ async function optimizeSafeRealPath() { } }) } -*/ export function ensureWatchedFile( watcher: FSWatcher, From d3c8f9070870e0855e7bea95943d62054f8af49f Mon Sep 17 00:00:00 2001 From: patak Date: Sat, 2 Dec 2023 22:59:40 +0100 Subject: [PATCH 5/9] fix: optimizeSafeRealPath should be run in parallel --- packages/vite/src/node/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vite/src/node/utils.ts b/packages/vite/src/node/utils.ts index ec0d6bfe0851c9..08e28871e8ea56 100644 --- a/packages/vite/src/node/utils.ts +++ b/packages/vite/src/node/utils.ts @@ -652,7 +652,7 @@ let firstSafeRealPathRun = false async function windowsSafeRealPath(path: string): Promise { if (!firstSafeRealPathRun) { - await optimizeSafeRealPath() + optimizeSafeRealPath() firstSafeRealPathRun = true } return fs.realpathSync(path) From cafe43321127bc02c141287c3989090115447e84 Mon Sep 17 00:00:00 2001 From: patak Date: Sat, 2 Dec 2023 23:11:45 +0100 Subject: [PATCH 6/9] fix: back to realpathSync for windows --- packages/vite/src/node/utils.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/packages/vite/src/node/utils.ts b/packages/vite/src/node/utils.ts index 08e28871e8ea56..1bf08dded36116 100644 --- a/packages/vite/src/node/utils.ts +++ b/packages/vite/src/node/utils.ts @@ -631,15 +631,11 @@ export function copyDir(srcDir: string, destDir: string): void { // https://github.com/nodejs/node/issues/37737 export let safeRealpath = isWindows ? windowsSafeRealPath : fsp.realpath -async function realpathFallback(path: string) { - return fs.realpathSync(path) -} - // Based on https://github.com/larrybahr/windows-network-drive // MIT License, Copyright (c) 2017 Larry Bahr const windowsNetworkMap = new Map() -async function windowsMappedRealpath(path: string) { - const realPath = await fsp.realpath(path) +function windowsMappedRealpath(path: string) { + const realPath = fs.realpathSync.native(path) if (realPath.startsWith('\\\\')) { for (const [network, volume] of windowsNetworkMap) { if (realPath.startsWith(network)) return realPath.replace(network, volume) @@ -650,7 +646,7 @@ async function windowsMappedRealpath(path: string) { const parseNetUseRE = /^(\w+)? +(\w:) +([^ ]+)\s/ let firstSafeRealPathRun = false -async function windowsSafeRealPath(path: string): Promise { +function windowsSafeRealPath(path: string): string { if (!firstSafeRealPathRun) { optimizeSafeRealPath() firstSafeRealPathRun = true @@ -662,7 +658,7 @@ async function optimizeSafeRealPath() { // Skip if using Node <18.10 due to MAX_PATH issue: https://github.com/vitejs/vite/issues/12931 const nodeVersion = process.versions.node.split('.').map(Number) if (nodeVersion[0] < 18 || (nodeVersion[0] === 18 && nodeVersion[1] < 10)) { - safeRealpath = realpathFallback + safeRealpath = fs.realpathSync return } // Check the availability `fs.realpathSync.native` @@ -672,7 +668,7 @@ async function optimizeSafeRealPath() { await fsp.realpath(path.resolve('./')) } catch (error) { if (error.message.includes('EISDIR: illegal operation on a directory')) { - safeRealpath = realpathFallback + safeRealpath = fs.realpathSync return } } @@ -686,7 +682,7 @@ async function optimizeSafeRealPath() { if (m) windowsNetworkMap.set(m[3], m[2]) } if (windowsNetworkMap.size === 0) { - safeRealpath = fsp.realpath + safeRealpath = fs.realpathSync.native } else { safeRealpath = windowsMappedRealpath } From 8d6019125d295d6940d1f18e6896d0668e576594 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Sun, 3 Dec 2023 20:30:14 +0900 Subject: [PATCH 7/9] chore: revert "fix: back to realpathSync for windows" This reverts commit cafe43321127bc02c141287c3989090115447e84. --- packages/vite/src/node/utils.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/vite/src/node/utils.ts b/packages/vite/src/node/utils.ts index 1bf08dded36116..08e28871e8ea56 100644 --- a/packages/vite/src/node/utils.ts +++ b/packages/vite/src/node/utils.ts @@ -631,11 +631,15 @@ export function copyDir(srcDir: string, destDir: string): void { // https://github.com/nodejs/node/issues/37737 export let safeRealpath = isWindows ? windowsSafeRealPath : fsp.realpath +async function realpathFallback(path: string) { + return fs.realpathSync(path) +} + // Based on https://github.com/larrybahr/windows-network-drive // MIT License, Copyright (c) 2017 Larry Bahr const windowsNetworkMap = new Map() -function windowsMappedRealpath(path: string) { - const realPath = fs.realpathSync.native(path) +async function windowsMappedRealpath(path: string) { + const realPath = await fsp.realpath(path) if (realPath.startsWith('\\\\')) { for (const [network, volume] of windowsNetworkMap) { if (realPath.startsWith(network)) return realPath.replace(network, volume) @@ -646,7 +650,7 @@ function windowsMappedRealpath(path: string) { const parseNetUseRE = /^(\w+)? +(\w:) +([^ ]+)\s/ let firstSafeRealPathRun = false -function windowsSafeRealPath(path: string): string { +async function windowsSafeRealPath(path: string): Promise { if (!firstSafeRealPathRun) { optimizeSafeRealPath() firstSafeRealPathRun = true @@ -658,7 +662,7 @@ async function optimizeSafeRealPath() { // Skip if using Node <18.10 due to MAX_PATH issue: https://github.com/vitejs/vite/issues/12931 const nodeVersion = process.versions.node.split('.').map(Number) if (nodeVersion[0] < 18 || (nodeVersion[0] === 18 && nodeVersion[1] < 10)) { - safeRealpath = fs.realpathSync + safeRealpath = realpathFallback return } // Check the availability `fs.realpathSync.native` @@ -668,7 +672,7 @@ async function optimizeSafeRealPath() { await fsp.realpath(path.resolve('./')) } catch (error) { if (error.message.includes('EISDIR: illegal operation on a directory')) { - safeRealpath = fs.realpathSync + safeRealpath = realpathFallback return } } @@ -682,7 +686,7 @@ async function optimizeSafeRealPath() { if (m) windowsNetworkMap.set(m[3], m[2]) } if (windowsNetworkMap.size === 0) { - safeRealpath = fs.realpathSync.native + safeRealpath = fsp.realpath } else { safeRealpath = windowsMappedRealpath } From 6a2210dd362a5f2ababb1fc21228a7a318c95b95 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Sun, 3 Dec 2023 21:04:35 +0900 Subject: [PATCH 8/9] fix: use `fs.realpath.native` instead of `fsp.realpath` --- packages/vite/src/node/utils.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/vite/src/node/utils.ts b/packages/vite/src/node/utils.ts index 08e28871e8ea56..71735c08f9baef 100644 --- a/packages/vite/src/node/utils.ts +++ b/packages/vite/src/node/utils.ts @@ -9,6 +9,7 @@ import { builtinModules, createRequire } from 'node:module' import { promises as dns } from 'node:dns' import { performance } from 'node:perf_hooks' import type { AddressInfo, Server } from 'node:net' +import { promisify } from 'node:util' import type { FSWatcher } from 'chokidar' import remapping from '@ampproject/remapping' import type { DecodedSourceMap, RawSourceMap } from '@ampproject/remapping' @@ -630,6 +631,9 @@ export function copyDir(srcDir: string, destDir: string): void { // causing file read errors. skip for now. // https://github.com/nodejs/node/issues/37737 export let safeRealpath = isWindows ? windowsSafeRealPath : fsp.realpath +// use this instead of fsp.realpath for Windows +// it seems there's a bug in Node: https://github.com/nodejs/node/issues/51031 +const realpathAsync = promisify(fs.realpath.native) async function realpathFallback(path: string) { return fs.realpathSync(path) @@ -639,7 +643,7 @@ async function realpathFallback(path: string) { // MIT License, Copyright (c) 2017 Larry Bahr const windowsNetworkMap = new Map() async function windowsMappedRealpath(path: string) { - const realPath = await fsp.realpath(path) + const realPath = await realpathAsync(path) if (realPath.startsWith('\\\\')) { for (const [network, volume] of windowsNetworkMap) { if (realPath.startsWith(network)) return realPath.replace(network, volume) @@ -669,7 +673,7 @@ async function optimizeSafeRealPath() { // in Windows virtual and RAM disks that bypass the Volume Mount Manager, in programs such as imDisk // get the error EISDIR: illegal operation on a directory try { - await fsp.realpath(path.resolve('./')) + await realpathAsync(path.resolve('./')) } catch (error) { if (error.message.includes('EISDIR: illegal operation on a directory')) { safeRealpath = realpathFallback @@ -686,7 +690,7 @@ async function optimizeSafeRealPath() { if (m) windowsNetworkMap.set(m[3], m[2]) } if (windowsNetworkMap.size === 0) { - safeRealpath = fsp.realpath + safeRealpath = realpathAsync } else { safeRealpath = windowsMappedRealpath } From fef309935a25ae7acd8b455ec5de3d524edb8756 Mon Sep 17 00:00:00 2001 From: patak Date: Sat, 9 Dec 2023 14:51:40 +0100 Subject: [PATCH 9/9] chore: update return type --- packages/vite/LICENSE.md | 22 ++++++++++++++++++++++ packages/vite/src/node/ssr/ssrExternal.ts | 4 +--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/packages/vite/LICENSE.md b/packages/vite/LICENSE.md index 8f8d65ee1f257d..cdbe40a51014bb 100644 --- a/packages/vite/LICENSE.md +++ b/packages/vite/LICENSE.md @@ -579,6 +579,28 @@ License: MIT By: Rich Harris Repository: rollup/plugins +> The MIT License (MIT) +> +> Copyright (c) 2019 RollupJS Plugin Contributors (https://github.com/rollup/plugins/graphs/contributors) +> +> Permission is hereby granted, free of charge, to any person obtaining a copy +> of this software and associated documentation files (the "Software"), to deal +> in the Software without restriction, including without limitation the rights +> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +> copies of the Software, and to permit persons to whom the Software is +> furnished to do so, subject to the following conditions: +> +> The above copyright notice and this permission notice shall be included in +> all copies or substantial portions of the Software. +> +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +> THE SOFTWARE. + --------------------------------------- ## acorn diff --git a/packages/vite/src/node/ssr/ssrExternal.ts b/packages/vite/src/node/ssr/ssrExternal.ts index b64da95dc3aca0..c9f2f8efefcfdb 100644 --- a/packages/vite/src/node/ssr/ssrExternal.ts +++ b/packages/vite/src/node/ssr/ssrExternal.ts @@ -121,9 +121,7 @@ export function createIsConfiguredAsSsrExternal( } } -function createIsSsrExternal( - config: ResolvedConfig, -): (id: string, importer?: string) => Promise { +function createIsSsrExternal(config: ResolvedConfig): IsSsrExternalFunction { const processedIds = new Map() const isConfiguredAsExternal = createIsConfiguredAsSsrExternal(config)