Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 126 additions & 5 deletions packages/vite/src/node/plugins/workerImportMetaUrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,94 @@ async function getWorkerType(
const workerImportMetaUrlRE =
/new\s+(?:Worker|SharedWorker)\s*\(\s*new\s+URL.+?import\.meta\.url/s

/**
* Checks if a template literal only contains safe expressions (import.meta.env.*)
* and transforms it to string concatenation if safe.
* Returns null if the template contains unsafe dynamic expressions.
*/
async function transformSafeTemplateLiteral(
rawUrl: string,
): Promise<string | null> {
// Not a template literal
if (rawUrl[0] !== '`' || !rawUrl.includes('${')) {
return null
}

try {
// Parse the template literal as an expression
const ast = await parseAstAsync(`(${rawUrl})`)
const expression = (ast.body[0] as RollupAstNode<ExpressionStatement>)
.expression

if (expression.type !== 'TemplateLiteral') {
return null
}

// Check if all expressions are safe (import.meta.env.*)
for (const expr of expression.expressions) {
if (!isSafeEnvExpression(expr)) {
return null
}
}

// Transform to string concatenation
const parts: string[] = []
for (let i = 0; i < expression.quasis.length; i++) {
const quasi = expression.quasis[i]
const quasiValue = quasi.value.raw

if (quasiValue) {
parts.push(JSON.stringify(quasiValue))
}

if (i < expression.expressions.length) {
const expr = expression.expressions[i]
parts.push(generateEnvAccessCode(expr))
}
}

return parts.join(' + ')
} catch {
// If parsing fails, treat as unsafe
return null
}
}

/**
* Checks if an expression is a safe import.meta.env.* access
*/
function isSafeEnvExpression(expr: any): boolean {
if (expr.type !== 'MemberExpression') {
return false
}

// Check if it's import.meta.env.*
if (
expr.object.type === 'MemberExpression' &&
expr.object.object.type === 'MetaProperty' &&
expr.object.object.meta.name === 'import' &&
expr.object.object.property.name === 'meta' &&
expr.object.property.type === 'Identifier' &&
expr.object.property.name === 'env'
) {
return true
}

return false
}

/**
* Generates code for accessing import.meta.env property
*/
function generateEnvAccessCode(expr: any): string {
if (expr.property.type === 'Identifier') {
return `import.meta.env.${expr.property.name}`
} else if (expr.property.type === 'Literal') {
return `import.meta.env[${JSON.stringify(expr.property.value)}]`
}
return 'import.meta.env'
}

export function workerImportMetaUrlPlugin(config: ResolvedConfig): Plugin {
const isBuild = config.command === 'build'
let workerResolver: ResolveIdFn
Expand All @@ -209,6 +297,42 @@ export function workerImportMetaUrlPlugin(config: ResolvedConfig): Plugin {
async handler(code, id) {
let s: MagicString | undefined
const cleanString = stripLiteral(code)

// First, check if there are template literals with expressions to transform
const templateLiteralRE =
/\bnew\s+(?:Worker|SharedWorker)\s*\(\s*new\s+URL\s*\(\s*(`[^`]+`)\s*,\s*import\.meta\.url\s*\)/dg

let templateMatch: RegExpExecArray | null
let hasTransformedTemplates = false

while ((templateMatch = templateLiteralRE.exec(cleanString))) {
const [[,], [urlStart, urlEnd]] = templateMatch.indices!
const rawUrl = code.slice(urlStart, urlEnd)

if (rawUrl.includes('${')) {
const transformed = await transformSafeTemplateLiteral(rawUrl)
if (transformed) {
s ||= new MagicString(code)
s.update(urlStart, urlEnd, transformed)
hasTransformedTemplates = true
} else {
// Unsafe dynamic template string
this.error(
`\`new URL(url, import.meta.url)\` is not supported in dynamic template string.\n` +
`Only template literals with \`import.meta.env.*\` expressions are supported.\n` +
`Use string concatenation instead: new URL('path/' + variable + '/file.ts', import.meta.url)`,
urlStart,
)
}
}
}

// If we transformed templates, return and let this run again
if (hasTransformedTemplates && s) {
return transformStableResult(s, id, config)
}

// Process worker URLs (regular strings and template literals without expressions)
const workerImportMetaUrlRE =
/\bnew\s+(?:Worker|SharedWorker)\s*\(\s*(new\s+URL\s*\(\s*('[^']+'|"[^"]+"|`[^`]+`)\s*,\s*import\.meta\.url\s*\))/dg

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

const rawUrl = code.slice(urlStart, urlEnd)

// potential dynamic template string
// Skip template literals with expressions (should not happen at this point)
if (rawUrl[0] === '`' && rawUrl.includes('${')) {
this.error(
`\`new URL(url, import.meta.url)\` is not supported in dynamic template string.`,
expStart,
)
continue
}

s ||= new MagicString(code)
Expand Down
12 changes: 12 additions & 0 deletions playground/worker/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Example environment variables for worker template literal tests
# Copy this file to .env and uncomment to test the template literal feature

# Directory where workers are located
# Example: VITE_WORKER_DIR=worker
# Results in path: ./worker/simple-worker.js
VITE_WORKER_DIR=worker

# Worker file name for testing multiple env vars
# Example: VITE_WORKER_FILE=simple-worker
# Combined with VITE_WORKER_DIR results in: ./worker/simple-worker.js
VITE_WORKER_FILE=simple-worker
5 changes: 5 additions & 0 deletions playground/worker/env-path-worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Worker for testing template literal paths with import.meta.env
self.onmessage = (e) => {
console.log('env-path-worker received:', e.data)
self.postMessage({ pong: 'from-env-path-worker' })
}
126 changes: 126 additions & 0 deletions playground/worker/template-literal-test.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<!doctype html>
<html>
<head>
<title>Template Literal Worker Test</title>
</head>
<body>
<h1>Template Literal Worker Path Test</h1>
<p>
This test demonstrates template literals with import.meta.env in worker
URLs.
</p>
<p>
Set <code>VITE_WORKER_DIR=worker</code> in .env file to test (defaults to
empty string if not set).
</p>

<div>
<p>
Test 1 - Single env variable in template (transformed to string
concatenation):
</p>
<pre><code>new URL(`./\${import.meta.env.VITE_WORKER_DIR}/simple-worker.js`, import.meta.url)</code></pre>
<div id="test1-result">Waiting...</div>
</div>

<div>
<p>
Test 2 - Multiple env variables (demonstrating complex template
literal):
</p>
<pre><code>new URL(`./\${import.meta.env.VITE_WORKER_DIR}/\${import.meta.env.VITE_WORKER_FILE}.js`, import.meta.url)</code></pre>
<div id="test2-result">Waiting...</div>
</div>

<div>
<p>Test 3 - Fallback to simple path when env not set:</p>
<div id="test3-result">Waiting...</div>
</div>

<script type="module">
// Note: In a real project, set these in your .env file:
// VITE_WORKER_DIR=worker
// VITE_WORKER_FILE=simple-worker

// Test 1: Single env variable (transformed to string concatenation)
// This template literal: `./${import.meta.env.VITE_WORKER_DIR}/simple-worker.js`
// Will be transformed to: './' + import.meta.env.VITE_WORKER_DIR + '/simple-worker.js'
try {
const worker1 = new Worker(
new URL(
`./${import.meta.env.VITE_WORKER_DIR}/simple-worker.js`,
import.meta.url,
),
{ type: 'module' },
)

worker1.onmessage = (e) => {
document.getElementById('test1-result').textContent =
`✅ Success: ${JSON.stringify(e.data)}`
}

worker1.onerror = (e) => {
document.getElementById('test1-result').textContent =
`❌ Error: ${e.message}`
}

worker1.postMessage('ping from test1')
} catch (e) {
document.getElementById('test1-result').textContent =
`❌ Exception: ${e.message}`
}

// Test 2: Multiple env variables
// This demonstrates the feature with multiple import.meta.env expressions
// Template: `./${import.meta.env.VITE_WORKER_DIR}/${import.meta.env.VITE_WORKER_FILE}.js`
// Will be transformed to: './' + import.meta.env.VITE_WORKER_DIR + '/' + import.meta.env.VITE_WORKER_FILE + '.js'
try {
const worker2 = new Worker(
new URL(
`./${import.meta.env.VITE_WORKER_DIR}/${import.meta.env.VITE_WORKER_FILE}.js`,
import.meta.url,
),
{ type: 'module' },
)

worker2.onmessage = (e) => {
document.getElementById('test2-result').textContent =
`✅ Success: ${JSON.stringify(e.data)}`
}

worker2.onerror = (e) => {
document.getElementById('test2-result').textContent =
`❌ Error: ${e.message}`
}

worker2.postMessage('ping from test2')
} catch (e) {
document.getElementById('test2-result').textContent =
`❌ Exception: ${e.message}`
}

// Test 3: Regular string (should still work as before)
try {
const worker3 = new Worker(
new URL('./simple-worker.js', import.meta.url),
{ type: 'module' },
)

worker3.onmessage = (e) => {
document.getElementById('test3-result').textContent =
`✅ Success: ${JSON.stringify(e.data)}`
}

worker3.onerror = (e) => {
document.getElementById('test3-result').textContent =
`❌ Error: ${e.message}`
}

worker3.postMessage('ping from test3')
} catch (e) {
document.getElementById('test3-result').textContent =
`❌ Exception: ${e.message}`
}
</script>
</body>
</html>