-
-
Notifications
You must be signed in to change notification settings - Fork 6.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(define): handle replacement with esbuild #11151
Changes from 2 commits
7217439
8d00673
fd92514
3724ce5
d86bc55
dc1e95c
f86b4cc
648c71b
0d2daff
2554a47
db48339
bb5ea8d
9d1772d
ddd513a
7ea6899
ea8a0f8
5c507af
56d6260
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,6 @@ | ||
import MagicString from 'magic-string' | ||
import { transform } from 'esbuild' | ||
import type { ResolvedConfig } from '../config' | ||
import type { Plugin } from '../plugin' | ||
import { transformStableResult } from '../utils' | ||
import { isCSSRequest } from './css' | ||
import { isHTMLRequest } from './html' | ||
|
||
|
@@ -13,21 +12,21 @@ export function definePlugin(config: ResolvedConfig): Plugin { | |
const isBuildLib = isBuild && config.build.lib | ||
|
||
// ignore replace process.env in lib build | ||
const processEnv: Record<string, string> = {} | ||
const processNodeEnv: Record<string, string> = {} | ||
const processEnv: Record<string, string> = {} | ||
if (!isBuildLib) { | ||
const nodeEnv = process.env.NODE_ENV || config.mode | ||
Object.assign(processEnv, { | ||
'process.env.': `({}).`, | ||
'global.process.env.': `({}).`, | ||
'globalThis.process.env.': `({}).` | ||
}) | ||
Object.assign(processNodeEnv, { | ||
'process.env.NODE_ENV': JSON.stringify(nodeEnv), | ||
'global.process.env.NODE_ENV': JSON.stringify(nodeEnv), | ||
'globalThis.process.env.NODE_ENV': JSON.stringify(nodeEnv), | ||
__vite_process_env_NODE_ENV: JSON.stringify(nodeEnv) | ||
}) | ||
Object.assign(processEnv, { | ||
'process.env': `{}`, | ||
'global.process.env': `{}`, | ||
'globalThis.process.env': `{}` | ||
}) | ||
} | ||
|
||
const userDefine: Record<string, string> = {} | ||
|
@@ -49,15 +48,12 @@ export function definePlugin(config: ResolvedConfig): Plugin { | |
importMetaKeys[`import.meta.env.${key}`] = JSON.stringify(env[key]) | ||
} | ||
Object.assign(importMetaFallbackKeys, { | ||
'import.meta.env.': `({}).`, | ||
'import.meta.env': JSON.stringify(config.env), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This change is fine as esbuild will share a single reference to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Update: not exactly fine, as in the bundle size will technically increase, but multiple usages of |
||
'import.meta.hot': `false` | ||
}) | ||
} | ||
|
||
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> = { | ||
|
@@ -72,38 +68,20 @@ export function definePlugin(config: ResolvedConfig): Plugin { | |
replacements['__vite_process_env_NODE_ENV'] = 'process.env.NODE_ENV' | ||
} | ||
|
||
const replacementsKeys = Object.keys(replacements) | ||
const pattern = replacementsKeys.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((str) => { | ||
return str.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&') | ||
}) | ||
.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*?=[^=]))', | ||
'gu' | ||
) | ||
: null | ||
|
||
return [replacements, pattern] | ||
return Object.keys(replacements).length ? replacements : null | ||
} | ||
|
||
const defaultPattern = generatePattern(false) | ||
const ssrPattern = generatePattern(true) | ||
const defaultReplacements = generatePattern(false) | ||
const ssrReplacements = generatePattern(true) | ||
|
||
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 plugin. | ||
return | ||
} | ||
|
||
|
@@ -117,36 +95,30 @@ export function definePlugin(config: ResolvedConfig): Plugin { | |
return | ||
} | ||
|
||
const [replacements, pattern] = ssr ? ssrPattern : defaultPattern | ||
|
||
if (!pattern) { | ||
return null | ||
} | ||
|
||
if (ssr && !isBuild) { | ||
// ssr + dev, simple replace | ||
return code.replace(pattern, (_, match) => { | ||
return '' + replacements[match] | ||
}) | ||
} | ||
const replacements = ssr ? ssrReplacements : defaultReplacements | ||
if (!replacements) return | ||
|
||
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, replacements, config) | ||
} | ||
} | ||
} | ||
|
||
export async function replaceDefine( | ||
code: string, | ||
id: string, | ||
replacements: Record<string, string>, | ||
config: ResolvedConfig | ||
): Promise<{ code: string; map: string | null }> { | ||
const result = await transform(code, { | ||
loader: 'js', | ||
charset: 'utf8', | ||
platform: 'neutral', | ||
define: replacements, | ||
sourcefile: id, | ||
sourcemap: config.command === 'build' && !!config.build.sourcemap | ||
}) | ||
return { | ||
code: result.code, | ||
map: result.map || null | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,10 @@ | ||
import { expect, test } from 'vitest' | ||
import viteConfig from '../vite.config' | ||
import { isBuild, page } from '~utils' | ||
import { page } from '~utils' | ||
|
||
test('string', async () => { | ||
const defines = viteConfig.define | ||
const defines = viteConfig.define | ||
|
||
test('string', async () => { | ||
expect(await page.textContent('.exp')).toBe( | ||
String(typeof eval(defines.__EXP__)) | ||
) | ||
|
@@ -44,10 +44,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' | ||
) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test isn't relevant for esbuild transform anymore. |
||
}) | ||
|
||
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') | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,3 @@ | ||
module.exports = { | ||
defined: __STRINGIFIED_OBJ__, | ||
importMetaEnvUndefined: 'import.meta.env.UNDEFINED', | ||
processEnvUndefined: 'process.env.UNDEFINED' | ||
defined: __STRINGIFIED_OBJ__ | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Esbuild transform tend to semi-format the entire code so this is changed here. That also means we can't do stable sourcemaps transform.