Skip to content

Commit

Permalink
feat(define): handle replacement with esbuild (#11151)
Browse files Browse the repository at this point in the history
Co-authored-by: Tony Trinh <[email protected]>
  • Loading branch information
bluwy and tony19 authored Oct 26, 2023
1 parent 0ae2e1d commit e4c801c
Show file tree
Hide file tree
Showing 18 changed files with 420 additions and 181 deletions.
33 changes: 10 additions & 23 deletions docs/config/shared-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

- 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.
**Example:**

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.
Expand All @@ -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[]>)[]`
Expand Down
36 changes: 36 additions & 0 deletions docs/guide/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 11 additions & 12 deletions packages/vite/src/node/__tests__/plugins/define.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand All @@ -23,35 +25,32 @@ 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',
)
})

test('preserve import.meta.hot with override', async () => {
// 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,
)
})
})
40 changes: 21 additions & 19 deletions packages/vite/src/node/plugins/clientInjections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
)
}
},
Expand All @@ -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 + `}`
}
Loading

0 comments on commit e4c801c

Please sign in to comment.