Skip to content

Commit 659b720

Browse files
yuzheng14bluwy
andauthored
fix(build): handle invalid JSON in import.meta.env (#17648)
Co-authored-by: bluwy <[email protected]>
1 parent 952bae3 commit 659b720

File tree

5 files changed

+108
-35
lines changed

5 files changed

+108
-35
lines changed

packages/vite/src/node/__tests__/plugins/define.spec.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,20 @@ describe('definePlugin', () => {
6464
)
6565
})
6666

67+
test('replace import.meta.env.UNKNOWN with undefined', async () => {
68+
const transform = await createDefinePluginTransform()
69+
expect(await transform('const foo = import.meta.env.UNKNOWN;')).toBe(
70+
'const foo = undefined ;\n',
71+
)
72+
})
73+
74+
test('leave import.meta.env["UNKNOWN"] to runtime', async () => {
75+
const transform = await createDefinePluginTransform()
76+
expect(await transform('const foo = import.meta.env["UNKNOWN"];')).toMatch(
77+
/const __vite_import_meta_env__ = .*;\nconst foo = __vite_import_meta_env__\["UNKNOWN"\];/,
78+
)
79+
})
80+
6781
test('preserve import.meta.env.UNKNOWN with override', async () => {
6882
const transform = await createDefinePluginTransform({
6983
'import.meta.env.UNKNOWN': 'import.meta.env.UNKNOWN',
@@ -72,4 +86,27 @@ describe('definePlugin', () => {
7286
'const foo = import.meta.env.UNKNOWN;\n',
7387
)
7488
})
89+
90+
test('replace import.meta.env when it is a invalid json', async () => {
91+
const transform = await createDefinePluginTransform({
92+
'import.meta.env.LEGACY': '__VITE_IS_LEGACY__',
93+
})
94+
95+
expect(
96+
await transform(
97+
'const isLegacy = import.meta.env.LEGACY;\nimport.meta.env.UNDEFINED && console.log(import.meta.env.UNDEFINED);',
98+
),
99+
).toMatchInlineSnapshot(`
100+
"const isLegacy = __VITE_IS_LEGACY__;
101+
undefined && console.log(undefined );
102+
"
103+
`)
104+
})
105+
106+
test('replace bare import.meta.env', async () => {
107+
const transform = await createDefinePluginTransform()
108+
expect(await transform('const env = import.meta.env;')).toMatch(
109+
/const __vite_import_meta_env__ = .*;\nconst env = __vite_import_meta_env__;/,
110+
)
111+
})
75112
})

packages/vite/src/node/plugins/define.ts

Lines changed: 33 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@ import { transform } from 'esbuild'
22
import { TraceMap, decodedMap, encodedMap } from '@jridgewell/trace-mapping'
33
import type { ResolvedConfig } from '../config'
44
import type { Plugin } from '../plugin'
5-
import { escapeRegex, getHash } from '../utils'
5+
import { escapeRegex } from '../utils'
66
import { isCSSRequest } from './css'
77
import { isHTMLRequest } from './html'
88

99
const nonJsRe = /\.json(?:$|\?)/
1010
const isNonJsRequest = (request: string): boolean => nonJsRe.test(request)
11+
const importMetaEnvMarker = '__vite_import_meta_env__'
12+
const bareImportMetaEnvRe = new RegExp(`${importMetaEnvMarker}(?!\\.)\\b`)
13+
const importMetaEnvKeyRe = new RegExp(`${importMetaEnvMarker}\\..+?\\b`, 'g')
1114

1215
export function definePlugin(config: ResolvedConfig): Plugin {
1316
const isBuild = config.command === 'build'
@@ -69,13 +72,16 @@ export function definePlugin(config: ResolvedConfig): Plugin {
6972
define['import.meta.env.SSR'] = ssr + ''
7073
}
7174
if ('import.meta.env' in define) {
72-
define['import.meta.env'] = serializeDefine({
73-
...importMetaEnvKeys,
74-
SSR: ssr + '',
75-
...userDefineEnv,
76-
})
75+
define['import.meta.env'] = importMetaEnvMarker
7776
}
7877

78+
const importMetaEnvVal = serializeDefine({
79+
...importMetaEnvKeys,
80+
SSR: ssr + '',
81+
...userDefineEnv,
82+
})
83+
const banner = `const ${importMetaEnvMarker} = ${importMetaEnvVal};\n`
84+
7985
// Create regex pattern as a fast check before running esbuild
8086
const patternKeys = Object.keys(userDefine)
8187
if (replaceProcessEnv && Object.keys(processEnv).length) {
@@ -88,7 +94,7 @@ export function definePlugin(config: ResolvedConfig): Plugin {
8894
? new RegExp(patternKeys.map(escapeRegex).join('|'))
8995
: null
9096

91-
return [define, pattern] as const
97+
return [define, pattern, banner] as const
9298
}
9399

94100
const defaultPattern = generatePattern(false)
@@ -116,14 +122,32 @@ export function definePlugin(config: ResolvedConfig): Plugin {
116122
return
117123
}
118124

119-
const [define, pattern] = ssr ? ssrPattern : defaultPattern
125+
const [define, pattern, banner] = ssr ? ssrPattern : defaultPattern
120126
if (!pattern) return
121127

122128
// Check if our code needs any replacements before running esbuild
123129
pattern.lastIndex = 0
124130
if (!pattern.test(code)) return
125131

126-
return await replaceDefine(code, id, define, config)
132+
const result = await replaceDefine(code, id, define, config)
133+
134+
// Replace `import.meta.env.*` with undefined
135+
result.code = result.code.replaceAll(importMetaEnvKeyRe, (m) =>
136+
'undefined'.padEnd(m.length),
137+
)
138+
139+
// If there's bare `import.meta.env` references, prepend the banner
140+
if (bareImportMetaEnvRe.test(result.code)) {
141+
result.code = banner + result.code
142+
143+
if (result.map) {
144+
const map = JSON.parse(result.map)
145+
map.mappings = ';' + map.mappings
146+
result.map = map
147+
}
148+
}
149+
150+
return result
127151
},
128152
}
129153
}
@@ -134,19 +158,6 @@ export async function replaceDefine(
134158
define: Record<string, string>,
135159
config: ResolvedConfig,
136160
): Promise<{ code: string; map: string | null }> {
137-
// Because esbuild only allows JSON-serializable values, and `import.meta.env`
138-
// may contain values with raw identifiers, making it non-JSON-serializable,
139-
// we replace it with a temporary marker and then replace it back after to
140-
// workaround it. This means that esbuild is unable to optimize the `import.meta.env`
141-
// access, but that's a tradeoff for now.
142-
const replacementMarkers: Record<string, string> = {}
143-
const env = define['import.meta.env']
144-
if (env && !canJsonParse(env)) {
145-
const marker = `_${getHash(env, env.length - 2)}_`
146-
replacementMarkers[marker] = env
147-
define = { ...define, 'import.meta.env': marker }
148-
}
149-
150161
const esbuildOptions = config.esbuild || {}
151162

152163
const result = await transform(code, {
@@ -178,10 +189,6 @@ export async function replaceDefine(
178189
}
179190
}
180191

181-
for (const marker in replacementMarkers) {
182-
result.code = result.code.replaceAll(marker, replacementMarkers[marker])
183-
}
184-
185192
return {
186193
code: result.code,
187194
map: result.map || null,
@@ -212,12 +219,3 @@ function handleDefineValue(value: any): string {
212219
if (typeof value === 'string') return value
213220
return JSON.stringify(value)
214221
}
215-
216-
function canJsonParse(value: any): boolean {
217-
try {
218-
JSON.parse(value)
219-
return true
220-
} catch {
221-
return false
222-
}
223-
}

playground/define/__tests__/define.spec.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,16 @@ test('replaces constants in template literal expressions', async () => {
9292
),
9393
).toBe('dev')
9494
})
95+
96+
test('replace constants on import.meta.env when it is a invalid json', async () => {
97+
expect(
98+
await page.textContent(
99+
'.replace-undefined-constants-on-import-meta-env .import-meta-env-UNDEFINED',
100+
),
101+
).toBe('undefined')
102+
expect(
103+
await page.textContent(
104+
'.replace-undefined-constants-on-import-meta-env .import-meta-env-SOME_IDENTIFIER',
105+
),
106+
).toBe('true')
107+
})

playground/define/index.html

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,21 @@ <h2>Define replaces constants in template literal expressions</h2>
6060
<p>import.meta.hot <code class="import-meta-hot"></code></p>
6161
</section>
6262

63+
<h2>Define undefined constants on import.meta.env when it's a invalid json</h2>
64+
<section class="replace-undefined-constants-on-import-meta-env">
65+
<p>
66+
import.meta.env.UNDEFINED <code class="import-meta-env-UNDEFINED"></code>
67+
</p>
68+
<p>
69+
import.meta.env.SOME_IDENTIFIER
70+
<code class="import-meta-env-SOME_IDENTIFIER"></code>
71+
</p>
72+
</section>
73+
74+
<script>
75+
// inject __VITE_SOME_IDENTIFIER__ to test if it's replaced
76+
window.__VITE_SOME_IDENTIFIER__ = true
77+
</script>
6378
<script type="module">
6479
const __VAR_NAME__ = true // ensure define doesn't replace var name
6580
text('.exp', typeof __EXP__) // typeof __EXP__ would be `boolean` instead of `string`
@@ -127,6 +142,15 @@ <h2>Define replaces constants in template literal expressions</h2>
127142
'.replaces-constants-in-template-literal-expressions .process-env-NODE_ENV',
128143
`${process.env.NODE_ENV}`,
129144
)
145+
146+
text(
147+
'.replace-undefined-constants-on-import-meta-env .import-meta-env-UNDEFINED',
148+
`${import.meta.env.UNDEFINED}`,
149+
)
150+
text(
151+
'.replace-undefined-constants-on-import-meta-env .import-meta-env-SOME_IDENTIFIER',
152+
`${import.meta.env.SOME_IDENTIFIER}`,
153+
)
130154
</script>
131155

132156
<style>

playground/define/vite.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,6 @@ export default defineConfig({
2929
ÖUNICODE_LETTERɵ: 789,
3030
__VAR_NAME__: false,
3131
__STRINGIFIED_OBJ__: JSON.stringify({ foo: true }),
32+
'import.meta.env.SOME_IDENTIFIER': '__VITE_SOME_IDENTIFIER__',
3233
},
3334
})

0 commit comments

Comments
 (0)