@@ -3,21 +3,47 @@ import MagicString from "magic-string";
33import { Plugin , normalizePath } from "vite" ;
44import { SourceMapConsumer , SourceMapGenerator } from "source-map" ;
55
6+ // Template string to avoid static analysis issues with import.meta.url
67const importMetaUrl = `${ "import" } .meta.url` ;
8+
9+ // Virtual module prefixes for identifying Comlink worker modules
710const urlPrefix_normal = "internal:comlink:" ;
811const 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
1015let mode = "" ;
1116let 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+ */
1326export 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 / ( \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;
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
167241export default comlink ;
0 commit comments