mirror of
https://github.com/oven-sh/bun
synced 2026-02-19 07:12:24 +00:00
Compare commits
4 Commits
claude/ref
...
claude/pre
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74cfdcdd19 | ||
|
|
85fd7f80cb | ||
|
|
022e1a2993 | ||
|
|
ae666a4657 |
@@ -81,6 +81,7 @@ export default [
|
||||
noConstructor: true,
|
||||
finalize: true,
|
||||
configurable: false,
|
||||
hasPendingActivity: true,
|
||||
klass: {},
|
||||
JSType: "0b11101110",
|
||||
proto: {
|
||||
|
||||
@@ -5967,7 +5967,7 @@ pub const NodeFS = struct {
|
||||
|
||||
pub fn watch(_: *NodeFS, args: Arguments.Watch, _: Flavor) Maybe(Return.Watch) {
|
||||
return switch (args.createFSWatcher()) {
|
||||
.result => |result| .{ .result = result.this_value.tryGet() orelse .zero },
|
||||
.result => |result| .{ .result = result.js_this },
|
||||
.err => |err| .{ .err = err },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,14 +17,14 @@ pub const FSWatcher = struct {
|
||||
path_watcher: ?*PathWatcher.PathWatcher,
|
||||
poll_ref: Async.KeepAlive = .{},
|
||||
globalThis: *jsc.JSGlobalObject,
|
||||
this_value: jsc.JSRef = jsc.JSRef.empty(),
|
||||
js_this: jsc.JSValue,
|
||||
encoding: jsc.Node.Encoding,
|
||||
|
||||
/// User can call close and pre-detach so we need to track this
|
||||
closed: bool,
|
||||
|
||||
/// Task reference count - protected by mutex
|
||||
task_count: u32 = 0,
|
||||
/// While it's not closed, the pending activity
|
||||
pending_activity_count: std.atomic.Value(u32) = std.atomic.Value(u32).init(1),
|
||||
current_task: FSWatchTask = undefined,
|
||||
|
||||
pub fn eventLoop(this: FSWatcher) *EventLoop {
|
||||
@@ -41,10 +41,7 @@ pub const FSWatcher = struct {
|
||||
bun.destroy(this);
|
||||
}
|
||||
|
||||
pub fn finalize(this: *FSWatcher) void {
|
||||
this.this_value.finalize();
|
||||
this.deinit();
|
||||
}
|
||||
pub const finalize = deinit;
|
||||
|
||||
pub const FSWatchTask = if (Environment.isWindows) FSWatchTaskWindows else FSWatchTaskPosix;
|
||||
pub const FSWatchTaskPosix = struct {
|
||||
@@ -423,18 +420,12 @@ pub const FSWatcher = struct {
|
||||
pub fn initJS(this: *FSWatcher, listener: jsc.JSValue) void {
|
||||
if (this.persistent) {
|
||||
this.poll_ref.ref(this.ctx);
|
||||
_ = this.pending_activity_count.fetchAdd(1, .monotonic);
|
||||
}
|
||||
|
||||
const js_this = this.toJS(this.globalThis);
|
||||
js_this.ensureStillAlive();
|
||||
this.this_value.setWeak(js_this);
|
||||
|
||||
// Set initial task count to 1 (representing the watcher being active)
|
||||
this.mutex.lock();
|
||||
this.task_count = 1;
|
||||
this.updateHasPendingActivityLocked();
|
||||
this.mutex.unlock();
|
||||
|
||||
this.js_this = js_this;
|
||||
js.listenerSetCached(js_this, this.globalThis, listener);
|
||||
|
||||
if (this.signal) |s| {
|
||||
@@ -463,13 +454,13 @@ pub const FSWatcher = struct {
|
||||
|
||||
pub fn emitAbort(this: *FSWatcher, err: jsc.JSValue) void {
|
||||
if (this.closed) return;
|
||||
_ = this.refTask();
|
||||
_ = this.pending_activity_count.fetchAdd(1, .monotonic);
|
||||
defer this.close();
|
||||
defer this.unrefTask();
|
||||
|
||||
err.ensureStillAlive();
|
||||
const js_this = this.this_value.tryGet() orelse .zero;
|
||||
if (js_this != .zero) {
|
||||
if (this.js_this != .zero) {
|
||||
const js_this = this.js_this;
|
||||
js_this.ensureStillAlive();
|
||||
if (js.listenerGetCached(js_this)) |listener| {
|
||||
listener.ensureStillAlive();
|
||||
@@ -488,8 +479,8 @@ pub const FSWatcher = struct {
|
||||
if (this.closed) return;
|
||||
defer this.close();
|
||||
|
||||
const js_this = this.this_value.tryGet() orelse .zero;
|
||||
if (js_this != .zero) {
|
||||
if (this.js_this != .zero) {
|
||||
const js_this = this.js_this;
|
||||
js_this.ensureStillAlive();
|
||||
if (js.listenerGetCached(js_this)) |listener| {
|
||||
listener.ensureStillAlive();
|
||||
@@ -507,7 +498,7 @@ pub const FSWatcher = struct {
|
||||
}
|
||||
|
||||
pub fn emitWithFilename(this: *FSWatcher, file_name: jsc.JSValue, comptime eventType: EventType) void {
|
||||
const js_this = this.this_value.tryGet() orelse .zero;
|
||||
const js_this = this.js_this;
|
||||
if (js_this == .zero) return;
|
||||
const listener = js.listenerGetCached(js_this) orelse return;
|
||||
emitJS(listener, this.globalThis, file_name, eventType);
|
||||
@@ -515,7 +506,7 @@ pub const FSWatcher = struct {
|
||||
|
||||
pub fn emit(this: *FSWatcher, file_name: string, comptime event_type: EventType) void {
|
||||
bun.assert(event_type != .@"error");
|
||||
const js_this = this.this_value.tryGet() orelse .zero;
|
||||
const js_this = this.js_this;
|
||||
if (js_this == .zero) return;
|
||||
const listener = js.listenerGetCached(js_this) orelse return;
|
||||
const globalObject = this.globalThis;
|
||||
@@ -571,46 +562,30 @@ pub const FSWatcher = struct {
|
||||
this.mutex.lock();
|
||||
defer this.mutex.unlock();
|
||||
if (this.closed) return false;
|
||||
this.task_count += 1;
|
||||
this.updateHasPendingActivityLocked();
|
||||
_ = this.pending_activity_count.fetchAdd(1, .monotonic);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn computeHasPendingActivity(this: *const FSWatcher) bool {
|
||||
return this.task_count > 0;
|
||||
}
|
||||
|
||||
pub fn updateHasPendingActivity(this: *FSWatcher) void {
|
||||
this.mutex.lock();
|
||||
defer this.mutex.unlock();
|
||||
this.updateHasPendingActivityLocked();
|
||||
}
|
||||
|
||||
fn updateHasPendingActivityLocked(this: *FSWatcher) void {
|
||||
const has_pending = this.computeHasPendingActivity();
|
||||
if (has_pending) {
|
||||
this.this_value.upgrade(this.globalThis);
|
||||
} else {
|
||||
this.this_value.downgrade();
|
||||
}
|
||||
pub fn hasPendingActivity(this: *FSWatcher) bool {
|
||||
return this.pending_activity_count.load(.acquire) > 0;
|
||||
}
|
||||
|
||||
pub fn unrefTask(this: *FSWatcher) void {
|
||||
this.mutex.lock();
|
||||
defer this.mutex.unlock();
|
||||
if (this.task_count == 0) return; // Already at 0, nothing to unref
|
||||
this.task_count -= 1;
|
||||
this.updateHasPendingActivityLocked();
|
||||
// JSC eventually will free it
|
||||
_ = this.pending_activity_count.fetchSub(1, .monotonic);
|
||||
}
|
||||
|
||||
pub fn close(this: *FSWatcher) void {
|
||||
this.mutex.lock();
|
||||
if (!this.closed) {
|
||||
this.closed = true;
|
||||
const js_this = this.js_this;
|
||||
this.mutex.unlock();
|
||||
this.detach();
|
||||
|
||||
const js_this = this.this_value.tryGet() orelse .zero;
|
||||
if (js_this != .zero) {
|
||||
if (FSWatcher.js.listenerGetCached(js_this)) |listener| {
|
||||
_ = this.refTask();
|
||||
@@ -642,6 +617,8 @@ pub const FSWatcher = struct {
|
||||
this.signal = null;
|
||||
signal.detach(this);
|
||||
}
|
||||
|
||||
this.js_this = .zero;
|
||||
}
|
||||
|
||||
pub fn doClose(this: *FSWatcher, _: *jsc.JSGlobalObject, _: *jsc.CallFrame) bun.JSError!jsc.JSValue {
|
||||
@@ -692,10 +669,9 @@ pub const FSWatcher = struct {
|
||||
.persistent = args.persistent,
|
||||
.path_watcher = null,
|
||||
.globalThis = args.global_this,
|
||||
.this_value = jsc.JSRef.empty(),
|
||||
.js_this = .zero,
|
||||
.encoding = args.encoding,
|
||||
.closed = false,
|
||||
.task_count = 0,
|
||||
.verbose = args.verbose,
|
||||
});
|
||||
ctx.current_task.ctx = ctx;
|
||||
|
||||
@@ -49,6 +49,10 @@ fn generateCompileResultForHTMLChunkImpl(worker: *ThreadPool.Worker, c: *LinkerC
|
||||
html: ?u32 = 0,
|
||||
},
|
||||
added_head_tags: bool,
|
||||
/// Track where we found script tags: null = not found, false = in head, true = in body
|
||||
script_in_body: ?bool = null,
|
||||
/// Track which section we're currently in
|
||||
current_section: enum { none, head, body } = .none,
|
||||
|
||||
pub fn onWriteHTML(this: *@This(), bytes: []const u8) void {
|
||||
bun.handleOom(this.output.appendSlice(bytes));
|
||||
@@ -58,7 +62,7 @@ fn generateCompileResultForHTMLChunkImpl(worker: *ThreadPool.Worker, c: *LinkerC
|
||||
Output.panic("Parsing HTML during replacement phase errored, which should never happen since the first pass succeeded: {s}", .{err});
|
||||
}
|
||||
|
||||
pub fn onTag(this: *@This(), element: *lol.Element, _: []const u8, url_attribute: []const u8, _: ImportKind) void {
|
||||
pub fn onTag(this: *@This(), element: *lol.Element, _: []const u8, url_attribute: []const u8, kind: ImportKind) void {
|
||||
if (this.current_import_record_index >= this.import_records.len) {
|
||||
Output.panic("Assertion failure in HTMLLoader.onTag: current_import_record_index ({d}) >= import_records.len ({d})", .{ this.current_import_record_index, this.import_records.len });
|
||||
}
|
||||
@@ -74,6 +78,13 @@ fn generateCompileResultForHTMLChunkImpl(worker: *ThreadPool.Worker, c: *LinkerC
|
||||
else
|
||||
.file;
|
||||
|
||||
// Track if this is a script tag and where it's located
|
||||
const is_script = kind == .stmt and loader.isJavaScriptLike();
|
||||
if (is_script and this.script_in_body == null) {
|
||||
// First script tag - record its location
|
||||
this.script_in_body = (this.current_section == .body);
|
||||
}
|
||||
|
||||
if (import_record.is_external_without_side_effects) {
|
||||
debug("Leaving external import: {s}", .{import_record.path.text});
|
||||
return;
|
||||
@@ -114,6 +125,7 @@ fn generateCompileResultForHTMLChunkImpl(worker: *ThreadPool.Worker, c: *LinkerC
|
||||
}
|
||||
|
||||
pub fn onHeadTag(this: *@This(), element: *lol.Element) bool {
|
||||
this.current_section = .head;
|
||||
element.onEndTag(endHeadTagHandler, this) catch return true;
|
||||
return false;
|
||||
}
|
||||
@@ -124,6 +136,7 @@ fn generateCompileResultForHTMLChunkImpl(worker: *ThreadPool.Worker, c: *LinkerC
|
||||
}
|
||||
|
||||
pub fn onBodyTag(this: *@This(), element: *lol.Element) bool {
|
||||
this.current_section = .body;
|
||||
element.onEndTag(endBodyTagHandler, this) catch return true;
|
||||
return false;
|
||||
}
|
||||
@@ -150,7 +163,7 @@ fn generateCompileResultForHTMLChunkImpl(worker: *ThreadPool.Worker, c: *LinkerC
|
||||
array.appendAssumeCapacity(link_tag);
|
||||
}
|
||||
if (this.chunk.getJSChunkForHTML(this.chunks)) |js_chunk| {
|
||||
// type="module" scripts do not block rendering, so it is okay to put them in head
|
||||
// type="module" scripts do not block rendering, placement is determined by original script location
|
||||
const script = bun.handleOom(std.fmt.allocPrintZ(allocator, "<script type=\"module\" crossorigin src=\"{s}\"></script>", .{js_chunk.unique_key}));
|
||||
array.appendAssumeCapacity(script);
|
||||
}
|
||||
@@ -160,7 +173,14 @@ fn generateCompileResultForHTMLChunkImpl(worker: *ThreadPool.Worker, c: *LinkerC
|
||||
fn endHeadTagHandler(end: *lol.EndTag, opaque_this: ?*anyopaque) callconv(.C) lol.Directive {
|
||||
const this: *@This() = @alignCast(@ptrCast(opaque_this.?));
|
||||
if (this.linker.dev_server == null) {
|
||||
this.addHeadTags(end) catch return .stop;
|
||||
// Only inject if scripts were explicitly found in head (script_in_body == false)
|
||||
// If script_in_body is null, we haven't seen any scripts yet, so defer injection
|
||||
if (this.script_in_body) |in_body| {
|
||||
if (!in_body) {
|
||||
// Scripts were in head, inject here
|
||||
this.addHeadTags(end) catch return .stop;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.end_tag_indices.head = @intCast(this.output.items.len);
|
||||
}
|
||||
@@ -170,20 +190,27 @@ fn generateCompileResultForHTMLChunkImpl(worker: *ThreadPool.Worker, c: *LinkerC
|
||||
fn endBodyTagHandler(end: *lol.EndTag, opaque_this: ?*anyopaque) callconv(.C) lol.Directive {
|
||||
const this: *@This() = @alignCast(@ptrCast(opaque_this.?));
|
||||
if (this.linker.dev_server == null) {
|
||||
this.addHeadTags(end) catch return .stop;
|
||||
// Only inject if scripts were explicitly found in body (script_in_body == true)
|
||||
// If script_in_body is null, we haven't seen any scripts yet, defer to html tag fallback
|
||||
if (this.script_in_body) |in_body| {
|
||||
if (in_body) {
|
||||
// Scripts were in body, inject here
|
||||
this.addHeadTags(end) catch return .stop;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.end_tag_indices.body = @intCast(this.output.items.len);
|
||||
}
|
||||
return .@"continue";
|
||||
}
|
||||
|
||||
fn endHtmlTagHandler(end: *lol.EndTag, opaque_this: ?*anyopaque) callconv(.C) lol.Directive {
|
||||
fn endHtmlTagHandler(_: *lol.EndTag, opaque_this: ?*anyopaque) callconv(.C) lol.Directive {
|
||||
const this: *@This() = @alignCast(@ptrCast(opaque_this.?));
|
||||
if (this.linker.dev_server == null) {
|
||||
this.addHeadTags(end) catch return .stop;
|
||||
} else {
|
||||
if (this.linker.dev_server != null) {
|
||||
this.end_tag_indices.html = @intCast(this.output.items.len);
|
||||
}
|
||||
// For production bundling, don't inject here - let post-processing handle it
|
||||
// so we can search for </head> and inject there for HTML with no scripts
|
||||
return .@"continue";
|
||||
}
|
||||
};
|
||||
@@ -216,17 +243,17 @@ fn generateCompileResultForHTMLChunkImpl(worker: *ThreadPool.Worker, c: *LinkerC
|
||||
sources[chunk.entry_point.source_index].contents,
|
||||
) catch std.debug.panic("unexpected error from HTMLProcessor.run", .{});
|
||||
|
||||
// There are some cases where invalid HTML will make it so </head> is
|
||||
// There are some cases where invalid HTML will make it so the end tag is
|
||||
// never emitted, even if the literal text DOES appear. These cases are
|
||||
// along the lines of having a self-closing tag for a non-self closing
|
||||
// element. In this case, head_end_tag_index will be 0, and a simple
|
||||
// search through the page is done to find the "</head>"
|
||||
// element. In this case, we do a simple search through the page.
|
||||
// See https://github.com/oven-sh/bun/issues/17554
|
||||
const script_injection_offset: u32 = if (c.dev_server != null) brk: {
|
||||
if (html_loader.end_tag_indices.head) |head|
|
||||
break :brk head;
|
||||
if (bun.strings.indexOf(html_loader.output.items, "</head>")) |head|
|
||||
break :brk @intCast(head);
|
||||
// Dev server logic - try head first, then body, then html, then end of file
|
||||
if (html_loader.end_tag_indices.head) |idx|
|
||||
break :brk idx;
|
||||
if (bun.strings.indexOf(html_loader.output.items, "</head>")) |idx|
|
||||
break :brk @intCast(idx);
|
||||
if (html_loader.end_tag_indices.body) |body|
|
||||
break :brk body;
|
||||
if (html_loader.end_tag_indices.html) |html|
|
||||
@@ -234,13 +261,45 @@ fn generateCompileResultForHTMLChunkImpl(worker: *ThreadPool.Worker, c: *LinkerC
|
||||
break :brk @intCast(html_loader.output.items.len); // inject at end of file.
|
||||
} else brk: {
|
||||
if (!html_loader.added_head_tags) {
|
||||
@branchHint(.cold); // this is if the document is missing all head, body, and html elements.
|
||||
var html_appender = std.heap.stackFallback(256, bun.default_allocator);
|
||||
const allocator = html_appender.get();
|
||||
const slices = html_loader.getHeadTags(allocator);
|
||||
for (slices.slice()) |slice| {
|
||||
bun.handleOom(html_loader.output.appendSlice(slice));
|
||||
allocator.free(slice);
|
||||
// If we never injected during parsing, try to inject at </head> position
|
||||
// This happens when the HTML has no scripts in the source
|
||||
if (bun.strings.indexOf(html_loader.output.items, "</head>")) |head_idx| {
|
||||
// Found </head>, insert before it
|
||||
var html_appender = std.heap.stackFallback(256, bun.default_allocator);
|
||||
const allocator = html_appender.get();
|
||||
const slices = html_loader.getHeadTags(allocator);
|
||||
defer for (slices.slice()) |slice|
|
||||
allocator.free(slice);
|
||||
|
||||
// Calculate total size needed for inserted tags
|
||||
var total_insert_size: usize = 0;
|
||||
for (slices.slice()) |slice|
|
||||
total_insert_size += slice.len;
|
||||
|
||||
// Make room for the tags before </head>
|
||||
const old_len = html_loader.output.items.len;
|
||||
bun.handleOom(html_loader.output.resize(old_len + total_insert_size));
|
||||
|
||||
// Move everything after </head> to make room
|
||||
const items = html_loader.output.items;
|
||||
std.mem.copyBackwards(u8, items[head_idx + total_insert_size .. items.len], items[head_idx..old_len]);
|
||||
|
||||
// Insert the tags
|
||||
var offset: usize = head_idx;
|
||||
for (slices.slice()) |slice| {
|
||||
@memcpy(items[offset .. offset + slice.len], slice);
|
||||
offset += slice.len;
|
||||
}
|
||||
} else {
|
||||
@branchHint(.cold); // this is if the document is missing all head, body, and html elements.
|
||||
// No </head> tag found - fallback to appending at end
|
||||
var html_appender = std.heap.stackFallback(256, bun.default_allocator);
|
||||
const allocator = html_appender.get();
|
||||
const slices = html_loader.getHeadTags(allocator);
|
||||
for (slices.slice()) |slice| {
|
||||
bun.handleOom(html_loader.output.appendSlice(slice));
|
||||
allocator.free(slice);
|
||||
}
|
||||
}
|
||||
}
|
||||
break :brk if (Environment.isDebug) undefined else 0; // value is ignored. fail loud if hit in debug
|
||||
|
||||
@@ -843,4 +843,78 @@ body {
|
||||
api.expectFile("out/" + jsFile).toContain("sourceMappingURL");
|
||||
},
|
||||
});
|
||||
|
||||
// Test script tags in body are preserved in body
|
||||
itBundled("html/script-in-body", {
|
||||
outdir: "out/",
|
||||
files: {
|
||||
"/index.html": `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="./styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello World</h1>
|
||||
<script src="./script.js"></script>
|
||||
</body>
|
||||
</html>`,
|
||||
"/styles.css": "body { background-color: red; }",
|
||||
"/script.js": "console.log('Hello World')",
|
||||
},
|
||||
entryPoints: ["/index.html"],
|
||||
onAfterBundle(api) {
|
||||
const htmlContent = api.readFile("out/index.html");
|
||||
|
||||
// Check that bundled script tag is in the body (before </body>), not in head
|
||||
const bodyCloseIndex = htmlContent.indexOf("</body>");
|
||||
const headCloseIndex = htmlContent.indexOf("</head>");
|
||||
const scriptIndex = htmlContent.indexOf("<script");
|
||||
|
||||
expect(scriptIndex).toBeGreaterThan(-1);
|
||||
expect(bodyCloseIndex).toBeGreaterThan(-1);
|
||||
expect(headCloseIndex).toBeGreaterThan(-1);
|
||||
|
||||
// Script should come after head close and before body close
|
||||
expect(scriptIndex).toBeGreaterThan(headCloseIndex);
|
||||
expect(scriptIndex).toBeLessThan(bodyCloseIndex);
|
||||
},
|
||||
});
|
||||
|
||||
// Test script tags in head are preserved in head
|
||||
itBundled("html/script-in-head", {
|
||||
outdir: "out/",
|
||||
files: {
|
||||
"/index.html": `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="./styles.css">
|
||||
<script src="./script.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello World</h1>
|
||||
</body>
|
||||
</html>`,
|
||||
"/styles.css": "body { background-color: blue; }",
|
||||
"/script.js": "console.log('Script in head')",
|
||||
},
|
||||
entryPoints: ["/index.html"],
|
||||
onAfterBundle(api) {
|
||||
const htmlContent = api.readFile("out/index.html");
|
||||
|
||||
// Check that bundled script tag is in the head (before </head>)
|
||||
const bodyCloseIndex = htmlContent.indexOf("</body>");
|
||||
const headCloseIndex = htmlContent.indexOf("</head>");
|
||||
const scriptIndex = htmlContent.indexOf("<script");
|
||||
|
||||
expect(scriptIndex).toBeGreaterThan(-1);
|
||||
expect(bodyCloseIndex).toBeGreaterThan(-1);
|
||||
expect(headCloseIndex).toBeGreaterThan(-1);
|
||||
|
||||
// Script should come before head close and before body close
|
||||
expect(scriptIndex).toBeLessThan(headCloseIndex);
|
||||
expect(scriptIndex).toBeLessThan(bodyCloseIndex);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user