Skip to content

Commit 54a15b8

Browse files
committed
feat: support import.meta.env in worker URL template literals
Allows template literals containing only import.meta.env.* expressions in Web Worker URL definitions, transforming them to string concatenation. Before (blocked with error): ```javascript const worker = new Worker( new URL(\`./path/\${import.meta.env.MY_VAR}/worker.ts\`, import.meta.url), { type: 'module' } ) ``` After (now supported): The template literal is automatically transformed to: ```javascript new URL('./path/' + import.meta.env.MY_VAR + '/worker.ts', import.meta.url) ``` Implementation: - Added transformSafeTemplateLiteral() to detect and transform safe templates - Only allows import.meta.env.* expressions (no other dynamic variables) - Transforms safe templates to string concatenation before Rollup processing - Provides helpful error messages for truly dynamic expressions - Added test files demonstrating the feature Technical details: - Two-pass transform: first transforms safe templates, then processes workers - Uses AST parsing to validate only safe expressions are present - Preserves source maps through MagicString transformations - No new dependencies required
1 parent a5e98e6 commit 54a15b8

File tree

4 files changed

+269
-5
lines changed

4 files changed

+269
-5
lines changed

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

Lines changed: 126 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,94 @@ async function getWorkerType(
184184
const workerImportMetaUrlRE =
185185
/new\s+(?:Worker|SharedWorker)\s*\(\s*new\s+URL.+?import\.meta\.url/s
186186

187+
/**
188+
* Checks if a template literal only contains safe expressions (import.meta.env.*)
189+
* and transforms it to string concatenation if safe.
190+
* Returns null if the template contains unsafe dynamic expressions.
191+
*/
192+
async function transformSafeTemplateLiteral(
193+
rawUrl: string,
194+
): Promise<string | null> {
195+
// Not a template literal
196+
if (rawUrl[0] !== '`' || !rawUrl.includes('${')) {
197+
return null
198+
}
199+
200+
try {
201+
// Parse the template literal as an expression
202+
const ast = await parseAstAsync(`(${rawUrl})`)
203+
const expression = (ast.body[0] as RollupAstNode<ExpressionStatement>)
204+
.expression
205+
206+
if (expression.type !== 'TemplateLiteral') {
207+
return null
208+
}
209+
210+
// Check if all expressions are safe (import.meta.env.*)
211+
for (const expr of expression.expressions) {
212+
if (!isSafeEnvExpression(expr)) {
213+
return null
214+
}
215+
}
216+
217+
// Transform to string concatenation
218+
const parts: string[] = []
219+
for (let i = 0; i < expression.quasis.length; i++) {
220+
const quasi = expression.quasis[i]
221+
const quasiValue = quasi.value.raw
222+
223+
if (quasiValue) {
224+
parts.push(JSON.stringify(quasiValue))
225+
}
226+
227+
if (i < expression.expressions.length) {
228+
const expr = expression.expressions[i]
229+
parts.push(generateEnvAccessCode(expr))
230+
}
231+
}
232+
233+
return parts.join(' + ')
234+
} catch {
235+
// If parsing fails, treat as unsafe
236+
return null
237+
}
238+
}
239+
240+
/**
241+
* Checks if an expression is a safe import.meta.env.* access
242+
*/
243+
function isSafeEnvExpression(expr: any): boolean {
244+
if (expr.type !== 'MemberExpression') {
245+
return false
246+
}
247+
248+
// Check if it's import.meta.env.*
249+
if (
250+
expr.object.type === 'MemberExpression' &&
251+
expr.object.object.type === 'MetaProperty' &&
252+
expr.object.object.meta.name === 'import' &&
253+
expr.object.object.property.name === 'meta' &&
254+
expr.object.property.type === 'Identifier' &&
255+
expr.object.property.name === 'env'
256+
) {
257+
return true
258+
}
259+
260+
return false
261+
}
262+
263+
/**
264+
* Generates code for accessing import.meta.env property
265+
*/
266+
function generateEnvAccessCode(expr: any): string {
267+
if (expr.property.type === 'Identifier') {
268+
return `import.meta.env.${expr.property.name}`
269+
} else if (expr.property.type === 'Literal') {
270+
return `import.meta.env[${JSON.stringify(expr.property.value)}]`
271+
}
272+
return 'import.meta.env'
273+
}
274+
187275
export function workerImportMetaUrlPlugin(config: ResolvedConfig): Plugin {
188276
const isBuild = config.command === 'build'
189277
let workerResolver: ResolveIdFn
@@ -209,6 +297,42 @@ export function workerImportMetaUrlPlugin(config: ResolvedConfig): Plugin {
209297
async handler(code, id) {
210298
let s: MagicString | undefined
211299
const cleanString = stripLiteral(code)
300+
301+
// First, check if there are template literals with expressions to transform
302+
const templateLiteralRE =
303+
/\bnew\s+(?:Worker|SharedWorker)\s*\(\s*new\s+URL\s*\(\s*(`[^`]+`)\s*,\s*import\.meta\.url\s*\)/dg
304+
305+
let templateMatch: RegExpExecArray | null
306+
let hasTransformedTemplates = false
307+
308+
while ((templateMatch = templateLiteralRE.exec(cleanString))) {
309+
const [[,], [urlStart, urlEnd]] = templateMatch.indices!
310+
const rawUrl = code.slice(urlStart, urlEnd)
311+
312+
if (rawUrl.includes('${')) {
313+
const transformed = await transformSafeTemplateLiteral(rawUrl)
314+
if (transformed) {
315+
s ||= new MagicString(code)
316+
s.update(urlStart, urlEnd, transformed)
317+
hasTransformedTemplates = true
318+
} else {
319+
// Unsafe dynamic template string
320+
this.error(
321+
`\`new URL(url, import.meta.url)\` is not supported in dynamic template string.\n` +
322+
`Only template literals with \`import.meta.env.*\` expressions are supported.\n` +
323+
`Use string concatenation instead: new URL('path/' + variable + '/file.ts', import.meta.url)`,
324+
urlStart,
325+
)
326+
}
327+
}
328+
}
329+
330+
// If we transformed templates, return and let this run again
331+
if (hasTransformedTemplates && s) {
332+
return transformStableResult(s, id, config)
333+
}
334+
335+
// Process worker URLs (regular strings and template literals without expressions)
212336
const workerImportMetaUrlRE =
213337
/\bnew\s+(?:Worker|SharedWorker)\s*\(\s*(new\s+URL\s*\(\s*('[^']+'|"[^"]+"|`[^`]+`)\s*,\s*import\.meta\.url\s*\))/dg
214338

@@ -219,12 +343,9 @@ export function workerImportMetaUrlPlugin(config: ResolvedConfig): Plugin {
219343

220344
const rawUrl = code.slice(urlStart, urlEnd)
221345

222-
// potential dynamic template string
346+
// Skip template literals with expressions (should not happen at this point)
223347
if (rawUrl[0] === '`' && rawUrl.includes('${')) {
224-
this.error(
225-
`\`new URL(url, import.meta.url)\` is not supported in dynamic template string.`,
226-
expStart,
227-
)
348+
continue
228349
}
229350

230351
s ||= new MagicString(code)

playground/worker/.env.example

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Example environment variables for worker template literal tests
2+
# Copy this file to .env and uncomment to test the template literal feature
3+
4+
# Directory where workers are located
5+
# Example: VITE_WORKER_DIR=worker
6+
# Results in path: ./worker/simple-worker.js
7+
VITE_WORKER_DIR=worker
8+
9+
# Worker file name for testing multiple env vars
10+
# Example: VITE_WORKER_FILE=simple-worker
11+
# Combined with VITE_WORKER_DIR results in: ./worker/simple-worker.js
12+
VITE_WORKER_FILE=simple-worker
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// Worker for testing template literal paths with import.meta.env
2+
self.onmessage = (e) => {
3+
console.log('env-path-worker received:', e.data)
4+
self.postMessage({ pong: 'from-env-path-worker' })
5+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<title>Template Literal Worker Test</title>
5+
</head>
6+
<body>
7+
<h1>Template Literal Worker Path Test</h1>
8+
<p>
9+
This test demonstrates template literals with import.meta.env in worker
10+
URLs.
11+
</p>
12+
<p>
13+
Set <code>VITE_WORKER_DIR=worker</code> in .env file to test (defaults to
14+
empty string if not set).
15+
</p>
16+
17+
<div>
18+
<p>
19+
Test 1 - Single env variable in template (transformed to string
20+
concatenation):
21+
</p>
22+
<pre><code>new URL(`./\${import.meta.env.VITE_WORKER_DIR}/simple-worker.js`, import.meta.url)</code></pre>
23+
<div id="test1-result">Waiting...</div>
24+
</div>
25+
26+
<div>
27+
<p>
28+
Test 2 - Multiple env variables (demonstrating complex template
29+
literal):
30+
</p>
31+
<pre><code>new URL(`./\${import.meta.env.VITE_WORKER_DIR}/\${import.meta.env.VITE_WORKER_FILE}.js`, import.meta.url)</code></pre>
32+
<div id="test2-result">Waiting...</div>
33+
</div>
34+
35+
<div>
36+
<p>Test 3 - Fallback to simple path when env not set:</p>
37+
<div id="test3-result">Waiting...</div>
38+
</div>
39+
40+
<script type="module">
41+
// Note: In a real project, set these in your .env file:
42+
// VITE_WORKER_DIR=worker
43+
// VITE_WORKER_FILE=simple-worker
44+
45+
// Test 1: Single env variable (transformed to string concatenation)
46+
// This template literal: `./${import.meta.env.VITE_WORKER_DIR}/simple-worker.js`
47+
// Will be transformed to: './' + import.meta.env.VITE_WORKER_DIR + '/simple-worker.js'
48+
try {
49+
const worker1 = new Worker(
50+
new URL(
51+
`./${import.meta.env.VITE_WORKER_DIR}/simple-worker.js`,
52+
import.meta.url,
53+
),
54+
{ type: 'module' },
55+
)
56+
57+
worker1.onmessage = (e) => {
58+
document.getElementById('test1-result').textContent =
59+
`✅ Success: ${JSON.stringify(e.data)}`
60+
}
61+
62+
worker1.onerror = (e) => {
63+
document.getElementById('test1-result').textContent =
64+
`❌ Error: ${e.message}`
65+
}
66+
67+
worker1.postMessage('ping from test1')
68+
} catch (e) {
69+
document.getElementById('test1-result').textContent =
70+
`❌ Exception: ${e.message}`
71+
}
72+
73+
// Test 2: Multiple env variables
74+
// This demonstrates the feature with multiple import.meta.env expressions
75+
// Template: `./${import.meta.env.VITE_WORKER_DIR}/${import.meta.env.VITE_WORKER_FILE}.js`
76+
// Will be transformed to: './' + import.meta.env.VITE_WORKER_DIR + '/' + import.meta.env.VITE_WORKER_FILE + '.js'
77+
try {
78+
const worker2 = new Worker(
79+
new URL(
80+
`./${import.meta.env.VITE_WORKER_DIR}/${import.meta.env.VITE_WORKER_FILE}.js`,
81+
import.meta.url,
82+
),
83+
{ type: 'module' },
84+
)
85+
86+
worker2.onmessage = (e) => {
87+
document.getElementById('test2-result').textContent =
88+
`✅ Success: ${JSON.stringify(e.data)}`
89+
}
90+
91+
worker2.onerror = (e) => {
92+
document.getElementById('test2-result').textContent =
93+
`❌ Error: ${e.message}`
94+
}
95+
96+
worker2.postMessage('ping from test2')
97+
} catch (e) {
98+
document.getElementById('test2-result').textContent =
99+
`❌ Exception: ${e.message}`
100+
}
101+
102+
// Test 3: Regular string (should still work as before)
103+
try {
104+
const worker3 = new Worker(
105+
new URL('./simple-worker.js', import.meta.url),
106+
{ type: 'module' },
107+
)
108+
109+
worker3.onmessage = (e) => {
110+
document.getElementById('test3-result').textContent =
111+
`✅ Success: ${JSON.stringify(e.data)}`
112+
}
113+
114+
worker3.onerror = (e) => {
115+
document.getElementById('test3-result').textContent =
116+
`❌ Error: ${e.message}`
117+
}
118+
119+
worker3.postMessage('ping from test3')
120+
} catch (e) {
121+
document.getElementById('test3-result').textContent =
122+
`❌ Exception: ${e.message}`
123+
}
124+
</script>
125+
</body>
126+
</html>

0 commit comments

Comments
 (0)