From 7217439c9284cac77dc03dfa78e5c984ed5f4336 Mon Sep 17 00:00:00 2001 From: bluwy Date: Fri, 2 Dec 2022 16:19:31 +0800 Subject: [PATCH 01/12] feat(define): handle replacement with esbuild Co-authored-by: Tony Trinh --- .../src/node/__tests__/plugins/define.spec.ts | 11 +- .../vite/src/node/plugins/clientInjections.ts | 11 +- packages/vite/src/node/plugins/define.ts | 100 +++++++----------- playground/define/__tests__/define.spec.ts | 60 +++++++++-- playground/define/commonjs-dep/index.js | 4 +- playground/define/index.html | 89 ++++++++++++++-- playground/env/__tests__/env.spec.ts | 26 +++++ playground/env/index.html | 22 ++++ playground/react/App.jsx | 2 + playground/react/__tests__/react.spec.ts | 4 + .../react/components/DefineVariables.jsx | 3 + playground/vue/DefineVariable.vue | 3 + playground/vue/Main.vue | 4 +- playground/vue/__tests__/vue.spec.ts | 6 ++ 14 files changed, 250 insertions(+), 95 deletions(-) create mode 100644 playground/react/components/DefineVariables.jsx create mode 100644 playground/vue/DefineVariable.vue diff --git a/packages/vite/src/node/__tests__/plugins/define.spec.ts b/packages/vite/src/node/__tests__/plugins/define.spec.ts index 932560a749f24d..18fa8787abb88a 100644 --- a/packages/vite/src/node/__tests__/plugins/define.spec.ts +++ b/packages/vite/src/node/__tests__/plugins/define.spec.ts @@ -10,6 +10,7 @@ async function createDefinePluginTransform( const config = await resolveConfig({ define }, build ? 'build' : 'serve') const instance = definePlugin(config) return async (code: string) => { + // @ts-expect-error const result = await instance.transform.call({}, code, 'foo.ts', { ssr }) return result?.code || result } @@ -20,21 +21,15 @@ describe('definePlugin', () => { const transform = await createDefinePluginTransform({ __APP_VERSION__: JSON.stringify('1.0') }) - expect(await transform('const version = __APP_VERSION__ ;')).toBe( - 'const version = "1.0" ;' - ) 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' ) }) }) diff --git a/packages/vite/src/node/plugins/clientInjections.ts b/packages/vite/src/node/plugins/clientInjections.ts index c4472cb8f14b62..79981c141d75d4 100644 --- a/packages/vite/src/node/plugins/clientInjections.ts +++ b/packages/vite/src/node/plugins/clientInjections.ts @@ -3,6 +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' +import { replaceDefine } from './define' // ids in transform are normalized to unix style const normalizedClientEntry = normalizePath(CLIENT_ENTRY) @@ -64,10 +65,14 @@ export function clientInjectionsPlugin(config: ResolvedConfig): Plugin { // 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( - /\bprocess\.env\.NODE_ENV\b/g, + 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 }, + config ) } } diff --git a/packages/vite/src/node/plugins/define.ts b/packages/vite/src/node/plugins/define.ts index 8743b6d8b4e7c5..30e56a416d83a3 100644 --- a/packages/vite/src/node/plugins/define.ts +++ b/packages/vite/src/node/plugins/define.ts @@ -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 = {} const processNodeEnv: Record = {} + const processEnv: Record = {} 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 = {} @@ -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), 'import.meta.hot': `false` }) } - function generatePattern( - ssr: boolean - ): [Record, RegExp | null] { + function generatePattern(ssr: boolean) { const replaceProcessEnv = !ssr || config.ssr?.target === 'webworker' const replacements: Record = { @@ -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 - '(? { - 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, + 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 + } +} diff --git a/playground/define/__tests__/define.spec.ts b/playground/define/__tests__/define.spec.ts index 8c27686bce1703..43e260715b5192 100644 --- a/playground/define/__tests__/define.spec.ts +++ b/playground/define/__tests__/define.spec.ts @@ -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' - ) +}) + +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') }) diff --git a/playground/define/commonjs-dep/index.js b/playground/define/commonjs-dep/index.js index 9be5641e04b844..de1a3e360119a3 100644 --- a/playground/define/commonjs-dep/index.js +++ b/playground/define/commonjs-dep/index.js @@ -1,5 +1,3 @@ module.exports = { - defined: __STRINGIFIED_OBJ__, - importMetaEnvUndefined: 'import.meta.env.UNDEFINED', - processEnvUndefined: 'process.env.UNDEFINED' + defined: __STRINGIFIED_OBJ__ } diff --git a/playground/define/index.html b/playground/define/index.html index 06b2f4f9479d38..6491b0d6d56990 100644 --- a/playground/define/index.html +++ b/playground/define/index.html @@ -1,3 +1,5 @@ + +

Define

Raw Expression

@@ -17,10 +19,52 @@

Define

define variable in html: __EXP__

import json:

define in dep:

-

- import.meta.env.UNDEFINED: -

-

process.env.UNDEFINED:

+ +

Define ignores string literals

+
+

process.env.

+

global.process.env.

+

+ globalThis.process.env. +

+

process.env.NODE_ENV

+

+ global.process.env.NODE_ENV + +

+

+ globalThis.process.env.NODE_ENV + +

+

+ __vite_process_env_NODE_ENV + +

+

import.meta.hot

+
+ +

Define replaces constants in template literal expressions

+
+

process.env.

+

global.process.env.

+

+ globalThis.process.env. +

+

process.env.NODE_ENV

+

+ global.process.env.NODE_ENV + +

+

+ globalThis.process.env.NODE_ENV + +

+

+ __vite_process_env_NODE_ENV + +

+

import.meta.hot

+