Skip to content

Commit d0c3523

Browse files
authoredJan 23, 2025··
feat(worker): support dynamic worker option fields (#19010)
1 parent caad985 commit d0c3523

File tree

3 files changed

+254
-10
lines changed

3 files changed

+254
-10
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import { describe, expect, test } from 'vitest'
2+
import { parseAst } from 'rollup/parseAst'
3+
import { workerImportMetaUrlPlugin } from '../../plugins/workerImportMetaUrl'
4+
import { resolveConfig } from '../../config'
5+
import { PartialEnvironment } from '../../baseEnvironment'
6+
7+
async function createWorkerImportMetaUrlPluginTransform() {
8+
const config = await resolveConfig({ configFile: false }, 'serve')
9+
const instance = workerImportMetaUrlPlugin(config)
10+
const environment = new PartialEnvironment('client', config)
11+
12+
return async (code: string) => {
13+
// @ts-expect-error transform should exist
14+
const result = await instance.transform.call(
15+
{ environment, parse: parseAst },
16+
code,
17+
'foo.ts',
18+
)
19+
return result?.code || result
20+
}
21+
}
22+
23+
describe('workerImportMetaUrlPlugin', async () => {
24+
const transform = await createWorkerImportMetaUrlPluginTransform()
25+
26+
test('without worker options', async () => {
27+
expect(
28+
await transform('new Worker(new URL("./worker.js", import.meta.url))'),
29+
).toMatchInlineSnapshot(
30+
`"new Worker(new URL(/* @vite-ignore */ "/worker.js?worker_file&type=classic", import.meta.url))"`,
31+
)
32+
})
33+
34+
test('with shared worker', async () => {
35+
expect(
36+
await transform(
37+
'new SharedWorker(new URL("./worker.js", import.meta.url))',
38+
),
39+
).toMatchInlineSnapshot(
40+
`"new SharedWorker(new URL(/* @vite-ignore */ "/worker.js?worker_file&type=classic", import.meta.url))"`,
41+
)
42+
})
43+
44+
test('with static worker options and identifier properties', async () => {
45+
expect(
46+
await transform(
47+
'new Worker(new URL("./worker.js", import.meta.url), { type: "module", name: "worker1" })',
48+
),
49+
).toMatchInlineSnapshot(
50+
`"new Worker(new URL(/* @vite-ignore */ "/worker.js?worker_file&type=module", import.meta.url), { type: "module", name: "worker1" })"`,
51+
)
52+
})
53+
54+
test('with static worker options and literal properties', async () => {
55+
expect(
56+
await transform(
57+
'new Worker(new URL("./worker.js", import.meta.url), { "type": "module", "name": "worker1" })',
58+
),
59+
).toMatchInlineSnapshot(
60+
`"new Worker(new URL(/* @vite-ignore */ "/worker.js?worker_file&type=module", import.meta.url), { "type": "module", "name": "worker1" })"`,
61+
)
62+
})
63+
64+
test('with dynamic name field in worker options', async () => {
65+
expect(
66+
await transform(
67+
'const id = 1; new Worker(new URL("./worker.js", import.meta.url), { name: "worker" + id })',
68+
),
69+
).toMatchInlineSnapshot(
70+
`"const id = 1; new Worker(new URL(/* @vite-ignore */ "/worker.js?worker_file&type=classic", import.meta.url), { name: "worker" + id })"`,
71+
)
72+
})
73+
74+
test('with dynamic name field and static type in worker options', async () => {
75+
expect(
76+
await transform(
77+
'const id = 1; new Worker(new URL("./worker.js", import.meta.url), { name: "worker" + id, type: "module" })',
78+
),
79+
).toMatchInlineSnapshot(
80+
`"const id = 1; new Worker(new URL(/* @vite-ignore */ "/worker.js?worker_file&type=module", import.meta.url), { name: "worker" + id, type: "module" })"`,
81+
)
82+
})
83+
84+
test('with parenthesis inside of worker options', async () => {
85+
expect(
86+
await transform(
87+
'const worker = new Worker(new URL("./worker.js", import.meta.url), { name: genName(), type: "module"})',
88+
),
89+
).toMatchInlineSnapshot(
90+
`"const worker = new Worker(new URL(/* @vite-ignore */ "/worker.js?worker_file&type=module", import.meta.url), { name: genName(), type: "module"})"`,
91+
)
92+
})
93+
94+
test('with multi-line code and worker options', async () => {
95+
expect(
96+
await transform(`
97+
const worker = new Worker(new URL("./worker.js", import.meta.url), {
98+
name: genName(),
99+
type: "module",
100+
},
101+
)
102+
103+
worker.addEventListener('message', (ev) => text('.simple-worker-url', JSON.stringify(ev.data)))
104+
`),
105+
).toMatchInlineSnapshot(`"
106+
const worker = new Worker(new URL(/* @vite-ignore */ "/worker.js?worker_file&type=module", import.meta.url), {
107+
name: genName(),
108+
type: "module",
109+
},
110+
)
111+
112+
worker.addEventListener('message', (ev) => text('.simple-worker-url', JSON.stringify(ev.data)))
113+
"`)
114+
})
115+
116+
test('throws an error when non-static worker options are provided', async () => {
117+
await expect(
118+
transform(
119+
'new Worker(new URL("./worker.js", import.meta.url), myWorkerOptions)',
120+
),
121+
).rejects.toThrow(
122+
'Vite is unable to parse the worker options as the value is not static. To ignore this error, please use /* @vite-ignore */ in the worker options.',
123+
)
124+
})
125+
126+
test('throws an error when worker options are not an object', async () => {
127+
await expect(
128+
transform(
129+
'new Worker(new URL("./worker.js", import.meta.url), "notAnObject")',
130+
),
131+
).rejects.toThrow('Expected worker options to be an object, got string')
132+
})
133+
134+
test('throws an error when non-literal type field in worker options', async () => {
135+
await expect(
136+
transform(
137+
'const type = "module"; new Worker(new URL("./worker.js", import.meta.url), { type })',
138+
),
139+
).rejects.toThrow(
140+
'Expected worker options type property to be a literal value.',
141+
)
142+
})
143+
144+
test('throws an error when spread operator used without the type field', async () => {
145+
await expect(
146+
transform(
147+
'const options = { name: "worker1" }; new Worker(new URL("./worker.js", import.meta.url), { ...options })',
148+
),
149+
).rejects.toThrow(
150+
'Expected object spread to be used before the definition of the type property. Vite needs a static value for the type property to correctly infer it.',
151+
)
152+
})
153+
154+
test('throws an error when spread operator used after definition of type field', async () => {
155+
await expect(
156+
transform(
157+
'const options = { name: "worker1" }; new Worker(new URL("./worker.js", import.meta.url), { type: "module", ...options })',
158+
),
159+
).rejects.toThrow(
160+
'Expected object spread to be used before the definition of the type property. Vite needs a static value for the type property to correctly infer it.',
161+
)
162+
})
163+
})

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

+91-9
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import path from 'node:path'
22
import MagicString from 'magic-string'
3-
import type { RollupError } from 'rollup'
3+
import type { RollupAstNode, RollupError } from 'rollup'
4+
import { parseAstAsync } from 'rollup/parseAst'
45
import { stripLiteral } from 'strip-literal'
6+
import type { Expression, ExpressionStatement } from 'estree'
57
import type { ResolvedConfig } from '../config'
68
import type { Plugin } from '../plugin'
79
import { evalValue, injectQuery, transformStableResult } from '../utils'
@@ -25,16 +27,92 @@ function err(e: string, pos: number) {
2527
return error
2628
}
2729

28-
function parseWorkerOptions(
30+
function findClosingParen(input: string, fromIndex: number) {
31+
let count = 1
32+
33+
for (let i = fromIndex + 1; i < input.length; i++) {
34+
if (input[i] === '(') count++
35+
if (input[i] === ')') count--
36+
if (count === 0) return i
37+
}
38+
39+
return -1
40+
}
41+
42+
function extractWorkerTypeFromAst(
43+
expression: Expression,
44+
optsStartIndex: number,
45+
): 'classic' | 'module' | undefined {
46+
if (expression.type !== 'ObjectExpression') {
47+
return
48+
}
49+
50+
let lastSpreadElementIndex = -1
51+
let typeProperty = null
52+
let typePropertyIndex = -1
53+
54+
for (let i = 0; i < expression.properties.length; i++) {
55+
const property = expression.properties[i]
56+
57+
if (property.type === 'SpreadElement') {
58+
lastSpreadElementIndex = i
59+
continue
60+
}
61+
62+
if (
63+
property.type === 'Property' &&
64+
((property.key.type === 'Identifier' && property.key.name === 'type') ||
65+
(property.key.type === 'Literal' && property.key.value === 'type'))
66+
) {
67+
typeProperty = property
68+
typePropertyIndex = i
69+
}
70+
}
71+
72+
if (typePropertyIndex === -1 && lastSpreadElementIndex === -1) {
73+
// No type property or spread element in use. Assume safe usage and default to classic
74+
return 'classic'
75+
}
76+
77+
if (typePropertyIndex < lastSpreadElementIndex) {
78+
throw err(
79+
'Expected object spread to be used before the definition of the type property. ' +
80+
'Vite needs a static value for the type property to correctly infer it.',
81+
optsStartIndex,
82+
)
83+
}
84+
85+
if (typeProperty?.value.type !== 'Literal') {
86+
throw err(
87+
'Expected worker options type property to be a literal value.',
88+
optsStartIndex,
89+
)
90+
}
91+
92+
// Silently default to classic type like the getWorkerType method
93+
return typeProperty?.value.value === 'module' ? 'module' : 'classic'
94+
}
95+
96+
async function parseWorkerOptions(
2997
rawOpts: string,
3098
optsStartIndex: number,
31-
): WorkerOptions {
99+
): Promise<WorkerOptions> {
32100
let opts: WorkerOptions = {}
33101
try {
34102
opts = evalValue<WorkerOptions>(rawOpts)
35103
} catch {
104+
const optsNode = (
105+
(await parseAstAsync(`(${rawOpts})`))
106+
.body[0] as RollupAstNode<ExpressionStatement>
107+
).expression
108+
109+
const type = extractWorkerTypeFromAst(optsNode, optsStartIndex)
110+
if (type) {
111+
return { type }
112+
}
113+
36114
throw err(
37-
'Vite is unable to parse the worker options as the value is not static.' +
115+
'Vite is unable to parse the worker options as the value is not static. ' +
38116
'To ignore this error, please use /* @vite-ignore */ in the worker options.',
39117
optsStartIndex,
40118
)
@@ -54,12 +132,16 @@ function parseWorkerOptions(
54132
return opts
55133
}
56134

57-
function getWorkerType(raw: string, clean: string, i: number): WorkerType {
135+
async function getWorkerType(
136+
raw: string,
137+
clean: string,
138+
i: number,
139+
): Promise<WorkerType> {
58140
const commaIndex = clean.indexOf(',', i)
59141
if (commaIndex === -1) {
60142
return 'classic'
61143
}
62-
const endIndex = clean.indexOf(')', i)
144+
const endIndex = findClosingParen(clean, i)
63145

64146
// case: ') ... ,' mean no worker options params
65147
if (commaIndex > endIndex) {
@@ -82,7 +164,7 @@ function getWorkerType(raw: string, clean: string, i: number): WorkerType {
82164
return 'classic'
83165
}
84166

85-
const workerOpts = parseWorkerOptions(workerOptString, commaIndex + 1)
167+
const workerOpts = await parseWorkerOptions(workerOptString, commaIndex + 1)
86168
if (
87169
workerOpts.type &&
88170
(workerOpts.type === 'module' || workerOpts.type === 'classic')
@@ -152,12 +234,12 @@ export function workerImportMetaUrlPlugin(config: ResolvedConfig): Plugin {
152234
}
153235

154236
s ||= new MagicString(code)
155-
const workerType = getWorkerType(code, cleanString, endIndex)
237+
const workerType = await getWorkerType(code, cleanString, endIndex)
156238
const url = rawUrl.slice(1, -1)
157239
let file: string | undefined
158240
if (url[0] === '.') {
159241
file = path.resolve(path.dirname(id), url)
160-
file = tryFsResolve(file, fsResolveOptions) ?? file
242+
file = slash(tryFsResolve(file, fsResolveOptions) ?? file)
161243
} else {
162244
workerResolver ??= createBackCompatIdResolver(config, {
163245
extensions: [],

‎playground/worker/worker/main-module.js

-1
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,6 @@ const genWorkerName = () => 'module'
140140
const w2 = new SharedWorker(
141141
new URL('../url-shared-worker.js', import.meta.url),
142142
{
143-
/* @vite-ignore */
144143
name: genWorkerName(),
145144
type: 'module',
146145
},

0 commit comments

Comments
 (0)
Please sign in to comment.