feat(define): handle replacement with esbuild (#11151)

Co-authored-by: Tony Trinh <tony19@gmail.com>
This commit is contained in:
Bjorn Lu 2023-10-26 11:30:05 +08:00 committed by GitHub
parent 0ae2e1dc63
commit e4c801c552
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 425 additions and 186 deletions

View File

@ -37,17 +37,18 @@ See [Env Variables and Modes](/guide/env-and-mode) for more details.
Define global constant replacements. Entries will be defined as globals during dev and statically replaced during build.
- String values will be used as raw expressions, so if defining a string constant, **it needs to be explicitly quoted** (e.g. with `JSON.stringify`).
Vite uses [esbuild defines](https://esbuild.github.io/api/#define) to perform replacements, so value expressions must be a string that contains a JSON-serializable value (null, boolean, number, string, array, or object) or a single identifier. For non-string values, Vite will automatically convert it to a string with `JSON.stringify`.
- To be consistent with [esbuild behavior](https://esbuild.github.io/api/#define), expressions must either be a JSON object (null, boolean, number, string, array, or object) or a single identifier.
**Example:**
- Replacements are performed only when the match isn't surrounded by other letters, numbers, `_` or `$`.
::: warning
Because it's implemented as straightforward text replacements without any syntax analysis, we recommend using `define` for CONSTANTS only.
For example, `process.env.FOO` and `__APP_VERSION__` are good fits. But `process` or `global` should not be put into this option. Variables can be shimmed or polyfilled instead.
:::
```js
export default defineConfig({
define: {
__APP_VERSION__: JSON.stringify('v1.0.0'),
__API_URL__: 'window.__backend_api_url',
},
})
```
::: tip NOTE
For TypeScript users, make sure to add the type declarations in the `env.d.ts` or `vite-env.d.ts` file to get type checks and Intellisense.
@ -61,20 +62,6 @@ declare const __APP_VERSION__: string
:::
::: tip NOTE
Since dev and build implement `define` differently, we should avoid some use cases to avoid inconsistency.
Example:
```js
const obj = {
__NAME__, // Don't define object shorthand property names
__KEY__: value, // Don't define object key
}
```
:::
## plugins
- **Type:** `(Plugin | Plugin[] | Promise<Plugin | Plugin[]>)[]`

View File

@ -32,6 +32,42 @@ For other projects, there are a few general approaches:
See the [troubleshooting guide](/guide/troubleshooting.html#vite-cjs-node-api-deprecated) for more information.
## Rework `define` and `import.meta.env.*` replacement strategy
In Vite 4, the `define` and `import.meta.env.*` features use different replacement strategies in dev and build:
- In dev, both features are injected as global variables to `globalThis` and `import.meta` respectively.
- In build, both features are statically replaced with a regex.
This results in a dev and build inconsistency when trying to access the variables, and sometimes even caused failed builds. For example:
```js
// vite.config.js
export default defineConfig({
define: {
__APP_VERSION__: JSON.stringify('1.0.0'),
},
})
```
```js
const data = { __APP_VERSION__ }
// dev: { __APP_VERSION__: "1.0.0" } ✅
// build: { "1.0.0" } ❌
const docs = 'I like import.meta.env.MODE'
// dev: "I like import.meta.env.MODE" ✅
// build: "I like "production"" ❌
```
Vite 5 fixes this by using `esbuild` to handle the replacements in builds, aligning with the dev behaviour.
This change should not affect most setups, as it's already documented that `define` values should follow esbuild's syntax:
> To be consistent with esbuild behavior, expressions must either be a JSON object (null, boolean, number, string, array, or object) or a single identifier.
However, if you prefer to keep statically replacing values directly, you can use [`@rollup/plugin-replace`](https://github.com/rollup/plugins/tree/master/packages/replace).
## General Changes
### SSR externalized modules value now matches production

View File

@ -7,12 +7,14 @@ async function createDefinePluginTransform(
build = true,
ssr = false,
) {
const config = await resolveConfig({ define }, build ? 'build' : 'serve')
const config = await resolveConfig(
{ configFile: false, define },
build ? 'build' : 'serve',
)
const instance = definePlugin(config)
return async (code: string) => {
const result = await (instance.transform as any).call({}, code, 'foo.ts', {
ssr,
})
// @ts-expect-error transform should exist
const result = await instance.transform.call({}, code, 'foo.ts', { ssr })
return result?.code || result
}
}
@ -23,20 +25,17 @@ describe('definePlugin', () => {
__APP_VERSION__: JSON.stringify('1.0'),
})
expect(await transform('const version = __APP_VERSION__ ;')).toBe(
'const version = "1.0" ;',
'const version = "1.0";\n',
)
expect(await transform('const version = __APP_VERSION__;')).toBe(
'const version = "1.0";',
'const version = "1.0";\n',
)
})
test('replaces import.meta.env.SSR with false', async () => {
const transform = await createDefinePluginTransform()
expect(await transform('const isSSR = import.meta.env.SSR ;')).toBe(
'const isSSR = false ;',
)
expect(await transform('const isSSR = import.meta.env.SSR;')).toBe(
'const isSSR = false;',
'const isSSR = false;\n',
)
})
@ -44,14 +43,14 @@ describe('definePlugin', () => {
// assert that the default behavior is to replace import.meta.hot with undefined
const transform = await createDefinePluginTransform()
expect(await transform('const hot = import.meta.hot;')).toBe(
'const hot = undefined;',
'const hot = void 0;\n',
)
// assert that we can specify a user define to preserve import.meta.hot
const overrideTransform = await createDefinePluginTransform({
'import.meta.hot': 'import.meta.hot',
})
expect(await overrideTransform('const hot = import.meta.hot;')).toBe(
'const hot = import.meta.hot;',
undefined,
)
})
})

View File

@ -3,9 +3,7 @@ import type { Plugin } from '../plugin'
import type { ResolvedConfig } from '../config'
import { CLIENT_ENTRY, ENV_ENTRY } from '../constants'
import { isObject, normalizePath, resolveHostname } from '../utils'
const process_env_NODE_ENV_RE =
/(\bglobal(This)?\.)?\bprocess\.env\.NODE_ENV\b/g
import { replaceDefine, serializeDefine } from './define'
// ids in transform are normalized to unix style
const normalizedClientEntry = normalizePath(CLIENT_ENTRY)
@ -53,7 +51,14 @@ export function clientInjectionsPlugin(config: ResolvedConfig): Plugin {
hmrBase = path.posix.join(hmrBase, hmrConfig.path)
}
const serializedDefines = serializeDefine(config.define || {})
const userDefine: Record<string, any> = {}
for (const key in config.define) {
// import.meta.env.* is handled in `importAnalysis` plugin
if (!key.startsWith('import.meta.env.')) {
userDefine[key] = config.define[key]
}
}
const serializedDefines = serializeDefine(userDefine)
const modeReplacement = escapeReplacement(config.mode)
const baseReplacement = escapeReplacement(devBase)
@ -84,17 +89,25 @@ export function clientInjectionsPlugin(config: ResolvedConfig): Plugin {
.replace(`__HMR_CONFIG_NAME__`, hmrConfigNameReplacement)
}
},
transform(code, id, options) {
async transform(code, id, options) {
if (id === normalizedClientEntry || id === normalizedEnvEntry) {
return injectConfigValues(code)
} else if (!options?.ssr && code.includes('process.env.NODE_ENV')) {
// replace process.env.NODE_ENV instead of defining a global
// for it to avoid shimming a `process` object during dev,
// avoiding inconsistencies between dev and build
return code.replace(
process_env_NODE_ENV_RE,
const nodeEnv =
config.define?.['process.env.NODE_ENV'] ||
JSON.stringify(process.env.NODE_ENV || config.mode),
JSON.stringify(process.env.NODE_ENV || config.mode)
return await replaceDefine(
code,
id,
{
'process.env.NODE_ENV': nodeEnv,
'global.process.env.NODE_ENV': nodeEnv,
'globalThis.process.env.NODE_ENV': nodeEnv,
},
config,
)
}
},
@ -105,14 +118,3 @@ function escapeReplacement(value: string | number | boolean | null) {
const jsonValue = JSON.stringify(value)
return () => jsonValue
}
function serializeDefine(define: Record<string, any>): string {
let res = `{`
for (const key in define) {
const val = define[key]
res += `${JSON.stringify(key)}: ${
typeof val === 'string' ? `(${val})` : JSON.stringify(val)
}, `
}
return res + `}`
}

View File

@ -1,12 +1,11 @@
import MagicString from 'magic-string'
import { transform } from 'esbuild'
import type { ResolvedConfig } from '../config'
import type { Plugin } from '../plugin'
import { escapeRegex, transformStableResult } from '../utils'
import { escapeRegex, getHash } from '../utils'
import { isCSSRequest } from './css'
import { isHTMLRequest } from './html'
const nonJsRe = /\.json(?:$|\?)/
const metaEnvRe = /import\.meta\.env\.(.+)/
const isNonJsRequest = (request: string): boolean => nonJsRe.test(request)
export function definePlugin(config: ResolvedConfig): Plugin {
@ -19,9 +18,9 @@ export function definePlugin(config: ResolvedConfig): Plugin {
if (!isBuildLib) {
const nodeEnv = process.env.NODE_ENV || config.mode
Object.assign(processEnv, {
'process.env.': `({}).`,
'global.process.env.': `({}).`,
'globalThis.process.env.': `({}).`,
'process.env': `{}`,
'global.process.env': `{}`,
'globalThis.process.env': `{}`,
})
Object.assign(processNodeEnv, {
'process.env.NODE_ENV': JSON.stringify(nodeEnv),
@ -31,86 +30,76 @@ export function definePlugin(config: ResolvedConfig): Plugin {
})
}
const userDefine: Record<string, string> = {}
const userDefineEnv: Record<string, string> = {}
for (const key in config.define) {
const val = config.define[key]
userDefine[key] = typeof val === 'string' ? val : JSON.stringify(val)
// make sure `import.meta.env` object has user define properties
if (isBuild) {
const match = key.match(metaEnvRe)
if (match) {
userDefineEnv[match[1]] = `__vite__define__${key}__define__vite__`
}
}
}
// during dev, import.meta properties are handled by importAnalysis plugin.
const importMetaKeys: Record<string, string> = {}
const importMetaEnvKeys: Record<string, string> = {}
const importMetaFallbackKeys: Record<string, string> = {}
if (isBuild) {
// set here to allow override with config.define
importMetaKeys['import.meta.hot'] = `undefined`
for (const key in config.env) {
importMetaKeys[`import.meta.env.${key}`] = JSON.stringify(config.env[key])
const val = JSON.stringify(config.env[key])
importMetaKeys[`import.meta.env.${key}`] = val
importMetaEnvKeys[key] = val
}
Object.assign(importMetaFallbackKeys, {
'import.meta.env.': `({}).`,
'import.meta.env': JSON.stringify({
...config.env,
SSR: '__vite__ssr__',
...userDefineEnv,
}).replace(
/"__vite__define__(.+?)__define__vite__"/g,
(_, key) => userDefine[key],
),
})
// these will be set to a proper value in `generatePattern`
importMetaKeys['import.meta.env.SSR'] = `undefined`
importMetaFallbackKeys['import.meta.env'] = `undefined`
}
function getImportMetaKeys(ssr: boolean): Record<string, string> {
if (!isBuild) return {}
return {
...importMetaKeys,
'import.meta.env.SSR': ssr + '',
const userDefine: Record<string, string> = {}
const userDefineEnv: Record<string, any> = {}
for (const key in config.define) {
// user can define keys with the same values to declare that some keys
// should not be replaced. in this case, we delete references of the key
// so they aren't replaced in the first place.
const val = config.define[key]
if (key === val) {
delete processNodeEnv[key]
delete importMetaKeys[key]
continue
}
userDefine[key] = handleDefineValue(config.define[key])
// make sure `import.meta.env` object has user define properties
if (isBuild && key.startsWith('import.meta.env.')) {
userDefineEnv[key.slice(16)] = config.define[key]
}
}
function getImportMetaFallbackKeys(ssr: boolean): Record<string, string> {
if (!isBuild) return {}
return {
...importMetaFallbackKeys,
'import.meta.env': importMetaFallbackKeys['import.meta.env'].replace(
'"__vite__ssr__"',
ssr + '',
),
}
}
function generatePattern(
ssr: boolean,
): [Record<string, string | undefined>, RegExp | null] {
function generatePattern(ssr: boolean) {
const replaceProcessEnv = !ssr || config.ssr?.target === 'webworker'
const replacements: Record<string, string> = {
const define: Record<string, string> = {
...(replaceProcessEnv ? processNodeEnv : {}),
...getImportMetaKeys(ssr),
...importMetaKeys,
...userDefine,
...getImportMetaFallbackKeys(ssr),
...importMetaFallbackKeys,
...(replaceProcessEnv ? processEnv : {}),
}
// Additional define fixes based on `ssr` value
if (isBuild && !replaceProcessEnv) {
replacements['__vite_process_env_NODE_ENV'] = 'process.env.NODE_ENV'
define['__vite_process_env_NODE_ENV'] = 'process.env.NODE_ENV'
}
if ('import.meta.env.SSR' in define) {
define['import.meta.env.SSR'] = ssr + ''
}
if ('import.meta.env' in define) {
define['import.meta.env'] = serializeDefine({
...importMetaEnvKeys,
SSR: ssr + '',
...userDefineEnv,
})
}
const replacementsKeys = Object.keys(replacements)
const pattern = replacementsKeys.length
const defineKeys = Object.keys(define)
const pattern = defineKeys.length
? new RegExp(
// Mustn't be preceded by a char that can be part of an identifier
// or a '.' that isn't part of a spread operator
'(?<![\\p{L}\\p{N}_$]|(?<!\\.\\.)\\.)(' +
replacementsKeys.map(escapeRegex).join('|') +
defineKeys.map(escapeRegex).join('|') +
// Mustn't be followed by a char that can be part of an identifier
// or an assignment (but allow equality operators)
')(?:(?<=\\.)|(?![\\p{L}\\p{N}_$]|\\s*?=[^=]))',
@ -118,7 +107,7 @@ export function definePlugin(config: ResolvedConfig): Plugin {
)
: null
return [replacements, pattern]
return [define, pattern] as const
}
const defaultPattern = generatePattern(false)
@ -127,11 +116,12 @@ export function definePlugin(config: ResolvedConfig): Plugin {
return {
name: 'vite:define',
transform(code, id, options) {
async transform(code, id, options) {
const ssr = options?.ssr === true
if (!ssr && !isBuild) {
// for dev we inject actual global defines in the vite client to
// avoid the transform cost.
// avoid the transform cost. see the `clientInjection` and
// `importAnalysis` plugin.
return
}
@ -145,36 +135,88 @@ export function definePlugin(config: ResolvedConfig): Plugin {
return
}
const [replacements, pattern] = ssr ? ssrPattern : defaultPattern
const [define, pattern] = ssr ? ssrPattern : defaultPattern
if (!pattern) return
if (!pattern) {
return null
}
// Check if our code needs any replacements before running esbuild
pattern.lastIndex = 0
if (!pattern.test(code)) return
if (ssr && !isBuild) {
// ssr + dev, simple replace
return code.replace(pattern, (_, match) => {
return '' + replacements[match]
})
}
const s = new MagicString(code)
let hasReplaced = false
let match: RegExpExecArray | null
while ((match = pattern.exec(code))) {
hasReplaced = true
const start = match.index
const end = start + match[0].length
const replacement = '' + replacements[match[1]]
s.update(start, end, replacement)
}
if (!hasReplaced) {
return null
}
return transformStableResult(s, id, config)
return await replaceDefine(code, id, define, config)
},
}
}
export async function replaceDefine(
code: string,
id: string,
define: Record<string, string>,
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<string, string> = {}
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, {
loader: 'js',
charset: esbuildOptions.charset ?? 'utf8',
platform: 'neutral',
define,
sourcefile: id,
sourcemap: config.command === 'build' ? !!config.build.sourcemap : true,
})
for (const marker in replacementMarkers) {
result.code = result.code.replaceAll(marker, replacementMarkers[marker])
}
return {
code: result.code,
map: result.map || null,
}
}
/**
* Like `JSON.stringify` but keeps raw string values as a literal
* in the generated code. For example: `"window"` would refer to
* the global `window` object directly.
*/
export function serializeDefine(define: Record<string, any>): string {
let res = `{`
const keys = Object.keys(define)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
const val = define[key]
res += `${JSON.stringify(key)}: ${handleDefineValue(val)}`
if (i !== keys.length - 1) {
res += `, `
}
}
return res + `}`
}
function handleDefineValue(value: any): string {
if (typeof value === 'undefined') return 'undefined'
if (typeof value === 'string') return value
return JSON.stringify(value)
}
function canJsonParse(value: any): boolean {
try {
JSON.parse(value)
return true
} catch {
return false
}
}

View File

@ -61,6 +61,7 @@ import {
} from './optimizedDeps'
import { isCSSRequest, isDirectCSSRequest } from './css'
import { browserExternalId } from './resolve'
import { serializeDefine } from './define'
const debug = createDebugger('vite:import-analysis')
@ -177,23 +178,29 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
let server: ViteDevServer
let _env: string | undefined
let _ssrEnv: string | undefined
function getEnv(ssr: boolean) {
if (!_env) {
_env = `import.meta.env = ${JSON.stringify({
...config.env,
SSR: '__vite__ssr__',
})};`
// account for user env defines
if (!_ssrEnv || !_env) {
const importMetaEnvKeys: Record<string, any> = {}
const userDefineEnv: Record<string, any> = {}
for (const key in config.env) {
importMetaEnvKeys[key] = JSON.stringify(config.env[key])
}
for (const key in config.define) {
if (key.startsWith(`import.meta.env.`)) {
const val = config.define[key]
_env += `${key} = ${
typeof val === 'string' ? val : JSON.stringify(val)
};`
// non-import.meta.env.* is handled in `clientInjection` plugin
if (key.startsWith('import.meta.env.')) {
userDefineEnv[key.slice(16)] = config.define[key]
}
}
const env = `import.meta.env = ${serializeDefine({
...importMetaEnvKeys,
SSR: '__vite_ssr__',
...userDefineEnv,
})};`
_ssrEnv = env.replace('__vite_ssr__', 'true')
_env = env.replace('__vite_ssr__', 'false')
}
return _env.replace('"__vite__ssr__"', ssr + '')
return ssr ? _ssrEnv : _env
}
return {

View File

@ -1002,8 +1002,10 @@ export function parseRequest(id: string): Record<string, string> | null {
export const blankReplacer = (match: string): string => ' '.repeat(match.length)
export function getHash(text: Buffer | string): string {
return createHash('sha256').update(text).digest('hex').substring(0, 8)
export function getHash(text: Buffer | string, length = 8): string {
const h = createHash('sha256').update(text).digest('hex').substring(0, length)
if (length <= 64) return h
return h.padEnd(length, '_')
}
const _dirname = path.dirname(fileURLToPath(import.meta.url))

View File

@ -1,16 +1,17 @@
import { expect, test } from 'vitest'
import viteConfig from '../vite.config'
import { isBuild, page } from '~utils'
import { page } from '~utils'
const defines = viteConfig.define
test('string', async () => {
const defines = viteConfig.define
expect(await page.textContent('.exp')).toBe(
String(typeof eval(defines.__EXP__)),
)
expect(await page.textContent('.string')).toBe(JSON.parse(defines.__STRING__))
expect(await page.textContent('.number')).toBe(String(defines.__NUMBER__))
expect(await page.textContent('.boolean')).toBe(String(defines.__BOOLEAN__))
expect(await page.textContent('.undefined')).toBe('')
expect(await page.textContent('.object')).toBe(
JSON.stringify(defines.__OBJ__, null, 2),
@ -44,10 +45,52 @@ test('string', async () => {
expect(await page.textContent('.define-in-dep')).toBe(
defines.__STRINGIFIED_OBJ__,
)
expect(await page.textContent('.import-meta-env-undefined')).toBe(
isBuild ? '({}).UNDEFINED' : 'import.meta.env.UNDEFINED',
)
expect(await page.textContent('.process-env-undefined')).toBe(
isBuild ? '({}).UNDEFINED' : 'process.env.UNDEFINED',
)
})
test('ignores constants in string literals', async () => {
expect(
await page.textContent('.ignores-string-literals .process-env-dot'),
).toBe('process.env.')
expect(
await page.textContent('.ignores-string-literals .global-process-env-dot'),
).toBe('global.process.env.')
expect(
await page.textContent(
'.ignores-string-literals .globalThis-process-env-dot',
),
).toBe('globalThis.process.env.')
expect(
await page.textContent('.ignores-string-literals .process-env-NODE_ENV'),
).toBe('process.env.NODE_ENV')
expect(
await page.textContent(
'.ignores-string-literals .global-process-env-NODE_ENV',
),
).toBe('global.process.env.NODE_ENV')
expect(
await page.textContent(
'.ignores-string-literals .globalThis-process-env-NODE_ENV',
),
).toBe('globalThis.process.env.NODE_ENV')
expect(
await page.textContent(
'.ignores-string-literals .__vite_process_env_NODE_ENV',
),
).toBe('__vite_process_env_NODE_ENV')
expect(
await page.textContent('.ignores-string-literals .import-meta-hot'),
).toBe('import' + '.meta.hot')
})
test('replaces constants in template literal expressions', async () => {
expect(
await page.textContent(
'.replaces-constants-in-template-literal-expressions .process-env-dot',
),
).toBe(JSON.parse(defines['process.env.SOMEVAR']))
expect(
await page.textContent(
'.replaces-constants-in-template-literal-expressions .process-env-NODE_ENV',
),
).toBe('dev')
})

View File

@ -1,5 +1,3 @@
module.exports = {
defined: __STRINGIFIED_OBJ__,
importMetaEnvUndefined: 'import.meta.env.UNDEFINED',
processEnvUndefined: 'process.env.UNDEFINED',
}

View File

@ -1,9 +1,12 @@
<meta charset="utf-8" />
<h1>Define</h1>
<p>Raw Expression <code class="exp"></code></p>
<p>String <code class="string"></code></p>
<p>Number <code class="number"></code></p>
<p>Boolean <code class="boolean"></code></p>
<p>Undefined <code class="undefined"></code></p>
<p>Object <span class="pre object"></span></p>
<p>Env Var <code class="env-var"></code></p>
<p>process node env: <code class="process-node-env"></code></p>
@ -17,10 +20,52 @@
<p>define variable in html: <code class="exp-define">__EXP__</code></p>
<p>import json: <code class="import-json"></code></p>
<p>define in dep: <code class="define-in-dep"></code></p>
<p>
import.meta.env.UNDEFINED: <code class="import-meta-env-undefined"></code>
</p>
<p>process.env.UNDEFINED: <code class="process-env-undefined"></code></p>
<h2>Define ignores string literals</h2>
<section class="ignores-string-literals">
<p>process.env. <code class="process-env-dot"></code></p>
<p>global.process.env. <code class="global-process-env-dot"></code></p>
<p>
globalThis.process.env. <code class="globalThis-process-env-dot"></code>
</p>
<p>process.env.NODE_ENV <code class="process-env-NODE_ENV"></code></p>
<p>
global.process.env.NODE_ENV
<code class="global-process-env-NODE_ENV"></code>
</p>
<p>
globalThis.process.env.NODE_ENV
<code class="globalThis-process-env-NODE_ENV"></code>
</p>
<p>
__vite_process_env_NODE_ENV
<code class="__vite_process_env_NODE_ENV"></code>
</p>
<p>import.meta.hot <code class="import-meta-hot"></code></p>
</section>
<h2>Define replaces constants in template literal expressions</h2>
<section class="replaces-constants-in-template-literal-expressions">
<p>process.env. <code class="process-env-dot"></code></p>
<p>global.process.env. <code class="global-process-env-dot"></code></p>
<p>
globalThis.process.env. <code class="globalThis-process-env-dot"></code>
</p>
<p>process.env.NODE_ENV <code class="process-env-NODE_ENV"></code></p>
<p>
global.process.env.NODE_ENV
<code class="global-process-env-NODE_ENV"></code>
</p>
<p>
globalThis.process.env.NODE_ENV
<code class="globalThis-process-env-NODE_ENV"></code>
</p>
<p>
__vite_process_env_NODE_ENV
<code class="__vite_process_env_NODE_ENV"></code>
</p>
<p>import.meta.hot <code class="import-meta-hot"></code></p>
</section>
<script type="module">
const __VAR_NAME__ = true // ensure define doesn't replace var name
@ -28,6 +73,7 @@
text('.string', __STRING__)
text('.number', __NUMBER__)
text('.boolean', __BOOLEAN__)
text('.undefined', __UNDEFINED__)
text('.object', JSON.stringify(__OBJ__, null, 2))
text('.process-node-env', process.env.NODE_ENV)
text('.env-var', process.env.SOMEVAR)
@ -56,14 +102,41 @@
document.querySelector(el).textContent = text
}
import {
defined,
importMetaEnvUndefined,
processEnvUndefined,
} from '@vitejs/test-commonjs-dep'
import { defined } from '@vitejs/test-commonjs-dep'
text('.define-in-dep', JSON.stringify(defined))
text('.import-meta-env-undefined', importMetaEnvUndefined)
text('.process-env-undefined', processEnvUndefined)
text('.ignores-string-literals .process-env-dot', 'process.env.')
text(
'.ignores-string-literals .global-process-env-dot',
'global.process.env.',
)
text(
'.ignores-string-literals .globalThis-process-env-dot',
'globalThis.process.env.',
)
text('.ignores-string-literals .process-env-NODE_ENV', 'process.env.NODE_ENV')
text(
'.ignores-string-literals .global-process-env-NODE_ENV',
'global.process.env.NODE_ENV',
)
text(
'.ignores-string-literals .globalThis-process-env-NODE_ENV',
'globalThis.process.env.NODE_ENV',
)
text(
'.ignores-string-literals .__vite_process_env_NODE_ENV',
'__vite_process_env_NODE_ENV',
)
text('.ignores-string-literals .import-meta-hot', 'import.meta.hot')
text(
'.replaces-constants-in-template-literal-expressions .process-env-dot',
`${process.env.SOMEVAR}`,
)
text(
'.replaces-constants-in-template-literal-expressions .process-env-NODE_ENV',
`${process.env.NODE_ENV}`,
)
</script>
<style>

View File

@ -6,6 +6,7 @@ export default defineConfig({
__STRING__: '"hello"',
__NUMBER__: 123,
__BOOLEAN__: true,
__UNDEFINED__: undefined,
__OBJ__: {
foo: 1,
bar: {

View File

@ -23,6 +23,10 @@ test('custom', async () => {
expect(await page.textContent('.custom')).toBe('1')
})
test('custom in template literal expression', async () => {
expect(await page.textContent('.custom-template-literal-exp')).toBe('1')
})
test('custom-prefix', async () => {
expect(await page.textContent('.custom-prefix')).toBe('1')
})
@ -91,8 +95,30 @@ test('env object', async () => {
})
})
test('env object in template literal expression', async () => {
const envText = await page.textContent('.env-object-in-template-literal-exp')
expect(JSON.parse(envText)).toMatchObject({
VITE_EFFECTIVE_MODE_FILE_NAME: `.env.${mode}`,
CUSTOM_PREFIX_ENV_VARIABLE: '1',
VITE_CUSTOM_ENV_VARIABLE: '1',
BASE_URL: '/env/',
MODE: mode,
DEV: !isBuild,
PROD: isBuild,
})
})
if (!isBuild) {
test('relative url import script return import.meta.url', async () => {
expect(await page.textContent('.url')).toMatch('/env/index.js')
})
}
test('ignores import' + '.meta.env in string literals', async () => {
expect(await page.textContent('.ignores-literal-import-meta-env-dot')).toBe(
'import' + '.meta.env.',
)
expect(await page.textContent('.ignores-literal-import-meta-env')).toBe(
'import' + '.meta.env',
)
})

View File

@ -4,6 +4,10 @@
<p>import.meta.env.DEV: <code class="dev"></code></p>
<p>import.meta.env.PROD: <code class="prod"></code></p>
<p>import.meta.env.VITE_CUSTOM_ENV_VARIABLE: <code class="custom"></code></p>
<p>
${import.meta.env.VITE_CUSTOM_ENV_VARIABLE}:
<code class="custom-template-literal-exp"></code>
</p>
<p>
import.meta.env.CUSTOM_PREFIX_ENV_VARIABLE:
<code class="custom-prefix"></code>
@ -28,7 +32,15 @@
<p>import.meta.env.VITE_EXPAND_B: <code class="expand-b"></code></p>
<p>import.meta.env.SSR: <code class="ssr"></code></p>
<p>import.meta.env: <span class="pre env-object"></span></p>
<p>
${import.meta.env}:
<span class="pre env-object-in-template-literal-exp"></span>
</p>
<p>import.meta.url: <span class="pre url"></span></p>
<p>
import.meta.env. <code class="ignores-literal-import-meta-env-dot"></code>
</p>
<p>import.meta.env <code class="ignores-literal-import-meta-env"></code></p>
<script type="module">
text('.base', import.meta.env.BASE_URL)
@ -36,6 +48,10 @@
text('.dev', import.meta.env.DEV)
text('.prod', import.meta.env.PROD)
text('.custom', import.meta.env.VITE_CUSTOM_ENV_VARIABLE)
text(
'.custom-template-literal-exp',
`${import.meta.env.VITE_CUSTOM_ENV_VARIABLE}`,
)
text('.custom-prefix', import.meta.env.CUSTOM_PREFIX_ENV_VARIABLE)
text('.mode-file', import.meta.env.VITE_EFFECTIVE_MODE_FILE_NAME)
text('.inline', import.meta.env.VITE_INLINE)
@ -48,6 +64,12 @@
text('.global-node-env', global.process.env.NODE_ENV)
text('.global-this-node-env', globalThis.process.env.NODE_ENV)
text('.env-object', JSON.stringify(import.meta.env, null, 2))
text(
'.env-object-in-template-literal-exp',
`${JSON.stringify(import.meta.env, null, 2)}`,
)
text('.ignores-literal-import-meta-env-dot', 'import' + '.meta.env.')
text('.ignores-literal-import-meta-env', 'import' + '.meta.env')
text('.expand-a', import.meta.env.VITE_EXPAND_A)
text('.expand-b', import.meta.env.VITE_EXPAND_B)

View File

@ -300,9 +300,6 @@ describe('env', () => {
expect(await page.textContent('.env-define-object-string')).toBe(
'{ "foo": "bar" }',
)
expect(await page.textContent('.env-define-template-literal')).toBe(
'`template literal`', // only double quotes will be unquoted
)
expect(await page.textContent('.env-define-null-string')).toBe('null')
expect(await page.textContent('.env-bar')).toBeTruthy()
expect(await page.textContent('.env-prod')).toBe(isBuild + '')

View File

@ -2,7 +2,6 @@
<p class="env-define">%VITE_NUMBER%</p>
<p class="env-define-string">%VITE_STRING%</p>
<p class="env-define-object-string">%VITE_OBJECT_STRING%</p>
<p class="env-define-template-literal">%VITE_TEMPLATE_LITERAL%</p>
<p class="env-define-null-string">%VITE_NULL_STRING%</p>
<p class="env-%VITE_FOO%">class name should be env-bar</p>
<p class="env-prod">%PROD%</p>

View File

@ -44,7 +44,6 @@ export default defineConfig({
'import.meta.env.VITE_NUMBER': 5173,
'import.meta.env.VITE_STRING': JSON.stringify('string'),
'import.meta.env.VITE_OBJECT_STRING': '{ "foo": "bar" }',
'import.meta.env.VITE_TEMPLATE_LITERAL': '`template literal`',
'import.meta.env.VITE_NULL_STRING': 'null',
},

View File

@ -0,0 +1,3 @@
export default function DefineVariable() {
return <div class="define-variable">import.meta.env</div>
}

View File

@ -0,0 +1,3 @@
<template>
<div class="define-variable">import.meta.env</div>
</template>