@@ -3,21 +3,47 @@ import MagicString from "magic-string";
3
3
import { Plugin , normalizePath } from "vite" ;
4
4
import { SourceMapConsumer , SourceMapGenerator } from "source-map" ;
5
5
6
+ // Template string to avoid static analysis issues with import.meta.url
6
7
const importMetaUrl = `${ "import" } .meta.url` ;
8
+
9
+ // Virtual module prefixes for identifying Comlink worker modules
7
10
const urlPrefix_normal = "internal:comlink:" ;
8
11
const urlPrefix_shared = "internal:comlink-shared:" ;
9
12
13
+ // Global state to track build mode and project root
14
+ // These are set during Vite's config resolution phase
10
15
let mode = "" ;
11
16
let root = "" ;
12
17
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
+ */
13
26
export function comlink ( ) : Plugin [ ] {
14
27
return [
15
28
{
29
+ /**
30
+ * Store Vite configuration values for later use in transformations
31
+ */
16
32
configResolved ( conf ) {
17
33
mode = conf . mode ;
18
34
root = conf . root ;
19
35
} ,
20
36
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
+ */
21
47
resolveId ( id ) {
22
48
if ( id . includes ( urlPrefix_normal ) ) {
23
49
return urlPrefix_normal + id . split ( urlPrefix_normal ) [ 1 ] ;
@@ -26,10 +52,22 @@ export function comlink(): Plugin[] {
26
52
return urlPrefix_shared + id . split ( urlPrefix_shared ) [ 1 ] ;
27
53
}
28
54
} ,
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
+ */
29
64
async load ( id ) {
30
65
if ( id . includes ( urlPrefix_normal ) ) {
66
+ // Extract the real worker file path from the virtual module ID
31
67
const realID = normalizePath ( id . replace ( urlPrefix_normal , "" ) ) ;
32
68
69
+ // Generate wrapper code for regular Workers
70
+ // This imports the worker's API and exposes it through Comlink
33
71
return `
34
72
import {expose} from 'comlink'
35
73
import * as api from '${ normalizePath ( realID ) } '
@@ -39,8 +77,11 @@ export function comlink(): Plugin[] {
39
77
}
40
78
41
79
if ( id . includes ( urlPrefix_shared ) ) {
80
+ // Extract the real worker file path from the virtual module ID
42
81
const realID = normalizePath ( id . replace ( urlPrefix_shared , "" ) ) ;
43
82
83
+ // Generate wrapper code for SharedWorkers
84
+ // SharedWorkers need to handle the 'connect' event and expose on each port
44
85
return `
45
86
import {expose} from 'comlink'
46
87
import * as api from '${ normalizePath ( realID ) } '
@@ -49,90 +90,121 @@ export function comlink(): Plugin[] {
49
90
const port = event.ports[0];
50
91
51
92
expose(api, port);
52
- // We might need this later...
53
- // port.start()
93
+ // Note: port.start() is typically not needed as expose() handles this
54
94
})
55
95
` ;
56
96
}
57
97
} ,
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
+ */
58
112
async transform ( code : string , id : string ) {
113
+ // Early exit if file doesn't contain Comlink worker constructors
59
114
if (
60
115
! code . includes ( "ComlinkWorker" ) &&
61
116
! code . includes ( "ComlinkSharedWorker" )
62
117
)
63
118
return ;
64
119
120
+ // Regex to match ComlinkWorker/ComlinkSharedWorker constructor patterns
121
+ // Captures: new keyword, constructor type, URL parameters, options, closing parenthesis
65
122
const workerSearcher =
66
123
/ ( \b n e w \s + ) ( C o m l i n k W o r k e r | C o m l i n k S h a r e d W o r k e r ) ( \s * \( \s * n e w \s + U R L \s * \( \s * ) ( ' [ ^ ' ] + ' | " [ ^ " ] + " | ` [ ^ ` ] + ` ) ( \s * , \s * i m p o r t \. m e t a \. u r l \s * \) \s * ) ( , ? ) ( [ ^ \) ] * ) ( \) ) / g;
67
124
68
125
let s : MagicString = new MagicString ( code ) ;
69
126
70
127
const matches = code . matchAll ( workerSearcher ) ;
71
128
129
+ // Process each matched ComlinkWorker/ComlinkSharedWorker constructor
72
130
for ( const match of matches ) {
73
131
const index = match . index ! ;
74
132
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.)
84
145
const opt = c7_options ? JSON5 . parse ( c7_options ) : { } ;
85
146
147
+ // Extract and remove quotes from the path
86
148
const urlQuote = c4_path [ 0 ] ;
87
-
88
149
c4_path = c4_path . substring ( 1 , c4_path . length - 1 ) ;
89
150
151
+ // Force module type in development for better debugging experience
90
152
if ( mode === "development" ) {
91
153
opt . type = "module" ;
92
154
}
93
155
const options = JSON . stringify ( opt ) ;
94
156
157
+ // Determine virtual module prefix and native worker class based on type
95
158
const prefix =
96
159
c2_type === "ComlinkWorker" ? urlPrefix_normal : urlPrefix_shared ;
97
160
const className =
98
161
c2_type == "ComlinkWorker" ? "Worker" : "SharedWorker" ;
99
162
163
+ // Resolve the worker file path using Vite's resolution system
100
164
const res = await this . resolve ( c4_path , id , { } ) ;
101
165
let path = c4_path ;
102
166
103
167
if ( res ) {
104
168
path = res . id ;
169
+ // Convert absolute path to relative if it's within project root
105
170
if ( path . startsWith ( root ) ) {
106
171
path = path . substring ( root . length ) ;
107
172
}
108
173
}
174
+
175
+ // Build the new worker constructor with virtual module URL
109
176
const worker_constructor = `${ c1_new } ${ className } ${ c3_new_url } ${ urlQuote } ${ prefix } ${ path } ${ urlQuote } ${ c5_import_meta } ,${ options } ${ c8_end } ` ;
110
177
178
+ // SharedWorkers need .port property to access MessagePort
111
179
const extra_shared = c2_type == "ComlinkWorker" ? "" : ".port" ;
112
180
181
+ // Generate the final code that wraps the worker with Comlink
113
182
const insertCode = `___wrap((${ worker_constructor } )${ extra_shared } );\n` ;
114
183
184
+ // Replace the original constructor call with our transformed version
115
185
s . overwrite ( index , index + matchCode . length , insertCode ) ;
116
186
}
117
187
188
+ // Add import for Comlink wrap function at the top of the file
118
189
s . appendLeft (
119
190
0 ,
120
191
`import {wrap as ___wrap} from 'vite-plugin-comlink/symbol';\n`
121
192
) ;
122
193
123
- // Generate source map for our transformations
194
+ // Generate source map for our transformations with high resolution
124
195
const magicStringMap = s . generateMap ( {
125
196
source : id ,
126
197
includeContent : true ,
127
- hires : true
198
+ hires : true // High-resolution source maps for better debugging
128
199
} ) ;
129
200
130
- // Get the existing source map from previous transforms
201
+ // Get the existing source map from previous transforms in the pipeline
131
202
const existingMap = this . getCombinedSourcemap ( ) ;
132
203
133
204
let finalMap = magicStringMap ;
134
205
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
136
208
if ( existingMap && existingMap . mappings && existingMap . mappings !== '' ) {
137
209
try {
138
210
// Create consumers for both source maps
@@ -145,11 +217,12 @@ export function comlink(): Plugin[] {
145
217
146
218
finalMap = generator . toJSON ( ) as any ;
147
219
148
- // Clean up consumers
220
+ // Clean up consumers to prevent memory leaks
149
221
existingConsumer . destroy ( ) ;
150
222
newConsumer . destroy ( ) ;
151
223
} 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
153
226
console . warn ( 'Failed to combine source maps:' , error ) ;
154
227
finalMap = magicStringMap ;
155
228
}
@@ -164,4 +237,5 @@ export function comlink(): Plugin[] {
164
237
] ;
165
238
}
166
239
240
+ // Export as default for convenience
167
241
export default comlink ;
0 commit comments