mirror of
https://github.com/vitejs/vite.git
synced 2024-11-21 14:48:41 +00:00
feat(define): handle replacement with esbuild (#11151)
Co-authored-by: Tony Trinh <tony19@gmail.com>
This commit is contained in:
parent
0ae2e1dc63
commit
e4c801c552
@ -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[]>)[]`
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
@ -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 + `}`
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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))
|
||||
|
@ -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')
|
||||
})
|
||||
|
@ -1,5 +1,3 @@
|
||||
module.exports = {
|
||||
defined: __STRINGIFIED_OBJ__,
|
||||
importMetaEnvUndefined: 'import.meta.env.UNDEFINED',
|
||||
processEnvUndefined: 'process.env.UNDEFINED',
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -6,6 +6,7 @@ export default defineConfig({
|
||||
__STRING__: '"hello"',
|
||||
__NUMBER__: 123,
|
||||
__BOOLEAN__: true,
|
||||
__UNDEFINED__: undefined,
|
||||
__OBJ__: {
|
||||
foo: 1,
|
||||
bar: {
|
||||
|
26
playground/env/__tests__/env.spec.ts
vendored
26
playground/env/__tests__/env.spec.ts
vendored
@ -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',
|
||||
)
|
||||
})
|
||||
|
22
playground/env/index.html
vendored
22
playground/env/index.html
vendored
@ -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)
|
||||
|
||||
|
@ -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 + '')
|
||||
|
@ -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>
|
||||
|
@ -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',
|
||||
},
|
||||
|
||||
|
3
playground/react/components/DefineVariables.jsx
Normal file
3
playground/react/components/DefineVariables.jsx
Normal file
@ -0,0 +1,3 @@
|
||||
export default function DefineVariable() {
|
||||
return <div class="define-variable">import.meta.env</div>
|
||||
}
|
3
playground/vue/DefineVariable.vue
Normal file
3
playground/vue/DefineVariable.vue
Normal file
@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<div class="define-variable">import.meta.env</div>
|
||||
</template>
|
Loading…
Reference in New Issue
Block a user