Skip to content

Commit 057b80e

Browse files
committed
Fix worker scripts
1 parent ea45229 commit 057b80e

7 files changed

Lines changed: 267 additions & 10 deletions

File tree

‎dist/spector.bundle.js‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎extensions/contentScript.js‎

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -187,14 +187,92 @@ window.__SPECTOR_Canvases = [];
187187
}
188188

189189
try {
190+
// Resolve to absolute so we can fetch and use as base URL
191+
// for runtime URL rewriting inside the blob worker.
192+
var absoluteScriptUrl;
193+
try {
194+
absoluteScriptUrl = new URL(urlStr, location.href).href;
195+
} catch (e) {
196+
absoluteScriptUrl = urlStr;
197+
}
198+
190199
var xhr = new XMLHttpRequest();
191-
xhr.open('GET', urlStr, false);
200+
xhr.open('GET', absoluteScriptUrl, false);
192201
xhr.send();
193202

194203
if (xhr.status === 200) {
195204
if (bundleUrl) {
196-
var importLine = 'importScripts("' + bundleUrl + '");\n';
197-
var modifiedScript = importLine + xhr.responseText;
205+
// Runtime shim: when a worker is loaded from a blob:
206+
// URL, `self.location.href` is the blob URL — which
207+
// means every relative URL passed to importScripts,
208+
// fetch, XMLHttpRequest, or new Worker resolves against
209+
// it and breaks (root-relative URLs throw "URL is
210+
// invalid"; relative URLs become blob:scheme paths).
211+
// We wrap each of those APIs to resolve URLs against
212+
// the worker's ORIGINAL location before delegating to
213+
// the real implementation.
214+
var runtimeShim =
215+
'(function(){\n' +
216+
' var __spectorBase = ' + JSON.stringify(absoluteScriptUrl) + ';\n' +
217+
' self.__SPECTOR_workerBaseUrl = __spectorBase;\n' +
218+
' function __spectorResolve(u) {\n' +
219+
' if (u == null) return u;\n' +
220+
' if (typeof u !== "string") {\n' +
221+
' if (u instanceof URL) return u;\n' +
222+
' try { u = String(u); } catch (e) { return u; }\n' +
223+
' }\n' +
224+
' if (/^(?:[a-z][a-z0-9+.-]*:|\\/\\/)/i.test(u)) return u;\n' +
225+
' try { return new URL(u, __spectorBase).href; }\n' +
226+
' catch (e) { return u; }\n' +
227+
' }\n' +
228+
' self.__SPECTOR_resolveWorkerUrl = __spectorResolve;\n' +
229+
// importScripts (variadic)
230+
' var __origImportScripts = self.importScripts;\n' +
231+
' if (typeof __origImportScripts === "function") {\n' +
232+
' self.importScripts = function() {\n' +
233+
' var args = new Array(arguments.length);\n' +
234+
' for (var i = 0; i < arguments.length; i++) args[i] = __spectorResolve(arguments[i]);\n' +
235+
' return __origImportScripts.apply(self, args);\n' +
236+
' };\n' +
237+
' }\n' +
238+
// fetch
239+
' var __origFetch = self.fetch;\n' +
240+
' if (typeof __origFetch === "function") {\n' +
241+
' self.fetch = function(input, init) {\n' +
242+
' try {\n' +
243+
' if (typeof input === "string") {\n' +
244+
' input = __spectorResolve(input);\n' +
245+
' } else if (input && typeof input === "object" && "url" in input && typeof Request === "function") {\n' +
246+
' var r = input;\n' +
247+
' var resolved = __spectorResolve(r.url);\n' +
248+
' if (resolved !== r.url) input = new Request(resolved, r);\n' +
249+
' }\n' +
250+
' } catch (e) { /* fall through with original input */ }\n' +
251+
' return __origFetch.call(self, input, init);\n' +
252+
' };\n' +
253+
' }\n' +
254+
// XMLHttpRequest.open
255+
' if (typeof XMLHttpRequest === "function" && XMLHttpRequest.prototype && XMLHttpRequest.prototype.open) {\n' +
256+
' var __origXhrOpen = XMLHttpRequest.prototype.open;\n' +
257+
' XMLHttpRequest.prototype.open = function(method, url) {\n' +
258+
' var args = Array.prototype.slice.call(arguments);\n' +
259+
' args[1] = __spectorResolve(url);\n' +
260+
' return __origXhrOpen.apply(this, args);\n' +
261+
' };\n' +
262+
' }\n' +
263+
// Nested Worker construction (workers can spawn workers)
264+
' if (typeof Worker === "function") {\n' +
265+
' var __OrigWorker = Worker;\n' +
266+
' self.Worker = function(scriptURL, options) {\n' +
267+
' var resolved = __spectorResolve(scriptURL);\n' +
268+
' return new __OrigWorker(resolved, options);\n' +
269+
' };\n' +
270+
' self.Worker.prototype = __OrigWorker.prototype;\n' +
271+
' }\n' +
272+
'})();\n';
273+
274+
var importLine = 'importScripts(' + JSON.stringify(bundleUrl) + ');\n';
275+
var modifiedScript = runtimeShim + importLine + xhr.responseText;
198276
var blob = new Blob([modifiedScript], { type: 'application/javascript' });
199277
var blobUrl = URL.createObjectURL(blob);
200278
var w = new __SPECTOR_Origin_Worker(blobUrl, options);

‎extensions/spector.bundle.func.js‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎extensions/spector.bundle.js‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎extensions/spector.worker.bundle.js‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/backend/spies/workerSpy.ts‎

Lines changed: 119 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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 = /importScripts\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;

‎test/unit/backend/spies/workerSpy.test.ts‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,67 @@ describe("WorkerSpy", () => {
7373
WorkerSpy.startIntercepting("spector.worker.bundle.js");
7474
expect(WorkerSpy.isIntercepting()).toBe(false);
7575
});
76+
77+
describe("rewriteImportScripts", () => {
78+
// The method is private but accessible for unit testing via cast.
79+
const rewrite = (WorkerSpy as any).rewriteImportScripts.bind(WorkerSpy) as
80+
(source: string, baseUrl: string) => string;
81+
82+
const baseUrl = "http://localhost:8081/scripts/index.worker.js";
83+
84+
it("rewrites a root-relative URL to absolute", () => {
85+
const out = rewrite('importScripts("/scripts/foo.js");', baseUrl);
86+
expect(out).toBe('importScripts("http://localhost:8081/scripts/foo.js");');
87+
});
88+
89+
it("rewrites a relative URL to absolute", () => {
90+
const out = rewrite('importScripts("./foo.js");', baseUrl);
91+
expect(out).toBe('importScripts("http://localhost:8081/scripts/foo.js");');
92+
});
93+
94+
it("leaves an already-absolute URL untouched", () => {
95+
const out = rewrite('importScripts("https://cdn.example.com/lib.js");', baseUrl);
96+
expect(out).toBe('importScripts("https://cdn.example.com/lib.js");');
97+
});
98+
99+
it("rewrites multiple comma-separated URLs", () => {
100+
const out = rewrite(
101+
'importScripts("/a.js", "./b.js", "https://x/c.js");',
102+
baseUrl,
103+
);
104+
expect(out).toBe(
105+
'importScripts("http://localhost:8081/a.js", "http://localhost:8081/scripts/b.js", "https://x/c.js");',
106+
);
107+
});
108+
109+
it("supports single-quoted strings", () => {
110+
const out = rewrite("importScripts('/foo.js');", baseUrl);
111+
expect(out).toBe("importScripts('http://localhost:8081/foo.js');");
112+
});
113+
114+
it("supports template literals without interpolation", () => {
115+
const out = rewrite("importScripts(`/foo.js`);", baseUrl);
116+
expect(out).toBe("importScripts(`http://localhost:8081/foo.js`);");
117+
});
118+
119+
it("leaves template literals with interpolation untouched", () => {
120+
const src = "importScripts(`/scripts/${name}.js`);";
121+
expect(rewrite(src, baseUrl)).toBe(src);
122+
});
123+
124+
it("leaves source without importScripts unchanged", () => {
125+
const src = 'self.addEventListener("message", () => {});';
126+
expect(rewrite(src, baseUrl)).toBe(src);
127+
});
128+
129+
it("rewrites multiple importScripts calls", () => {
130+
const out = rewrite(
131+
'importScripts("/a.js");\nself.x = 1;\nimportScripts("./b.js");',
132+
baseUrl,
133+
);
134+
expect(out).toBe(
135+
'importScripts("http://localhost:8081/a.js");\nself.x = 1;\nimportScripts("http://localhost:8081/scripts/b.js");',
136+
);
137+
});
138+
});
76139
});

0 commit comments

Comments
 (0)