Skip to content

Commit 376b1b4

Browse files
Support preload option in Worker (#15045)
1 parent 27067d2 commit 376b1b4

10 files changed

Lines changed: 237 additions & 82 deletions

File tree

packages/bun-types/globals.d.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -595,8 +595,16 @@ declare global {
595595
* @default true
596596
*/
597597
// trackUnmanagedFds?: boolean;
598-
599598
// resourceLimits?: import("worker_threads").ResourceLimits;
599+
600+
/**
601+
* An array of module specifiers to preload in the worker.
602+
*
603+
* These modules load before the worker's entry point is executed.
604+
*
605+
* Equivalent to passing the `--preload` CLI argument, but only for this Worker.
606+
*/
607+
preload?: string[] | string | undefined;
600608
}
601609

602610
interface Worker extends EventTarget, AbstractWorker {

src/bun.js/bindings/webcore/JSWorker.cpp

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,30 @@ template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSWorkerDOMConstructor::
148148
RETURN_IF_EXCEPTION(throwScope, {});
149149
}
150150

151+
if (auto preloadModulesValue = optionsObject->getIfPropertyExists(lexicalGlobalObject, Identifier::fromString(vm, "preload"_s))) {
152+
if (!preloadModulesValue.isUndefinedOrNull()) {
153+
if (preloadModulesValue.isString()) {
154+
auto str = preloadModulesValue.toWTFString(lexicalGlobalObject);
155+
RETURN_IF_EXCEPTION(throwScope, {});
156+
if (!str.isEmpty()) {
157+
options.bun.preloadModules.append(str);
158+
}
159+
} else if (auto* array = jsDynamicCast<JSC::JSArray*>(preloadModulesValue)) {
160+
std::optional<Vector<String>> seq = convert<IDLSequence<IDLDOMString>>(*lexicalGlobalObject, array);
161+
RETURN_IF_EXCEPTION(throwScope, {});
162+
if (seq) {
163+
options.bun.preloadModules = WTFMove(*seq);
164+
options.bun.preloadModules.removeAllMatching([](const String& str) {
165+
return str.isEmpty();
166+
});
167+
}
168+
} else {
169+
throwVMError(lexicalGlobalObject, throwScope, "preload must be an array or string"_s);
170+
return encodedJSValue();
171+
}
172+
}
173+
}
174+
151175
auto workerData = optionsObject->getIfPropertyExists(lexicalGlobalObject, Identifier::fromString(vm, "workerData"_s));
152176
if (!workerData) {
153177
workerData = optionsObject->getIfPropertyExists(lexicalGlobalObject, Identifier::fromString(vm, "data"_s));

src/bun.js/bindings/webcore/Worker.cpp

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,9 @@ extern "C" void* WebWorker__create(
121121
StringImpl* argvPtr,
122122
uint32_t argvLen,
123123
StringImpl* execArgvPtr,
124-
uint32_t execArgvLen);
124+
uint32_t execArgvLen,
125+
BunString* preloadModulesPtr,
126+
uint32_t preloadModulesLen);
125127
extern "C" void WebWorker__setRef(
126128
void* worker,
127129
bool ref);
@@ -149,7 +151,12 @@ ExceptionOr<Ref<Worker>> Worker::create(ScriptExecutionContext& context, const S
149151

150152
WTF::String url = urlInit;
151153
if (url.startsWith("file://"_s)) {
152-
url = WTF::URL(url).fileSystemPath();
154+
WTF::URL urlObject = WTF::URL(url);
155+
if (urlObject.isValid()) {
156+
url = urlObject.fileSystemPath();
157+
} else {
158+
return Exception { TypeError, makeString("Invalid file URL: \""_s, urlInit, '"') };
159+
}
153160
}
154161
BunString urlStr = Bun::toString(url);
155162
BunString errorMessage = BunStringEmpty;
@@ -160,6 +167,20 @@ ExceptionOr<Ref<Worker>> Worker::create(ScriptExecutionContext& context, const S
160167

161168
Vector<String>* argv = worker->m_options.bun.argv.get();
162169
Vector<String>* execArgv = worker->m_options.bun.execArgv.get();
170+
Vector<String>* preloadModuleStrings = &worker->m_options.bun.preloadModules;
171+
Vector<BunString> preloadModules;
172+
preloadModules.reserveInitialCapacity(preloadModuleStrings->size());
173+
for (auto& str : *preloadModuleStrings) {
174+
if (str.startsWith("file://"_s)) {
175+
WTF::URL urlObject = WTF::URL(str);
176+
if (!urlObject.isValid()) {
177+
return Exception { TypeError, makeString("Invalid file URL: \""_s, str, '"') };
178+
}
179+
str = urlObject.fileSystemPath();
180+
}
181+
182+
preloadModules.append(Bun::toString(str));
183+
}
163184

164185
void* impl = WebWorker__create(
165186
worker.ptr(),
@@ -174,7 +195,11 @@ ExceptionOr<Ref<Worker>> Worker::create(ScriptExecutionContext& context, const S
174195
argv ? reinterpret_cast<StringImpl*>(argv->data()) : nullptr,
175196
argv ? static_cast<uint32_t>(argv->size()) : 0,
176197
execArgv ? reinterpret_cast<StringImpl*>(execArgv->data()) : nullptr,
177-
execArgv ? static_cast<uint32_t>(execArgv->size()) : 0);
198+
execArgv ? static_cast<uint32_t>(execArgv->size()) : 0,
199+
preloadModules.size() ? preloadModules.data() : nullptr,
200+
static_cast<uint32_t>(preloadModules.size()));
201+
202+
preloadModuleStrings->clear();
178203

179204
if (!impl) {
180205
return Exception { TypeError, errorMessage.toWTFString(BunString::ZeroCopy) };

src/bun.js/bindings/webcore/WorkerOptions.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ struct BunOptions {
1212
bool unref { false };
1313
RefPtr<SerializedScriptValue> data;
1414
Vector<TransferredMessagePort> dataMessagePorts;
15+
Vector<String> preloadModules;
1516
std::unique_ptr<HashMap<String, String>> env { nullptr };
1617
std::unique_ptr<Vector<String>> argv { nullptr };
1718
std::unique_ptr<Vector<String>> execArgv { nullptr };

src/bun.js/web_worker.zig

Lines changed: 116 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ pub const WebWorker = struct {
2222

2323
/// Already resolved.
2424
specifier: []const u8 = "",
25+
preloads: [][]const u8 = &.{},
2526
store_fd: bool = false,
2627
arena: ?bun.MimallocArena = null,
2728
name: [:0]const u8 = "Worker",
@@ -76,6 +77,94 @@ pub const WebWorker = struct {
7677
return true;
7778
}
7879

80+
fn resolveEntryPointSpecifier(
81+
parent: *JSC.VirtualMachine,
82+
str: []const u8,
83+
error_message: *bun.String,
84+
logger: *bun.logger.Log,
85+
) ?[]const u8 {
86+
if (parent.standalone_module_graph) |graph| {
87+
if (graph.find(str) != null) {
88+
return str;
89+
}
90+
91+
// Since `bun build --compile` renames files to `.js` by
92+
// default, we need to do the reverse of our file extension
93+
// mapping.
94+
//
95+
// new Worker("./foo") -> new Worker("./foo.js")
96+
// new Worker("./foo.ts") -> new Worker("./foo.js")
97+
// new Worker("./foo.jsx") -> new Worker("./foo.js")
98+
// new Worker("./foo.mjs") -> new Worker("./foo.js")
99+
// new Worker("./foo.mts") -> new Worker("./foo.js")
100+
// new Worker("./foo.cjs") -> new Worker("./foo.js")
101+
// new Worker("./foo.cts") -> new Worker("./foo.js")
102+
// new Worker("./foo.tsx") -> new Worker("./foo.js")
103+
//
104+
if (bun.strings.hasPrefixComptime(str, "./") or bun.strings.hasPrefixComptime(str, "../")) try_from_extension: {
105+
var pathbuf: bun.PathBuffer = undefined;
106+
var base = str;
107+
108+
base = bun.path.joinAbsStringBuf(bun.StandaloneModuleGraph.base_public_path_with_default_suffix, &pathbuf, &.{str}, .loose);
109+
const extname = std.fs.path.extension(base);
110+
111+
// ./foo -> ./foo.js
112+
if (extname.len == 0) {
113+
pathbuf[base.len..][0..3].* = ".js".*;
114+
if (graph.find(pathbuf[0 .. base.len + 3])) |js_file| {
115+
return js_file.name;
116+
}
117+
118+
break :try_from_extension;
119+
}
120+
121+
// ./foo.ts -> ./foo.js
122+
if (bun.strings.eqlComptime(extname, ".ts")) {
123+
pathbuf[base.len - 3 .. base.len][0..3].* = ".js".*;
124+
if (graph.find(pathbuf[0..base.len])) |js_file| {
125+
return js_file.name;
126+
}
127+
128+
break :try_from_extension;
129+
}
130+
131+
if (extname.len == 4) {
132+
inline for (.{ ".tsx", ".jsx", ".mjs", ".mts", ".cts", ".cjs" }) |ext| {
133+
if (bun.strings.eqlComptime(extname, ext)) {
134+
pathbuf[base.len - ext.len ..][0..".js".len].* = ".js".*;
135+
const as_js = pathbuf[0 .. base.len - ext.len + ".js".len];
136+
if (graph.find(as_js)) |js_file| {
137+
return js_file.name;
138+
}
139+
break :try_from_extension;
140+
}
141+
}
142+
}
143+
}
144+
}
145+
146+
if (JSC.WebCore.ObjectURLRegistry.isBlobURL(str)) {
147+
if (JSC.WebCore.ObjectURLRegistry.singleton().has(str["blob:".len..])) {
148+
return str;
149+
} else {
150+
error_message.* = bun.String.static("Blob URL is missing");
151+
return null;
152+
}
153+
}
154+
155+
var resolved_entry_point: bun.resolver.Result = parent.bundler.resolveEntryPoint(str) catch {
156+
const out = logger.toJS(parent.global, bun.default_allocator, "Error resolving Worker entry point").toBunString(parent.global);
157+
error_message.* = out;
158+
return null;
159+
};
160+
161+
const entry_path: *bun.fs.Path = resolved_entry_point.path() orelse {
162+
error_message.* = bun.String.static("Worker entry point is missing");
163+
return null;
164+
};
165+
return entry_path.text;
166+
}
167+
79168
pub fn create(
80169
cpp_worker: *void,
81170
parent: *JSC.VirtualMachine,
@@ -90,6 +179,8 @@ pub const WebWorker = struct {
90179
argv_len: u32,
91180
execArgv_ptr: ?[*]WTFStringImpl,
92181
execArgv_len: u32,
182+
preload_modules_ptr: ?[*]bun.String,
183+
preload_modules_len: u32,
93184
) callconv(.C) ?*WebWorker {
94185
JSC.markBinding(@src());
95186
log("[{d}] WebWorker.create", .{this_context_id});
@@ -101,89 +192,31 @@ pub const WebWorker = struct {
101192
defer parent.bundler.setLog(prev_log);
102193
defer temp_log.deinit();
103194

104-
const path = brk: {
105-
const str = spec_slice.slice();
106-
if (parent.standalone_module_graph) |graph| {
107-
if (graph.find(str) != null) {
108-
break :brk str;
109-
}
195+
const preload_modules = if (preload_modules_ptr) |ptr|
196+
ptr[0..preload_modules_len]
197+
else
198+
&.{};
110199

111-
// Since `bun build --compile` renames files to `.js` by
112-
// default, we need to do the reverse of our file extension
113-
// mapping.
114-
//
115-
// new Worker("./foo") -> new Worker("./foo.js")
116-
// new Worker("./foo.ts") -> new Worker("./foo.js")
117-
// new Worker("./foo.jsx") -> new Worker("./foo.js")
118-
// new Worker("./foo.mjs") -> new Worker("./foo.js")
119-
// new Worker("./foo.mts") -> new Worker("./foo.js")
120-
// new Worker("./foo.cjs") -> new Worker("./foo.js")
121-
// new Worker("./foo.cts") -> new Worker("./foo.js")
122-
// new Worker("./foo.tsx") -> new Worker("./foo.js")
123-
//
124-
if (bun.strings.hasPrefixComptime(str, "./") or bun.strings.hasPrefixComptime(str, "../")) try_from_extension: {
125-
var pathbuf: bun.PathBuffer = undefined;
126-
var base = str;
127-
128-
base = bun.path.joinAbsStringBuf(bun.StandaloneModuleGraph.base_public_path_with_default_suffix, &pathbuf, &.{str}, .loose);
129-
const extname = std.fs.path.extension(base);
130-
131-
// ./foo -> ./foo.js
132-
if (extname.len == 0) {
133-
pathbuf[base.len..][0..3].* = ".js".*;
134-
if (graph.find(pathbuf[0 .. base.len + 3])) |js_file| {
135-
break :brk js_file.name;
136-
}
137-
138-
break :try_from_extension;
139-
}
140-
141-
// ./foo.ts -> ./foo.js
142-
if (bun.strings.eqlComptime(extname, ".ts")) {
143-
pathbuf[base.len - 3 .. base.len][0..3].* = ".js".*;
144-
if (graph.find(pathbuf[0..base.len])) |js_file| {
145-
break :brk js_file.name;
146-
}
147-
148-
break :try_from_extension;
149-
}
200+
const path = resolveEntryPointSpecifier(parent, spec_slice.slice(), error_message, &temp_log) orelse {
201+
return null;
202+
};
150203

151-
if (extname.len == 4) {
152-
inline for (.{ ".tsx", ".jsx", ".mjs", ".mts", ".cts", ".cjs" }) |ext| {
153-
if (bun.strings.eqlComptime(extname, ext)) {
154-
pathbuf[base.len - ext.len ..][0..".js".len].* = ".js".*;
155-
const as_js = pathbuf[0 .. base.len - ext.len + ".js".len];
156-
if (graph.find(as_js)) |js_file| {
157-
break :brk js_file.name;
158-
}
159-
break :try_from_extension;
160-
}
161-
}
162-
}
163-
}
204+
var preloads = std.ArrayList([]const u8).initCapacity(bun.default_allocator, preload_modules_len) catch bun.outOfMemory();
205+
for (preload_modules) |module| {
206+
const utf8_slice = module.toUTF8(bun.default_allocator);
207+
defer utf8_slice.deinit();
208+
if (resolveEntryPointSpecifier(parent, utf8_slice.slice(), error_message, &temp_log)) |preload| {
209+
preloads.append(bun.default_allocator.dupe(u8, preload) catch bun.outOfMemory()) catch bun.outOfMemory();
164210
}
165211

166-
if (JSC.WebCore.ObjectURLRegistry.isBlobURL(str)) {
167-
if (JSC.WebCore.ObjectURLRegistry.singleton().has(str["blob:".len..])) {
168-
break :brk str;
169-
} else {
170-
error_message.* = bun.String.static("Blob URL is missing");
171-
return null;
212+
if (!error_message.isEmpty()) {
213+
for (preloads.items) |preload| {
214+
bun.default_allocator.free(preload);
172215
}
173-
}
174-
175-
var resolved_entry_point: bun.resolver.Result = parent.bundler.resolveEntryPoint(str) catch {
176-
const out = temp_log.toJS(parent.global, bun.default_allocator, "Error resolving Worker entry point").toBunString(parent.global);
177-
error_message.* = out;
178-
return null;
179-
};
180-
181-
const entry_path: *bun.fs.Path = resolved_entry_point.path() orelse {
182-
error_message.* = bun.String.static("Worker entry point is missing");
216+
preloads.deinit();
183217
return null;
184-
};
185-
break :brk entry_path.text;
186-
};
218+
}
219+
}
187220

188221
var worker = bun.default_allocator.create(WebWorker) catch bun.outOfMemory();
189222
worker.* = WebWorker{
@@ -204,6 +237,7 @@ pub const WebWorker = struct {
204237
.worker_event_loop_running = true,
205238
.argv = if (argv_ptr) |ptr| ptr[0..argv_len] else null,
206239
.execArgv = if (execArgv_ptr) |ptr| ptr[0..execArgv_len] else null,
240+
.preloads = preloads.items,
207241
};
208242

209243
worker.parent_poll_ref.ref(parent);
@@ -282,6 +316,10 @@ pub const WebWorker = struct {
282316
log("[{d}] deinit", .{this.execution_context_id});
283317
this.parent_poll_ref.unrefConcurrently(this.parent);
284318
bun.default_allocator.free(this.specifier);
319+
for (this.preloads) |preload| {
320+
bun.default_allocator.free(preload);
321+
}
322+
bun.default_allocator.free(this.preloads);
285323
bun.default_allocator.destroy(this);
286324
}
287325

@@ -355,7 +393,7 @@ pub const WebWorker = struct {
355393
var vm = this.vm.?;
356394
assert(this.status.load(.acquire) == .start);
357395
this.setStatus(.starting);
358-
396+
vm.preload = this.preloads;
359397
var promise = vm.loadEntryPointForWebWorker(this.specifier) catch {
360398
this.flushLogs();
361399
this.exitAndDeinit();

test/js/web/workers/worker-fixture-preload-2.js

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

test/js/web/workers/worker-fixture-preload-bad.js

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

test/js/web/workers/worker-fixture-preload-entry.js

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

test/js/web/workers/worker-fixture-preload.js

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

0 commit comments

Comments
 (0)