Skip to content

Commit 64eabe0

Browse files
author
Sebastian Krüger
committed
2 parents 5eeec97 + 61e7a4b commit 64eabe0

File tree

10 files changed

+376
-52
lines changed

10 files changed

+376
-52
lines changed

.github/workflows/test-latest-vite.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,12 @@ jobs:
4242
4343
- name: Install Playwright browsers
4444
working-directory: test-minimal-example
45-
run: npx playwright install chromium
45+
run: npx playwright install chromium firefox
4646

4747
- name: Run tests
4848
working-directory: test-minimal-example
49+
env:
50+
CI: true
4951
run: npm test
5052

5153
- name: Report Vite version

example/package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
"@types/node": "22.17.0",
3939
"comlink": "4.4.2",
4040
"tsup": "8.5.0",
41-
"typescript": "5.7.3",
41+
"typescript": "5.9.2",
4242
"vite": "5.4.19"
4343
},
4444
"peerDependencies": {

src/index.ts

Lines changed: 92 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,47 @@ import MagicString from "magic-string";
33
import { Plugin, normalizePath } from "vite";
44
import { SourceMapConsumer, SourceMapGenerator } from "source-map";
55

6+
// Template string to avoid static analysis issues with import.meta.url
67
const importMetaUrl = `${"import"}.meta.url`;
8+
9+
// Virtual module prefixes for identifying Comlink worker modules
710
const urlPrefix_normal = "internal:comlink:";
811
const urlPrefix_shared = "internal:comlink-shared:";
912

13+
// Global state to track build mode and project root
14+
// These are set during Vite's config resolution phase
1015
let mode = "";
1116
let root = "";
1217

18+
/**
19+
* Vite plugin that automatically integrates Comlink with WebWorkers and SharedWorkers.
20+
*
21+
* This plugin transforms ComlinkWorker and ComlinkSharedWorker constructor calls
22+
* to regular Worker/SharedWorker instances wrapped with Comlink's expose/wrap functionality.
23+
*
24+
* @returns Array of Vite plugins (currently contains only one plugin)
25+
*/
1326
export function comlink(): Plugin[] {
1427
return [
1528
{
29+
/**
30+
* Store Vite configuration values for later use in transformations
31+
*/
1632
configResolved(conf) {
1733
mode = conf.mode;
1834
root = conf.root;
1935
},
2036
name: "comlink",
37+
38+
/**
39+
* Resolve virtual module IDs for Comlink worker wrappers.
40+
*
41+
* When a ComlinkWorker/ComlinkSharedWorker is detected, we create virtual modules
42+
* with special prefixes that contain the Comlink setup code.
43+
*
44+
* @param id - Module ID to resolve
45+
* @returns Resolved ID if it's a Comlink virtual module, undefined otherwise
46+
*/
2147
resolveId(id) {
2248
if (id.includes(urlPrefix_normal)) {
2349
return urlPrefix_normal + id.split(urlPrefix_normal)[1];
@@ -26,10 +52,22 @@ export function comlink(): Plugin[] {
2652
return urlPrefix_shared + id.split(urlPrefix_shared)[1];
2753
}
2854
},
55+
/**
56+
* Load virtual modules that contain Comlink worker setup code.
57+
*
58+
* This creates wrapper modules that automatically call Comlink's expose()
59+
* function with the worker's exported API.
60+
*
61+
* @param id - Module ID to load
62+
* @returns Generated module code for Comlink setup, or undefined
63+
*/
2964
async load(id) {
3065
if (id.includes(urlPrefix_normal)) {
66+
// Extract the real worker file path from the virtual module ID
3167
const realID = normalizePath(id.replace(urlPrefix_normal, ""));
3268

69+
// Generate wrapper code for regular Workers
70+
// This imports the worker's API and exposes it through Comlink
3371
return `
3472
import {expose} from 'comlink'
3573
import * as api from '${normalizePath(realID)}'
@@ -39,8 +77,11 @@ export function comlink(): Plugin[] {
3977
}
4078

4179
if (id.includes(urlPrefix_shared)) {
80+
// Extract the real worker file path from the virtual module ID
4281
const realID = normalizePath(id.replace(urlPrefix_shared, ""));
4382

83+
// Generate wrapper code for SharedWorkers
84+
// SharedWorkers need to handle the 'connect' event and expose on each port
4485
return `
4586
import {expose} from 'comlink'
4687
import * as api from '${normalizePath(realID)}'
@@ -49,90 +90,121 @@ export function comlink(): Plugin[] {
4990
const port = event.ports[0];
5091
5192
expose(api, port);
52-
// We might need this later...
53-
// port.start()
93+
// Note: port.start() is typically not needed as expose() handles this
5494
})
5595
`;
5696
}
5797
},
98+
/**
99+
* Transform source code to replace ComlinkWorker/ComlinkSharedWorker constructors.
100+
*
101+
* This is the core transformation that:
102+
* 1. Finds ComlinkWorker/ComlinkSharedWorker constructor calls
103+
* 2. Extracts the worker URL and options
104+
* 3. Replaces them with regular Worker/SharedWorker constructors
105+
* 4. Wraps the result with Comlink's wrap() function
106+
* 5. Redirects to virtual modules for automatic Comlink setup
107+
*
108+
* @param code - Source code to transform
109+
* @param id - File ID being transformed
110+
* @returns Transformed code with source maps, or undefined if no changes needed
111+
*/
58112
async transform(code: string, id: string) {
113+
// Early exit if file doesn't contain Comlink worker constructors
59114
if (
60115
!code.includes("ComlinkWorker") &&
61116
!code.includes("ComlinkSharedWorker")
62117
)
63118
return;
64119

120+
// Regex to match ComlinkWorker/ComlinkSharedWorker constructor patterns
121+
// Captures: new keyword, constructor type, URL parameters, options, closing parenthesis
65122
const workerSearcher =
66123
/(\bnew\s+)(ComlinkWorker|ComlinkSharedWorker)(\s*\(\s*new\s+URL\s*\(\s*)('[^']+'|"[^"]+"|`[^`]+`)(\s*,\s*import\.meta\.url\s*\)\s*)(,?)([^\)]*)(\))/g;
67124

68125
let s: MagicString = new MagicString(code);
69126

70127
const matches = code.matchAll(workerSearcher);
71128

129+
// Process each matched ComlinkWorker/ComlinkSharedWorker constructor
72130
for (const match of matches) {
73131
const index = match.index!;
74132
const matchCode = match[0];
75-
const c1_new = match[1];
76-
const c2_type = match[2];
77-
const c3_new_url = match[3];
78-
let c4_path = match[4];
79-
const c5_import_meta = match[5];
80-
const c6_koma = match[6];
81-
const c7_options = match[7];
82-
const c8_end = match[8];
83-
133+
134+
// Extract regex capture groups
135+
const c1_new = match[1]; // "new " keyword
136+
const c2_type = match[2]; // "ComlinkWorker" or "ComlinkSharedWorker"
137+
const c3_new_url = match[3]; // "new URL(" part
138+
let c4_path = match[4]; // The quoted path string
139+
const c5_import_meta = match[5]; // ", import.meta.url)" part
140+
const c6_koma = match[6]; // Optional comma before options
141+
const c7_options = match[7]; // Worker options object
142+
const c8_end = match[8]; // Closing parenthesis
143+
144+
// Parse worker options using JSON5 (supports comments, trailing commas, etc.)
84145
const opt = c7_options ? JSON5.parse(c7_options) : {};
85146

147+
// Extract and remove quotes from the path
86148
const urlQuote = c4_path[0];
87-
88149
c4_path = c4_path.substring(1, c4_path.length - 1);
89150

151+
// Force module type in development for better debugging experience
90152
if (mode === "development") {
91153
opt.type = "module";
92154
}
93155
const options = JSON.stringify(opt);
94156

157+
// Determine virtual module prefix and native worker class based on type
95158
const prefix =
96159
c2_type === "ComlinkWorker" ? urlPrefix_normal : urlPrefix_shared;
97160
const className =
98161
c2_type == "ComlinkWorker" ? "Worker" : "SharedWorker";
99162

163+
// Resolve the worker file path using Vite's resolution system
100164
const res = await this.resolve(c4_path, id, {});
101165
let path = c4_path;
102166

103167
if (res) {
104168
path = res.id;
169+
// Convert absolute path to relative if it's within project root
105170
if (path.startsWith(root)) {
106171
path = path.substring(root.length);
107172
}
108173
}
174+
175+
// Build the new worker constructor with virtual module URL
109176
const worker_constructor = `${c1_new}${className}${c3_new_url}${urlQuote}${prefix}${path}${urlQuote}${c5_import_meta},${options}${c8_end}`;
110177

178+
// SharedWorkers need .port property to access MessagePort
111179
const extra_shared = c2_type == "ComlinkWorker" ? "" : ".port";
112180

181+
// Generate the final code that wraps the worker with Comlink
113182
const insertCode = `___wrap((${worker_constructor})${extra_shared});\n`;
114183

184+
// Replace the original constructor call with our transformed version
115185
s.overwrite(index, index + matchCode.length, insertCode);
116186
}
117187

188+
// Add import for Comlink wrap function at the top of the file
118189
s.appendLeft(
119190
0,
120191
`import {wrap as ___wrap} from 'vite-plugin-comlink/symbol';\n`
121192
);
122193

123-
// Generate source map for our transformations
194+
// Generate source map for our transformations with high resolution
124195
const magicStringMap = s.generateMap({
125196
source: id,
126197
includeContent: true,
127-
hires: true
198+
hires: true // High-resolution source maps for better debugging
128199
});
129200

130-
// Get the existing source map from previous transforms
201+
// Get the existing source map from previous transforms in the pipeline
131202
const existingMap = this.getCombinedSourcemap();
132203

133204
let finalMap = magicStringMap;
134205

135-
// If there's an existing source map, we need to combine them
206+
// Combine source maps if there are previous transformations
207+
// This ensures debugging works correctly through the entire transformation chain
136208
if (existingMap && existingMap.mappings && existingMap.mappings !== '') {
137209
try {
138210
// Create consumers for both source maps
@@ -145,11 +217,12 @@ export function comlink(): Plugin[] {
145217

146218
finalMap = generator.toJSON() as any;
147219

148-
// Clean up consumers
220+
// Clean up consumers to prevent memory leaks
149221
existingConsumer.destroy();
150222
newConsumer.destroy();
151223
} catch (error) {
152-
// If source map combination fails, fall back to magic string map
224+
// If source map combination fails, fall back to our generated map
225+
// This ensures the build doesn't fail due to source map issues
153226
console.warn('Failed to combine source maps:', error);
154227
finalMap = magicStringMap;
155228
}
@@ -164,4 +237,5 @@ export function comlink(): Plugin[] {
164237
];
165238
}
166239

240+
// Export as default for convenience
167241
export default comlink;

src/symbol.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,51 @@
11
import { wrap as comlink_wrap } from "comlink";
2+
3+
// Re-export commonly used Comlink utilities for convenience
24
export {
35
proxy,
46
proxyMarker,
57
finalizer,
68
releaseProxy,
79
createEndpoint
810
} from 'comlink'
11+
12+
/**
13+
* Symbol used to access the underlying Worker/SharedWorker instance
14+
* from a Comlink-wrapped worker proxy.
15+
*
16+
* Usage:
17+
* ```ts
18+
* const worker = new ComlinkWorker<typeof import('./worker')>(
19+
* new URL('./worker', import.meta.url)
20+
* );
21+
* const nativeWorker = worker[endpointSymbol]; // Access underlying Worker
22+
* ```
23+
*/
924
export const endpointSymbol = Symbol("getEndpoint");
1025

1126
/**
12-
* internal API
27+
* Enhanced wrap function that extends Comlink's wrap with endpoint access.
28+
*
29+
* This function wraps a Worker/SharedWorker endpoint with Comlink's proxy,
30+
* but also adds the ability to access the original endpoint via a symbol.
31+
* This allows users to access native Worker methods and properties when needed.
32+
*
33+
* @param ep - The endpoint (Worker, SharedWorker.port, MessagePort, etc.) to wrap
34+
* @returns Comlink proxy with additional endpoint access via endpointSymbol
35+
*
36+
* @internal This is used internally by the plugin transformation
1337
*/
1438
export const wrap: typeof comlink_wrap = (ep) => {
39+
// Create the standard Comlink proxy
1540
const wrapped = comlink_wrap(ep);
41+
42+
// Enhance the proxy to expose the underlying endpoint via symbol
1643
return new Proxy(wrapped, {
1744
get(target, prop, receiver) {
45+
// If accessing the endpoint symbol, return the original endpoint
1846
if (prop === endpointSymbol) return ep;
47+
48+
// Otherwise, delegate to the wrapped Comlink proxy
1949
return Reflect.get(target, prop, receiver);
2050
}
2151
}) as any;
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
export function conditionalWorkerCreation(shouldCreateShared: boolean) {
2+
// Check if we're in a worker context or main thread
3+
const isMainThread = typeof window !== 'undefined';
4+
const hasSharedWorker = isMainThread && typeof SharedWorker !== 'undefined';
5+
6+
if (shouldCreateShared && hasSharedWorker) {
7+
return { type: 'shared', supported: true, context: 'main', hasSharedWorker };
8+
} else {
9+
return { type: 'regular', supported: true, context: isMainThread ? 'main' : 'worker', hasSharedWorker };
10+
}
11+
}
12+
13+
export async function testWorkerInFunction() {
14+
const worker = new ComlinkWorker<typeof import('./worker')>(
15+
new URL('./worker.ts', import.meta.url)
16+
);
17+
18+
const result = await worker.add(10, 20);
19+
return result;
20+
}
21+
22+
// Test multiple workers creation and management
23+
let workers: Array<any> = [];
24+
25+
export async function createMultipleWorkers(count: number) {
26+
workers = [];
27+
for (let i = 0; i < count; i++) {
28+
const worker = new ComlinkWorker<typeof import('./worker')>(
29+
new URL('./worker.ts', import.meta.url)
30+
);
31+
workers.push(worker);
32+
}
33+
return workers.length;
34+
}
35+
36+
export async function testAllWorkers() {
37+
const promises = workers.map((worker, index) =>
38+
worker.add(index, index + 1)
39+
);
40+
return Promise.all(promises);
41+
}

0 commit comments

Comments
 (0)