@@ -48,14 +48,92 @@ export class WorkerSpy {
4848 }
4949
5050 try {
51+ // Resolve the original script URL to absolute (so we can use it
52+ // both as a fetch URL and as a base for rewriting relative imports).
53+ let absoluteScriptUrl : string ;
54+ try {
55+ absoluteScriptUrl = new URL ( urlStr , location . href ) . href ;
56+ } catch {
57+ return new OriginalWorker ( scriptURL , options ) ;
58+ }
59+
5160 // Fetch the worker script and prepend Spector import
5261 const xhr = new XMLHttpRequest ( ) ;
53- xhr . open ( "GET" , urlStr , false ) ; // synchronous
62+ xhr . open ( "GET" , absoluteScriptUrl , false ) ; // synchronous
5463 xhr . send ( ) ;
5564
5665 if ( xhr . status === 200 ) {
57- const importLine = 'importScripts("' + WorkerSpy . workerBundleUrl + '");\n' ;
58- const modifiedScript = importLine + xhr . responseText ;
66+ // Rewrite static `importScripts(...)` calls in source so
67+ // relative / root-relative URLs work when the worker runs
68+ // from a `blob:` URL.
69+ const rewrittenSource = WorkerSpy . rewriteImportScripts ( xhr . responseText , absoluteScriptUrl ) ;
70+
71+ // Runtime guard: webpack and other bundlers compute chunk
72+ // URLs at runtime and pass them straight to importScripts.
73+ // We can't statically rewrite those, so we wrap
74+ // `self.importScripts` to resolve every URL against the
75+ // original worker location before delegating to the real
76+ // implementation. We also override `self.location` lookups
77+ // by exposing a synthetic `__SPECTOR_workerBaseUrl` that
78+ // bundlers reading `self.location.href` could pick up.
79+ const runtimeShim =
80+ "(function(){\n" +
81+ " var __spectorBase = " + JSON . stringify ( absoluteScriptUrl ) + ";\n" +
82+ " self.__SPECTOR_workerBaseUrl = __spectorBase;\n" +
83+ " function __spectorResolve(u) {\n" +
84+ " if (u == null) return u;\n" +
85+ " if (typeof u !== \"string\") {\n" +
86+ " if (u instanceof URL) return u;\n" +
87+ " try { u = String(u); } catch (e) { return u; }\n" +
88+ " }\n" +
89+ " if (/^(?:[a-z][a-z0-9+.-]*:|\\/\\/)/i.test(u)) return u;\n" +
90+ " try { return new URL(u, __spectorBase).href; }\n" +
91+ " catch (e) { return u; }\n" +
92+ " }\n" +
93+ " self.__SPECTOR_resolveWorkerUrl = __spectorResolve;\n" +
94+ " var __origImportScripts = self.importScripts;\n" +
95+ " if (typeof __origImportScripts === \"function\") {\n" +
96+ " self.importScripts = function() {\n" +
97+ " var args = new Array(arguments.length);\n" +
98+ " for (var i = 0; i < arguments.length; i++) args[i] = __spectorResolve(arguments[i]);\n" +
99+ " return __origImportScripts.apply(self, args);\n" +
100+ " };\n" +
101+ " }\n" +
102+ " var __origFetch = self.fetch;\n" +
103+ " if (typeof __origFetch === \"function\") {\n" +
104+ " self.fetch = function(input, init) {\n" +
105+ " try {\n" +
106+ " if (typeof input === \"string\") {\n" +
107+ " input = __spectorResolve(input);\n" +
108+ " } else if (input && typeof input === \"object\" && \"url\" in input && typeof Request === \"function\") {\n" +
109+ " var r = input;\n" +
110+ " var resolved = __spectorResolve(r.url);\n" +
111+ " if (resolved !== r.url) input = new Request(resolved, r);\n" +
112+ " }\n" +
113+ " } catch (e) { /* fall through with original input */ }\n" +
114+ " return __origFetch.call(self, input, init);\n" +
115+ " };\n" +
116+ " }\n" +
117+ " if (typeof XMLHttpRequest === \"function\" && XMLHttpRequest.prototype && XMLHttpRequest.prototype.open) {\n" +
118+ " var __origXhrOpen = XMLHttpRequest.prototype.open;\n" +
119+ " XMLHttpRequest.prototype.open = function(method, url) {\n" +
120+ " var args = Array.prototype.slice.call(arguments);\n" +
121+ " args[1] = __spectorResolve(url);\n" +
122+ " return __origXhrOpen.apply(this, args);\n" +
123+ " };\n" +
124+ " }\n" +
125+ " if (typeof Worker === \"function\") {\n" +
126+ " var __OrigWorker = Worker;\n" +
127+ " self.Worker = function(scriptURL, options) {\n" +
128+ " var resolved = __spectorResolve(scriptURL);\n" +
129+ " return new __OrigWorker(resolved, options);\n" +
130+ " };\n" +
131+ " self.Worker.prototype = __OrigWorker.prototype;\n" +
132+ " }\n" +
133+ "})();\n" ;
134+
135+ const spectorImport = "importScripts(" + JSON . stringify ( WorkerSpy . workerBundleUrl ) + ");\n" ;
136+ const modifiedScript = runtimeShim + spectorImport + rewrittenSource ;
59137 const blob = new Blob ( [ modifiedScript ] , { type : "application/javascript" } ) ;
60138 const blobUrl = URL . createObjectURL ( blob ) ;
61139
@@ -96,6 +174,44 @@ export class WorkerSpy {
96174 WorkerSpy . intercepting = false ;
97175 }
98176
177+ /**
178+ * Rewrite every URL passed to `importScripts(...)` in the worker source
179+ * to an absolute URL resolved against the worker's original location.
180+ *
181+ * Without this, when the worker is loaded from a `blob:` URL, calls like
182+ * `importScripts("/scripts/foo.js")` or `importScripts("./bar.js")`
183+ * resolve against the blob URL — which has no meaningful path — and the
184+ * browser throws `The URL '...' is invalid`.
185+ *
186+ * Handles single, double, and template (no-interpolation) string literals,
187+ * and multiple comma-separated arguments.
188+ */
189+ private static rewriteImportScripts ( source : string , baseUrl : string ) : string {
190+ // Match `importScripts(...)` and rewrite each string literal argument.
191+ // We deliberately keep this regex permissive — argument list is
192+ // re-tokenised inside the replacer.
193+ const importScriptsPattern = / i m p o r t S c r i p t s \s * \( ( [ ^ ) ] * ) \) / g;
194+ const stringLiteralPattern = / ( [ ' " ` ] ) ( (?: \\ .| (? ! \1) .) * ) \1/ g;
195+
196+ return source . replace ( importScriptsPattern , ( match , argList : string ) => {
197+ const rewrittenArgs = argList . replace ( stringLiteralPattern , ( _lit , quote : string , value : string ) => {
198+ // Skip rewriting if it contains any escape that suggests
199+ // template interpolation — we can't statically resolve those.
200+ if ( quote === "`" && value . indexOf ( "${" ) !== - 1 ) {
201+ return _lit ;
202+ }
203+ let resolved : string ;
204+ try {
205+ resolved = new URL ( value , baseUrl ) . href ;
206+ } catch {
207+ return _lit ;
208+ }
209+ return quote + resolved + quote ;
210+ } ) ;
211+ return "importScripts(" + rewrittenArgs + ")" ;
212+ } ) ;
213+ }
214+
99215 /** Get all Workers that were created while intercepting. */
100216 public static getSpiedWorkers ( ) : ISpiedWorker [ ] {
101217 return WorkerSpy . spiedWorkers ;
0 commit comments