diff --git a/packages/vite/src/node/__tests__/plugins/define.spec.ts b/packages/vite/src/node/__tests__/plugins/define.spec.ts index 8876a9d7ecc3e2..2165461c77acaa 100644 --- a/packages/vite/src/node/__tests__/plugins/define.spec.ts +++ b/packages/vite/src/node/__tests__/plugins/define.spec.ts @@ -64,6 +64,20 @@ describe('definePlugin', () => { ) }) + test('replace import.meta.env.UNKNOWN with undefined', async () => { + const transform = await createDefinePluginTransform() + expect(await transform('const foo = import.meta.env.UNKNOWN;')).toBe( + 'const foo = undefined ;\n', + ) + }) + + test('leave import.meta.env["UNKNOWN"] to runtime', async () => { + const transform = await createDefinePluginTransform() + expect(await transform('const foo = import.meta.env["UNKNOWN"];')).toMatch( + /const __vite_import_meta_env__ = .*;\nconst foo = __vite_import_meta_env__\["UNKNOWN"\];/, + ) + }) + test('preserve import.meta.env.UNKNOWN with override', async () => { const transform = await createDefinePluginTransform({ 'import.meta.env.UNKNOWN': 'import.meta.env.UNKNOWN', @@ -72,4 +86,27 @@ describe('definePlugin', () => { 'const foo = import.meta.env.UNKNOWN;\n', ) }) + + test('replace import.meta.env when it is a invalid json', async () => { + const transform = await createDefinePluginTransform({ + 'import.meta.env.LEGACY': '__VITE_IS_LEGACY__', + }) + + expect( + await transform( + 'const isLegacy = import.meta.env.LEGACY;\nimport.meta.env.UNDEFINED && console.log(import.meta.env.UNDEFINED);', + ), + ).toMatchInlineSnapshot(` + "const isLegacy = __VITE_IS_LEGACY__; + undefined && console.log(undefined ); + " + `) + }) + + test('replace bare import.meta.env', async () => { + const transform = await createDefinePluginTransform() + expect(await transform('const env = import.meta.env;')).toMatch( + /const __vite_import_meta_env__ = .*;\nconst env = __vite_import_meta_env__;/, + ) + }) }) diff --git a/packages/vite/src/node/plugins/define.ts b/packages/vite/src/node/plugins/define.ts index da50836294a0af..585bc0154fa263 100644 --- a/packages/vite/src/node/plugins/define.ts +++ b/packages/vite/src/node/plugins/define.ts @@ -2,12 +2,15 @@ import { transform } from 'esbuild' import { TraceMap, decodedMap, encodedMap } from '@jridgewell/trace-mapping' import type { ResolvedConfig } from '../config' import type { Plugin } from '../plugin' -import { escapeRegex, getHash } from '../utils' +import { escapeRegex } from '../utils' import { isCSSRequest } from './css' import { isHTMLRequest } from './html' const nonJsRe = /\.json(?:$|\?)/ const isNonJsRequest = (request: string): boolean => nonJsRe.test(request) +const importMetaEnvMarker = '__vite_import_meta_env__' +const bareImportMetaEnvRe = new RegExp(`${importMetaEnvMarker}(?!\\.)\\b`) +const importMetaEnvKeyRe = new RegExp(`${importMetaEnvMarker}\\..+?\\b`, 'g') export function definePlugin(config: ResolvedConfig): Plugin { const isBuild = config.command === 'build' @@ -69,13 +72,16 @@ export function definePlugin(config: ResolvedConfig): Plugin { define['import.meta.env.SSR'] = ssr + '' } if ('import.meta.env' in define) { - define['import.meta.env'] = serializeDefine({ - ...importMetaEnvKeys, - SSR: ssr + '', - ...userDefineEnv, - }) + define['import.meta.env'] = importMetaEnvMarker } + const importMetaEnvVal = serializeDefine({ + ...importMetaEnvKeys, + SSR: ssr + '', + ...userDefineEnv, + }) + const banner = `const ${importMetaEnvMarker} = ${importMetaEnvVal};\n` + // Create regex pattern as a fast check before running esbuild const patternKeys = Object.keys(userDefine) if (replaceProcessEnv && Object.keys(processEnv).length) { @@ -88,7 +94,7 @@ export function definePlugin(config: ResolvedConfig): Plugin { ? new RegExp(patternKeys.map(escapeRegex).join('|')) : null - return [define, pattern] as const + return [define, pattern, banner] as const } const defaultPattern = generatePattern(false) @@ -116,14 +122,32 @@ export function definePlugin(config: ResolvedConfig): Plugin { return } - const [define, pattern] = ssr ? ssrPattern : defaultPattern + const [define, pattern, banner] = ssr ? ssrPattern : defaultPattern if (!pattern) return // Check if our code needs any replacements before running esbuild pattern.lastIndex = 0 if (!pattern.test(code)) return - return await replaceDefine(code, id, define, config) + const result = await replaceDefine(code, id, define, config) + + // Replace `import.meta.env.*` with undefined + result.code = result.code.replaceAll(importMetaEnvKeyRe, (m) => + 'undefined'.padEnd(m.length), + ) + + // If there's bare `import.meta.env` references, prepend the banner + if (bareImportMetaEnvRe.test(result.code)) { + result.code = banner + result.code + + if (result.map) { + const map = JSON.parse(result.map) + map.mappings = ';' + map.mappings + result.map = map + } + } + + return result }, } } @@ -134,19 +158,6 @@ export async function replaceDefine( define: Record, config: ResolvedConfig, ): Promise<{ code: string; map: string | null }> { - // Because esbuild only allows JSON-serializable values, and `import.meta.env` - // may contain values with raw identifiers, making it non-JSON-serializable, - // we replace it with a temporary marker and then replace it back after to - // workaround it. This means that esbuild is unable to optimize the `import.meta.env` - // access, but that's a tradeoff for now. - const replacementMarkers: Record = {} - const env = define['import.meta.env'] - if (env && !canJsonParse(env)) { - const marker = `_${getHash(env, env.length - 2)}_` - replacementMarkers[marker] = env - define = { ...define, 'import.meta.env': marker } - } - const esbuildOptions = config.esbuild || {} const result = await transform(code, { @@ -178,10 +189,6 @@ export async function replaceDefine( } } - for (const marker in replacementMarkers) { - result.code = result.code.replaceAll(marker, replacementMarkers[marker]) - } - return { code: result.code, map: result.map || null, @@ -212,12 +219,3 @@ function handleDefineValue(value: any): string { if (typeof value === 'string') return value return JSON.stringify(value) } - -function canJsonParse(value: any): boolean { - try { - JSON.parse(value) - return true - } catch { - return false - } -} diff --git a/playground/define/__tests__/define.spec.ts b/playground/define/__tests__/define.spec.ts index ec1801c7876ca4..399e9c801266a9 100644 --- a/playground/define/__tests__/define.spec.ts +++ b/playground/define/__tests__/define.spec.ts @@ -92,3 +92,16 @@ test('replaces constants in template literal expressions', async () => { ), ).toBe('dev') }) + +test('replace constants on import.meta.env when it is a invalid json', async () => { + expect( + await page.textContent( + '.replace-undefined-constants-on-import-meta-env .import-meta-env-UNDEFINED', + ), + ).toBe('undefined') + expect( + await page.textContent( + '.replace-undefined-constants-on-import-meta-env .import-meta-env-SOME_IDENTIFIER', + ), + ).toBe('true') +}) diff --git a/playground/define/index.html b/playground/define/index.html index 2c2898771e5d4e..164ba46c64bd0e 100644 --- a/playground/define/index.html +++ b/playground/define/index.html @@ -60,6 +60,21 @@

Define replaces constants in template literal expressions

import.meta.hot

+

Define undefined constants on import.meta.env when it's a invalid json

+
+

+ import.meta.env.UNDEFINED +

+

+ import.meta.env.SOME_IDENTIFIER + +

+
+ +