Skip to content

Commit

Permalink
fix(build): handle invalid JSON in import.meta.env (#17648)
Browse files Browse the repository at this point in the history
Co-authored-by: bluwy <bjornlu.dev@gmail.com>
  • Loading branch information
yuzheng14 and bluwy committed Jul 30, 2024
1 parent 952bae3 commit 659b720
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 35 deletions.
37 changes: 37 additions & 0 deletions packages/vite/src/node/__tests__/plugins/define.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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__;/,
)
})
})
68 changes: 33 additions & 35 deletions packages/vite/src/node/plugins/define.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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) {
Expand All @@ -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)
Expand Down Expand Up @@ -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
},
}
}
Expand All @@ -134,19 +158,6 @@ export async function replaceDefine(
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, {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
}
}
13 changes: 13 additions & 0 deletions playground/define/__tests__/define.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
})
24 changes: 24 additions & 0 deletions playground/define/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,21 @@ <h2>Define replaces constants in template literal expressions</h2>
<p>import.meta.hot <code class="import-meta-hot"></code></p>
</section>

<h2>Define undefined constants on import.meta.env when it's a invalid json</h2>
<section class="replace-undefined-constants-on-import-meta-env">
<p>
import.meta.env.UNDEFINED <code class="import-meta-env-UNDEFINED"></code>
</p>
<p>
import.meta.env.SOME_IDENTIFIER
<code class="import-meta-env-SOME_IDENTIFIER"></code>
</p>
</section>

<script>
// inject __VITE_SOME_IDENTIFIER__ to test if it's replaced
window.__VITE_SOME_IDENTIFIER__ = true
</script>
<script type="module">
const __VAR_NAME__ = true // ensure define doesn't replace var name
text('.exp', typeof __EXP__) // typeof __EXP__ would be `boolean` instead of `string`
Expand Down Expand Up @@ -127,6 +142,15 @@ <h2>Define replaces constants in template literal expressions</h2>
'.replaces-constants-in-template-literal-expressions .process-env-NODE_ENV',
`${process.env.NODE_ENV}`,
)

text(
'.replace-undefined-constants-on-import-meta-env .import-meta-env-UNDEFINED',
`${import.meta.env.UNDEFINED}`,
)
text(
'.replace-undefined-constants-on-import-meta-env .import-meta-env-SOME_IDENTIFIER',
`${import.meta.env.SOME_IDENTIFIER}`,
)
</script>

<style>
Expand Down
1 change: 1 addition & 0 deletions playground/define/vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,6 @@ export default defineConfig({
ÖUNICODE_LETTERɵ: 789,
__VAR_NAME__: false,
__STRINGIFIED_OBJ__: JSON.stringify({ foo: true }),
'import.meta.env.SOME_IDENTIFIER': '__VITE_SOME_IDENTIFIER__',
},
})

0 comments on commit 659b720

Please sign in to comment.