Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions packages/vite/src/node/__tests__/config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -803,6 +803,42 @@
`)
})

test('loadConfigFromFile with import.meta.resolve', async () => {
const result = await loadConfigFromFile(
{ command: 'build', mode: 'production' },
path.resolve(fixtures, './import-meta-resolve/vite.config.mjs'),
path.resolve(fixtures, './import-meta-resolve'),
)
expect(result).toBeTruthy()
expect(result?.config).toHaveProperty('define')
expect(result?.config.define).toHaveProperty('IMPORT_META_URL')
expect(result?.config.define).toHaveProperty('IMPORT_META_RESOLVE_TEST')
expect(typeof result?.config.define.IMPORT_META_RESOLVE_TEST).toBe('string')
expect(result?.config.define.IMPORT_META_RESOLVE_TEST).toMatch(

Check failure on line 817 in packages/vite/src/node/__tests__/config.spec.ts

View workflow job for this annotation

GitHub Actions / Build&Test: node-22, windows-latest

packages/vite/src/node/__tests__/config.spec.ts > loadConfigFromFile > loadConfigFromFile with import.meta.resolve

AssertionError: expected 'file:///D:/D:/a/vite/vite/packages/vi…' to match /test-module\.js$/ - Expected: /test-module\.js$/ + Received: "file:///D:/D:/a/vite/vite/packages/vite/src/node/__tests__/fixtures/config/import-meta-resolve/test-module" ❯ packages/vite/src/node/__tests__/config.spec.ts:817:60
/test-module\.js$/,
)
})

test('loadConfigFromFile with import.meta.resolve advanced cases', async () => {
const result = await loadConfigFromFile(
{ command: 'build', mode: 'production' },
path.resolve(fixtures, './import-meta-resolve/vite.advanced.mjs'),
path.resolve(fixtures, './import-meta-resolve'),
)
expect(result).toBeTruthy()
expect(result?.config).toHaveProperty('define')

// Check all resolved paths
expect(result?.config.define.RESOLVED_MODULE).toMatch(/test-module\.js$/)

Check failure on line 832 in packages/vite/src/node/__tests__/config.spec.ts

View workflow job for this annotation

GitHub Actions / Build&Test: node-22, windows-latest

packages/vite/src/node/__tests__/config.spec.ts > loadConfigFromFile > loadConfigFromFile with import.meta.resolve advanced cases

AssertionError: expected 'file:///D:/D:/a/vite/vite/packages/vi…' to match /test-module\.js$/ - Expected: /test-module\.js$/ + Received: "file:///D:/D:/a/vite/vite/packages/vite/src/node/__tests__/fixtures/config/import-meta-resolve/test-module" ❯ packages/vite/src/node/__tests__/config.spec.ts:832:51
expect(result?.config.define.RESOLVED_WITH_EXT).toMatch(/test-module\.js$/)
expect(result?.config.define.BASE_URL).toMatch(/vite\.advanced\.mjs$/)

// Both should resolve to the same file
expect(result?.config.define.RESOLVED_MODULE).toBe(
result?.config.define.RESOLVED_WITH_EXT,
)
})

describe('loadConfigFromFile with configLoader: native', () => {
const fixtureRoot = path.resolve(fixtures, './native-import')

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Test module for import.meta.resolve
export const testValue = 'test-module-value'
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Test config that uses import.meta.resolve with different cases
export default {
define: {
// Basic usage
RESOLVED_MODULE: import.meta.resolve('./test-module'),
// With explicit extension
RESOLVED_WITH_EXT: import.meta.resolve('./test-module.js'),
// URL context
BASE_URL: import.meta.url,
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Test config that uses import.meta.resolve
export default {
define: {
IMPORT_META_URL: import.meta.url,
IMPORT_META_RESOLVE_TEST: import.meta.resolve('./test-module'),
},
}
196 changes: 139 additions & 57 deletions packages/vite/src/node/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type { PluginContextMeta, RollupOptions } from 'rollup'
import picomatch from 'picomatch'
import { build } from 'esbuild'
import type { AnymatchFn } from '../types/anymatch'
import { createImportMetaResolver } from '../module-runner/importMetaResolver'
import { withTrailingSlash } from '../shared/utils'
import {
CLIENT_ENTRY,
Expand Down Expand Up @@ -1922,6 +1923,8 @@ async function bundleConfigFile(
const dirnameVarName = '__vite_injected_original_dirname'
const filenameVarName = '__vite_injected_original_filename'
const importMetaUrlVarName = '__vite_injected_original_import_meta_url'
const importMetaResolveVarName =
'__vite_injected_original_import_meta_resolve'
const result = await build({
absWorkingDir: process.cwd(),
entryPoints: [fileName],
Expand All @@ -1941,6 +1944,7 @@ async function bundleConfigFile(
'import.meta.url': importMetaUrlVarName,
'import.meta.dirname': dirnameVarName,
'import.meta.filename': filenameVarName,
'import.meta.resolve': importMetaResolveVarName,
},
plugins: [
{
Expand Down Expand Up @@ -2034,14 +2038,21 @@ async function bundleConfigFile(
setup(build) {
build.onLoad({ filter: /\.[cm]?[jt]s$/ }, async (args) => {
const contents = await fsp.readFile(args.path, 'utf-8')
const fileUrl = pathToFileURL(args.path).href

const injectValues =
`const ${dirnameVarName} = ${JSON.stringify(
path.dirname(args.path),
)};` +
`const ${filenameVarName} = ${JSON.stringify(args.path)};` +
`const ${importMetaUrlVarName} = ${JSON.stringify(
pathToFileURL(args.path).href,
)};`
`const ${importMetaUrlVarName} = ${JSON.stringify(fileUrl)};` +
`const ${importMetaResolveVarName} = (function(specifier, parent) {
// Use Node.js built-in import.meta.resolve when available
if (typeof globalThis !== 'undefined' && globalThis.__vite_import_meta_resolve_impl) {
return globalThis.__vite_import_meta_resolve_impl(specifier, parent || ${JSON.stringify(fileUrl)});
}
throw new Error('[config] "import.meta.resolve" is not supported.');
});`

return {
loader: args.path.endsWith('ts') ? 'ts' : 'js',
Expand All @@ -2064,72 +2075,143 @@ interface NodeModuleWithCompile extends NodeModule {
}

const _require = createRequire(/** #__KEEP__ */ import.meta.url)

// Set up import.meta.resolve support for config loading
let importMetaResolverSetup: Promise<void> | undefined
async function setupImportMetaResolverForConfig() {
if (!importMetaResolverSetup) {
importMetaResolverSetup = (async () => {
try {
await createImportMetaResolver()
} catch {
// Ignore errors - import.meta.resolve may not be available
}
})()
}
return importMetaResolverSetup
}

async function loadConfigFromBundledFile(
fileName: string,
bundledCode: string,
isESM: boolean,
): Promise<UserConfigExport> {
// for esm, before we can register loaders without requiring users to run node
// with --experimental-loader themselves, we have to do a hack here:
// write it to disk, load it with native Node ESM, then delete the file.
if (isESM) {
// Storing the bundled file in node_modules/ is avoided for Deno
// because Deno only supports Node.js style modules under node_modules/
// and configs with `npm:` import statements will fail when executed.
let nodeModulesDir =
typeof process.versions.deno === 'string'
? undefined
: findNearestNodeModules(path.dirname(fileName))
if (nodeModulesDir) {
try {
await fsp.mkdir(path.resolve(nodeModulesDir, '.vite-temp/'), {
recursive: true,
})
} catch (e) {
if (e.code === 'EACCES') {
// If there is no access permission, a temporary configuration file is created by default.
nodeModulesDir = undefined
} else {
throw e
// Set up import.meta.resolve support
await setupImportMetaResolverForConfig()

// Create simpler resolver implementation using Node.js built-in capabilities
const createSimpleResolver = () => {
// Try to use native import.meta.resolve if available
try {
const testResolve = eval('import.meta.resolve')
if (typeof testResolve === 'function') {
return (specifier: string, parent: string) => {
return testResolve(specifier, parent)
}
}
} catch {
// Fall back to require.resolve for relative paths
}
const hash = `timestamp-${Date.now()}-${Math.random().toString(16).slice(2)}`
const tempFileName = nodeModulesDir
? path.resolve(
nodeModulesDir,
`.vite-temp/${path.basename(fileName)}.${hash}.mjs`,
)
: `${fileName}.${hash}.mjs`
await fsp.writeFile(tempFileName, bundledCode)
try {
return (await import(pathToFileURL(tempFileName).href)).default
} finally {
fs.unlink(tempFileName, () => {}) // Ignore errors

return (specifier: string, parent: string) => {
if (specifier.startsWith('.')) {
// Handle relative imports
const parentDir = path.dirname(parent.replace(/^file:\/\//, ''))
let resolved = path.resolve(parentDir, specifier)

// Try to add extension if not present
if (!path.extname(resolved)) {
// Try common extensions
const extensions = ['.js', '.mjs', '.cjs', '.ts', '.mts', '.cts']
for (const ext of extensions) {
const withExt = resolved + ext
if (fs.existsSync(withExt)) {
resolved = withExt
break
}
}
}

return pathToFileURL(resolved).href
}
// For non-relative imports, just return as is or throw
throw new Error(`[config] Cannot resolve "${specifier}" from "${parent}"`)
}
}
// for cjs, we can register a custom loader via `_require.extensions`
else {
const extension = path.extname(fileName)
// We don't use fsp.realpath() here because it has the same behaviour as
// fs.realpath.native. On some Windows systems, it returns uppercase volume
// letters (e.g. "C:\") while the Node.js loader uses lowercase volume letters.
// See https://github.com/vitejs/vite/issues/12923
const realFileName = await promisifiedRealpath(fileName)
const loaderExt = extension in _require.extensions ? extension : '.js'
const defaultLoader = _require.extensions[loaderExt]!
_require.extensions[loaderExt] = (module: NodeModule, filename: string) => {
if (filename === realFileName) {
;(module as NodeModuleWithCompile)._compile(bundledCode, filename)
} else {
defaultLoader(module, filename)

// @ts-expect-error - adding global function for config loading
globalThis.__vite_import_meta_resolve_impl = createSimpleResolver()

try {
// for esm, before we can register loaders without requiring users to run node
// with --experimental-loader themselves, we have to do a hack here:
// write it to disk, load it with native Node ESM, then delete the file.
if (isESM) {
// Storing the bundled file in node_modules/ is avoided for Deno
// because Deno only supports Node.js style modules under node_modules/
// and configs with `npm:` import statements will fail when executed.
let nodeModulesDir =
typeof process.versions.deno === 'string'
? undefined
: findNearestNodeModules(path.dirname(fileName))
if (nodeModulesDir) {
try {
await fsp.mkdir(path.resolve(nodeModulesDir, '.vite-temp/'), {
recursive: true,
})
} catch (e) {
if (e.code === 'EACCES') {
// If there is no access permission, a temporary configuration file is created by default.
nodeModulesDir = undefined
} else {
throw e
}
}
}
const hash = `timestamp-${Date.now()}-${Math.random().toString(16).slice(2)}`
const tempFileName = nodeModulesDir
? path.resolve(
nodeModulesDir,
`.vite-temp/${path.basename(fileName)}.${hash}.mjs`,
)
: `${fileName}.${hash}.mjs`
await fsp.writeFile(tempFileName, bundledCode)
try {
return (await import(pathToFileURL(tempFileName).href)).default
} finally {
fs.unlink(tempFileName, () => {}) // Ignore errors
}
}
// for cjs, we can register a custom loader via `_require.extensions`
else {
const extension = path.extname(fileName)
// We don't use fsp.realpath() here because it has the same behaviour as
// fs.realpath.native. On some Windows systems, it returns uppercase volume
// letters (e.g. "C:\") while the Node.js loader uses lowercase volume letters.
// See https://github.com/vitejs/vite/issues/12923
const realFileName = await promisifiedRealpath(fileName)
const loaderExt = extension in _require.extensions ? extension : '.js'
const defaultLoader = _require.extensions[loaderExt]!
_require.extensions[loaderExt] = (
module: NodeModule,
filename: string,
) => {
if (filename === realFileName) {
;(module as NodeModuleWithCompile)._compile(bundledCode, filename)
} else {
defaultLoader(module, filename)
}
}
// clear cache in case of server restart
delete _require.cache[_require.resolve(fileName)]
const raw = _require(fileName)
_require.extensions[loaderExt] = defaultLoader
return raw.__esModule ? raw.default : raw
}
// clear cache in case of server restart
delete _require.cache[_require.resolve(fileName)]
const raw = _require(fileName)
_require.extensions[loaderExt] = defaultLoader
return raw.__esModule ? raw.default : raw
} finally {
// Clean up global resolver implementation
// @ts-expect-error - removing global function after config loading
delete globalThis.__vite_import_meta_resolve_impl
}
}

Expand Down
Loading