Compare commits

...

3 Commits

Author SHA1 Message Date
Jarred Sumner
b9af8a925a Merge branch 'main' into claude/fix-self-global-main-thread 2026-03-01 00:15:49 -08:00
autofix-ci[bot]
944e31326f [autofix.ci] apply automated fixes 2026-02-26 20:30:40 +00:00
Claude Bot
8d61f3e677 fix: don't define self global on the main thread
Many libraries use `typeof self !== "undefined"` to detect a browser
environment. Since Bun defined `self` (as an alias for `globalThis`) in
all contexts including the main thread, these browser-detection checks
incorrectly identified Bun as a browser runtime, causing issues like
fish-lsp hanging indefinitely.

Node.js intentionally does not define `self` in any context. This change
aligns with Node.js by only defining `self` in worker contexts (where it
is part of the Web Worker API spec), not on the main thread.

Closes #27476

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-26 20:28:37 +00:00
4 changed files with 106 additions and 29 deletions

View File

@@ -2742,15 +2742,21 @@ void GlobalObject::addBuiltinGlobals(JSC::VM& vm)
// ----- Public Properties -----
// a direct accessor (uses js functions for get and set) cannot be on the lookup table. i think.
putDirectAccessor(
this,
builtinNames.selfPublicName(),
JSC::GetterSetter::create(
vm,
// Only define `self` in worker contexts (not the main thread).
// Node.js does not define `self` in the main context, and many libraries use
// `typeof self !== "undefined"` to detect a browser environment.
// See: https://github.com/oven-sh/bun/issues/27476
if (!m_scriptExecutionContext->isMainThread()) {
putDirectAccessor(
this,
JSFunction::create(vm, this, 0, "get"_s, functionGetSelf, ImplementationVisibility::Public),
JSFunction::create(vm, this, 0, "set"_s, functionSetSelf, ImplementationVisibility::Public)),
PropertyAttribute::Accessor | 0);
builtinNames.selfPublicName(),
JSC::GetterSetter::create(
vm,
this,
JSFunction::create(vm, this, 0, "get"_s, functionGetSelf, ImplementationVisibility::Public),
JSFunction::create(vm, this, 0, "set"_s, functionSetSelf, ImplementationVisibility::Public)),
PropertyAttribute::Accessor | 0);
}
// TODO: this should be usable on the lookup table. it crashed las time i tried it
putDirectCustomAccessor(vm, JSC::Identifier::fromString(vm, "onmessage"_s), JSC::CustomGetterSetter::create(vm, globalOnMessage, setGlobalOnMessage), 0);

View File

@@ -190,13 +190,12 @@ it("globals are deletable", () => {
expect(exitCode).toBe(0);
});
it("self is a getter", () => {
const descriptor = Object.getOwnPropertyDescriptor(globalThis, "self");
expect(descriptor.get).toBeInstanceOf(Function);
expect(descriptor.set).toBeInstanceOf(Function);
expect(descriptor.enumerable).toBe(true);
expect(descriptor.configurable).toBe(true);
expect(globalThis.self).toBe(globalThis);
it("self is not defined on the main thread", () => {
// Node.js does not define `self` in the main context.
// Many libraries use `typeof self !== "undefined"` to detect a browser environment.
// See: https://github.com/oven-sh/bun/issues/27476
expect(globalThis.self).toBeUndefined();
expect("self" in globalThis).toBe(false);
});
it("errors thrown by native code should be TypeError", async () => {

View File

@@ -308,18 +308,9 @@ test("confirm (no) windows newline", async () => {
expect(await proc.stderr.text()).toBe("No\n");
});
test("globalThis.self = 123 works", () => {
expect(Object.getOwnPropertyDescriptor(globalThis, "self")).toMatchObject({
configurable: true,
enumerable: true,
get: expect.any(Function),
set: expect.any(Function),
});
const original = Object.getOwnPropertyDescriptor(globalThis, "self");
try {
globalThis.self = 123;
expect(globalThis.self).toBe(123);
} finally {
Object.defineProperty(globalThis, "self", original);
}
test("globalThis.self is not defined on main thread", () => {
// self should not be defined on the main thread (Node.js compat).
// See: https://github.com/oven-sh/bun/issues/27476
expect(globalThis.self).toBeUndefined();
expect(Object.getOwnPropertyDescriptor(globalThis, "self")).toBeUndefined();
});

View File

@@ -0,0 +1,81 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";
// https://github.com/oven-sh/bun/issues/27476
// `typeof self` should be "undefined" on the main thread (Node.js compat).
// Many libraries use `typeof self !== "undefined"` to detect a browser environment.
test("typeof self is undefined on the main thread", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", "console.log(typeof self)"],
env: bunEnv,
});
const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]);
expect(stdout.trim()).toBe("undefined");
expect(exitCode).toBe(0);
});
test("self is not in globalThis on the main thread", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", 'console.log("self" in globalThis)'],
env: bunEnv,
});
const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]);
expect(stdout.trim()).toBe("false");
expect(exitCode).toBe(0);
});
test("browser detection pattern returns false on the main thread", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", `const isBrowser = typeof window < "u" || typeof self < "u"; console.log(isBrowser);`],
env: bunEnv,
});
const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]);
expect(stdout.trim()).toBe("false");
expect(exitCode).toBe(0);
});
test("self is defined in a Worker context", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const worker = new Worker(new URL("data:text/javascript," + encodeURIComponent('postMessage(typeof self)')), { type: "module" });
worker.onmessage = (e) => {
console.log(e.data);
worker.terminate();
};
`,
],
env: bunEnv,
});
const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]);
expect(stdout.trim()).toBe("object");
expect(exitCode).toBe(0);
});
test("self equals globalThis in a Worker context", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const worker = new Worker(new URL("data:text/javascript," + encodeURIComponent('postMessage(self === globalThis)')), { type: "module" });
worker.onmessage = (e) => {
console.log(e.data);
worker.terminate();
};
`,
],
env: bunEnv,
});
const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]);
expect(stdout.trim()).toBe("true");
expect(exitCode).toBe(0);
});