1
- import MagicString from 'magic-string' ;
2
1
import { fileURLToPath } from 'node:url' ;
3
2
import type * as vite from 'vite' ;
4
3
import { loadEnv } from 'vite' ;
4
+ import { transform } from 'esbuild' ;
5
+ import MagicString from 'magic-string' ;
5
6
import type { AstroConfig , AstroSettings } from '../@types/astro.js' ;
6
7
7
8
interface EnvPluginOptions {
8
9
settings : AstroSettings ;
9
10
}
10
11
12
+ // Match `import.meta.env` directly without trailing property access
13
+ const importMetaEnvOnlyRe = / \b i m p o r t \. m e t a \. e n v \b (? ! \. ) / ;
14
+ // Match valid JS variable names (identifiers), which accepts most alphanumeric characters,
15
+ // except that the first character cannot be a number.
16
+ const isValidIdentifierRe = / ^ [ _ $ a - z A - Z ] [ _ $ a - z A - Z 0 - 9 ] * $ / ;
17
+
11
18
function getPrivateEnv (
12
19
viteConfig : vite . ResolvedConfig ,
13
20
astroConfig : AstroConfig
@@ -29,7 +36,7 @@ function getPrivateEnv(
29
36
const privateEnv : Record < string , string > = { } ;
30
37
for ( const key in fullEnv ) {
31
38
// Ignore public env var
32
- if ( envPrefixes . every ( ( prefix ) => ! key . startsWith ( prefix ) ) ) {
39
+ if ( isValidIdentifierRe . test ( key ) && envPrefixes . every ( ( prefix ) => ! key . startsWith ( prefix ) ) ) {
33
40
if ( typeof process . env [ key ] !== 'undefined' ) {
34
41
let value = process . env [ key ] ;
35
42
// Replacements are always strings, so try to convert to strings here first
@@ -61,71 +68,136 @@ function getReferencedPrivateKeys(source: string, privateEnv: Record<string, any
61
68
return references ;
62
69
}
63
70
64
- export default function envVitePlugin ( { settings } : EnvPluginOptions ) : vite . PluginOption {
71
+ /**
72
+ * Use esbuild to perform replacememts like Vite
73
+ * https://github.com/vitejs/vite/blob/5ea9edbc9ceb991e85f893fe62d68ed028677451/packages/vite/src/node/plugins/define.ts#L130
74
+ */
75
+ async function replaceDefine (
76
+ code : string ,
77
+ id : string ,
78
+ define : Record < string , string > ,
79
+ config : vite . ResolvedConfig
80
+ ) : Promise < { code : string ; map : string | null } > {
81
+ // Since esbuild doesn't support replacing complex expressions, we replace `import.meta.env`
82
+ // with a marker string first, then postprocess and apply the `Object.assign` code.
83
+ const replacementMarkers : Record < string , string > = { } ;
84
+ const env = define [ 'import.meta.env' ] ;
85
+ if ( env ) {
86
+ // Compute the marker from the length of the replaced code. We do this so that esbuild generates
87
+ // the sourcemap with the right column offset when we do the postprocessing.
88
+ const marker = `__astro_import_meta_env${ '_' . repeat (
89
+ env . length - 23 /* length of preceding string */
90
+ ) } `;
91
+ replacementMarkers [ marker ] = env ;
92
+ define = { ...define , 'import.meta.env' : marker } ;
93
+ }
94
+
95
+ const esbuildOptions = config . esbuild || { } ;
96
+
97
+ const result = await transform ( code , {
98
+ loader : 'js' ,
99
+ charset : esbuildOptions . charset ?? 'utf8' ,
100
+ platform : 'neutral' ,
101
+ define,
102
+ sourcefile : id ,
103
+ sourcemap : config . command === 'build' ? ! ! config . build . sourcemap : true ,
104
+ } ) ;
105
+
106
+ for ( const marker in replacementMarkers ) {
107
+ result . code = result . code . replaceAll ( marker , replacementMarkers [ marker ] ) ;
108
+ }
109
+
110
+ return {
111
+ code : result . code ,
112
+ map : result . map || null ,
113
+ } ;
114
+ }
115
+
116
+ export default function envVitePlugin ( { settings } : EnvPluginOptions ) : vite . Plugin {
65
117
let privateEnv : Record < string , string > ;
118
+ let defaultDefines : Record < string , string > ;
119
+ let isDev : boolean ;
120
+ let devImportMetaEnvPrepend : string ;
66
121
let viteConfig : vite . ResolvedConfig ;
67
122
const { config : astroConfig } = settings ;
68
123
return {
69
124
name : 'astro:vite-plugin-env' ,
70
- enforce : 'pre' ,
125
+ config ( _ , { command } ) {
126
+ isDev = command !== 'build' ;
127
+ } ,
71
128
configResolved ( resolvedConfig ) {
72
129
viteConfig = resolvedConfig ;
130
+
131
+ // HACK: move ourselves before Vite's define plugin to apply replacements at the right time (before Vite normal plugins)
132
+ const viteDefinePluginIndex = resolvedConfig . plugins . findIndex (
133
+ ( p ) => p . name === 'vite:define'
134
+ ) ;
135
+ if ( viteDefinePluginIndex !== - 1 ) {
136
+ const myPluginIndex = resolvedConfig . plugins . findIndex (
137
+ ( p ) => p . name === 'astro:vite-plugin-env'
138
+ ) ;
139
+ if ( myPluginIndex !== - 1 ) {
140
+ const myPlugin = resolvedConfig . plugins [ myPluginIndex ] ;
141
+ // @ts -ignore-error ignore readonly annotation
142
+ resolvedConfig . plugins . splice ( viteDefinePluginIndex , 0 , myPlugin ) ;
143
+ // @ts -ignore-error ignore readonly annotation
144
+ resolvedConfig . plugins . splice ( myPluginIndex , 1 ) ;
145
+ }
146
+ }
73
147
} ,
74
- async transform ( source , id , options ) {
148
+ transform ( source , id , options ) {
75
149
if ( ! options ?. ssr || ! source . includes ( 'import.meta.env' ) ) {
76
150
return ;
77
151
}
78
152
79
153
// Find matches for *private* env and do our own replacement.
80
- let s : MagicString | undefined ;
81
- const pattern = new RegExp (
82
- // Do not allow preceding '.', but do allow preceding '...' for spread operations
83
- '(?<!(?<!\\.\\.)\\.)\\b(' +
84
- // Captures `import.meta.env.*` calls and replace with `privateEnv`
85
- `import\\.meta\\.env\\.(.+?)` +
86
- '|' +
87
- // This catches destructed `import.meta.env` calls,
88
- // BUT we only want to inject private keys referenced in the file.
89
- // We overwrite this value on a per-file basis.
90
- 'import\\.meta\\.env' +
91
- // prevent trailing assignments
92
- ')\\b(?!\\s*?=[^=])' ,
93
- 'g'
94
- ) ;
95
- let references : Set < string > ;
96
- let match : RegExpExecArray | null ;
97
-
98
- while ( ( match = pattern . exec ( source ) ) ) {
99
- let replacement : string | undefined ;
100
- // If we match exactly `import.meta.env`, define _only_ referenced private variables
101
- if ( match [ 0 ] === 'import.meta.env' ) {
102
- privateEnv ??= getPrivateEnv ( viteConfig , astroConfig ) ;
103
- references ??= getReferencedPrivateKeys ( source , privateEnv ) ;
104
- replacement = `(Object.assign(import.meta.env,{` ;
105
- for ( const key of references . values ( ) ) {
106
- replacement += `${ key } :${ privateEnv [ key ] } ,` ;
154
+ privateEnv ??= getPrivateEnv ( viteConfig , astroConfig ) ;
155
+
156
+ // In dev, we can assign the private env vars to `import.meta.env` directly for performance
157
+ if ( isDev ) {
158
+ const s = new MagicString ( source ) ;
159
+ if ( ! devImportMetaEnvPrepend ) {
160
+ devImportMetaEnvPrepend = `Object.assign(import.meta.env,{` ;
161
+ for ( const key in privateEnv ) {
162
+ devImportMetaEnvPrepend += `${ key } :${ privateEnv [ key ] } ,` ;
107
163
}
108
- replacement += '}))' ;
109
- }
110
- // If we match `import.meta.env.*`, replace with private env
111
- else if ( match [ 2 ] ) {
112
- privateEnv ??= getPrivateEnv ( viteConfig , astroConfig ) ;
113
- replacement = privateEnv [ match [ 2 ] ] ;
164
+ devImportMetaEnvPrepend += '});' ;
114
165
}
115
- if ( replacement ) {
116
- const start = match . index ;
117
- const end = start + match [ 0 ] . length ;
118
- s ??= new MagicString ( source ) ;
119
- s . overwrite ( start , end , replacement ) ;
120
- }
121
- }
122
-
123
- if ( s ) {
166
+ s . prepend ( devImportMetaEnvPrepend ) ;
124
167
return {
125
168
code : s . toString ( ) ,
126
169
map : s . generateMap ( { hires : 'boundary' } ) ,
127
170
} ;
128
171
}
172
+
173
+ // In build, use esbuild to perform replacements. Compute the default defines for esbuild here as a
174
+ // separate object as it could be extended by `import.meta.env` later.
175
+ if ( ! defaultDefines ) {
176
+ defaultDefines = { } ;
177
+ for ( const key in privateEnv ) {
178
+ defaultDefines [ `import.meta.env.${ key } ` ] = privateEnv [ key ] ;
179
+ }
180
+ }
181
+
182
+ let defines = defaultDefines ;
183
+
184
+ // If reference the `import.meta.env` object directly, we want to inject private env vars
185
+ // into Vite's injected `import.meta.env` object. To do this, we use `Object.assign` and keeping
186
+ // the `import.meta.env` identifier so Vite sees it.
187
+ if ( importMetaEnvOnlyRe . test ( source ) ) {
188
+ const references = getReferencedPrivateKeys ( source , privateEnv ) ;
189
+ let replacement = `(Object.assign(import.meta.env,{` ;
190
+ for ( const key of references . values ( ) ) {
191
+ replacement += `${ key } :${ privateEnv [ key ] } ,` ;
192
+ }
193
+ replacement += '}))' ;
194
+ defines = {
195
+ ...defaultDefines ,
196
+ 'import.meta.env' : replacement ,
197
+ } ;
198
+ }
199
+
200
+ return replaceDefine ( source , id , defines , viteConfig ) ;
129
201
} ,
130
202
} ;
131
203
}
0 commit comments