Compare commits

..

2 Commits

Author SHA1 Message Date
autofix-ci[bot]
053637d55f [autofix.ci] apply automated fixes 2026-02-11 16:24:28 +00:00
Claude Bot
b746919e38 fix(web): give File a distinct prototype from Blob
Previously `File.prototype === Blob.prototype` was `true`, causing
`new File(...).constructor.name` to return `"Blob"` instead of `"File"`.
This creates a separate `FilePrototype` that inherits from
`Blob.prototype`, fixing the prototype chain per the spec.

Closes #26899

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-11 16:22:22 +00:00
7 changed files with 191 additions and 290 deletions

View File

@@ -69,7 +69,6 @@
#include "ZigSourceProvider.h"
#include <JavaScriptCore/FunctionPrototype.h>
#include "JSCommonJSModule.h"
#include <JavaScriptCore/JSModuleLoader.h>
#include <JavaScriptCore/JSModuleNamespaceObject.h>
#include <JavaScriptCore/JSSourceCode.h>
#include <JavaScriptCore/LazyPropertyInlines.h>
@@ -684,19 +683,6 @@ JSC_DEFINE_CUSTOM_SETTER(setterUnderscoreCompile,
return true;
}
static BunLoaderType loaderFromFilename(const String& filename)
{
if (filename.endsWith(".ts"_s))
return BunLoaderTypeTS;
if (filename.endsWith(".tsx"_s))
return BunLoaderTypeTSX;
if (filename.endsWith(".jsx"_s))
return BunLoaderTypeJSX;
// _compile always receives JavaScript/TypeScript source code.
// Default to JS for unknown extensions (e.g. .custom, .node, etc.)
return BunLoaderTypeJS;
}
JSC_DEFINE_HOST_FUNCTION(functionJSCommonJSModule_compile, (JSGlobalObject * globalObject, CallFrame* callframe))
{
auto* moduleObject = jsDynamicCast<JSCommonJSModule*>(callframe->thisValue());
@@ -714,95 +700,46 @@ JSC_DEFINE_HOST_FUNCTION(functionJSCommonJSModule_compile, (JSGlobalObject * glo
String filenameString = filenameValue.toWTFString(globalObject);
RETURN_IF_EXCEPTION(throwScope, {});
String wrappedString;
auto* zigGlobalObject = jsCast<Zig::GlobalObject*>(globalObject);
if (zigGlobalObject->hasOverriddenModuleWrapper) [[unlikely]] {
wrappedString = makeString(
zigGlobalObject->m_moduleWrapperStart,
sourceString,
zigGlobalObject->m_moduleWrapperEnd);
} else {
wrappedString = makeString(
"(function(exports,require,module,__filename,__dirname){"_s,
sourceString,
"\n})"_s);
}
// Determine loader from filename extension.
BunLoaderType loader = loaderFromFilename(filenameString);
moduleObject->sourceCode = makeSource(
WTF::move(wrappedString),
SourceOrigin(URL::fileURLWithFileSystemPath(filenameString)),
JSC::SourceTaintedOrigin::Untainted,
filenameString,
WTF::TextPosition(),
JSC::SourceProviderSourceType::Program);
// Transpile the source through Bun's transpiler. This handles:
// - TypeScript syntax (.ts, .tsx)
// - JSX syntax (.jsx, .tsx)
// - ESM import/export → CJS conversion (when code uses CJS features)
BunString specifier = Bun::toStringRef(filenameString);
BunString referrer = BunStringEmpty;
auto sourceUTF8 = sourceString.utf8();
ZigString sourceZig = { reinterpret_cast<const unsigned char*>(sourceUTF8.data()), sourceUTF8.length() };
ErrorableResolvedSource transpileResult = {};
Bun__transpileVirtualModule(globalObject, &specifier, &referrer, &sourceZig, loader, &transpileResult);
EncodedJSValue encodedFilename = JSValue::encode(filenameValue);
#if OS(WINDOWS)
JSValue dirnameValue = JSValue::decode(Bun__Path__dirname(globalObject, true, &encodedFilename, 1));
#else
JSValue dirnameValue = JSValue::decode(Bun__Path__dirname(globalObject, false, &encodedFilename, 1));
#endif
RETURN_IF_EXCEPTION(throwScope, {});
if (!transpileResult.success) {
if (!throwScope.exception()) {
throwException(globalObject, throwScope, createError(globalObject, "Failed to transpile source in Module._compile"_s));
}
return {};
}
String dirnameString = dirnameValue.toWTFString(globalObject);
ResolvedSource& source = transpileResult.result.value;
if (source.isCommonJSModule) {
// The transpiler detected CJS usage and already wrapped the code in
// (function(exports, require, module, __filename, __dirname) { ... }).
String wrappedString = source.source_code.toWTFString(BunString::ZeroCopy);
if (zigGlobalObject->hasOverriddenModuleWrapper) [[unlikely]] {
// Strip the default wrapper and re-wrap with the custom one.
auto trimStart = wrappedString.find('\n');
if (trimStart != WTF::notFound) {
wrappedString = makeString(
zigGlobalObject->m_moduleWrapperStart,
wrappedString.substring(trimStart, wrappedString.length() - trimStart - 4),
zigGlobalObject->m_moduleWrapperEnd);
}
}
moduleObject->sourceCode = makeSource(
WTF::move(wrappedString),
SourceOrigin(URL::fileURLWithFileSystemPath(filenameString)),
JSC::SourceTaintedOrigin::Untainted,
filenameString,
WTF::TextPosition(),
JSC::SourceProviderSourceType::Program);
EncodedJSValue encodedFilename = JSValue::encode(filenameValue);
#if OS(WINDOWS)
JSValue dirnameValue = JSValue::decode(Bun__Path__dirname(globalObject, true, &encodedFilename, 1));
#else
JSValue dirnameValue = JSValue::decode(Bun__Path__dirname(globalObject, false, &encodedFilename, 1));
#endif
RETURN_IF_EXCEPTION(throwScope, {});
String dirnameString = dirnameValue.toWTFString(globalObject);
evaluateCommonJSModuleOnce(
vm,
zigGlobalObject,
moduleObject,
jsString(vm, dirnameString),
jsString(vm, filenameString));
RETURN_IF_EXCEPTION(throwScope, {});
} else {
// The source contains ESM syntax (import/export). The transpiler kept it as ESM
// (with TS/JSX stripped). Provide the transpiled source to JSC's module loader
// and evaluate it as ESM, then populate the CJS module's exports.
auto provider = Zig::SourceProvider::create(zigGlobalObject, source, JSC::SourceProviderSourceType::Module, false);
zigGlobalObject->moduleLoader()->provideFetch(globalObject, filenameValue, JSC::SourceCode(WTF::move(provider)));
RETURN_IF_EXCEPTION(throwScope, {});
// Use requireESMFromHijackedExtension to synchronously evaluate the ESM module
// and populate the CJS module's exports from the ESM namespace.
JSC::JSFunction* requireESM = zigGlobalObject->requireESMFromHijackedExtension();
JSC::MarkedArgumentBuffer args;
args.append(filenameValue);
JSC::CallData callData = JSC::getCallData(requireESM);
NakedPtr<JSC::Exception> returnedException = nullptr;
JSC::profiledCall(globalObject, JSC::ProfilingReason::API, requireESM, callData, moduleObject, args, returnedException);
if (returnedException) [[unlikely]] {
throwException(globalObject, throwScope, returnedException->value());
return {};
}
}
WTF::NakedPtr<JSC::Exception> exception;
evaluateCommonJSModuleOnce(
vm,
jsCast<Zig::GlobalObject*>(globalObject),
moduleObject,
jsString(vm, dirnameString),
jsString(vm, filenameString));
RETURN_IF_EXCEPTION(throwScope, {});
return JSValue::encode(jsUndefined());
}

View File

@@ -10,8 +10,56 @@ using namespace JSC;
extern "C" SYSV_ABI void* JSDOMFile__construct(JSC::JSGlobalObject*, JSC::CallFrame* callframe);
extern "C" SYSV_ABI bool JSDOMFile__hasInstance(EncodedJSValue, JSC::JSGlobalObject*, EncodedJSValue);
// TODO: make this inehrit from JSBlob instead of InternalFunction
// That will let us remove this hack for [Symbol.hasInstance] and fix the prototype chain.
// File.prototype inherits from Blob.prototype per the spec.
// This gives File instances all Blob methods while having a distinct prototype
// with constructor === File and [Symbol.toStringTag] === "File".
class JSDOMFilePrototype final : public JSC::JSNonFinalObject {
using Base = JSC::JSNonFinalObject;
public:
static constexpr unsigned StructureFlags = Base::StructureFlags;
static JSDOMFilePrototype* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure)
{
JSDOMFilePrototype* prototype = new (NotNull, JSC::allocateCell<JSDOMFilePrototype>(vm)) JSDOMFilePrototype(vm, structure);
prototype->finishCreation(vm, globalObject);
return prototype;
}
DECLARE_INFO;
static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype)
{
auto* structure = JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info());
structure->setMayBePrototype(true);
return structure;
}
template<typename CellType, JSC::SubspaceAccess>
static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm)
{
STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSDOMFilePrototype, Base);
return &vm.plainObjectSpace();
}
protected:
JSDOMFilePrototype(JSC::VM& vm, JSC::Structure* structure)
: Base(vm, structure)
{
}
void finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject)
{
Base::finishCreation(vm);
// Set [Symbol.toStringTag] = "File" so Object.prototype.toString.call(file) === "[object File]"
this->putDirectWithoutTransition(vm, vm.propertyNames->toStringTagSymbol,
jsNontrivialString(vm, "File"_s),
JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::ReadOnly);
}
};
const JSC::ClassInfo JSDOMFilePrototype::s_info = { "File"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSDOMFilePrototype) };
class JSDOMFile : public JSC::InternalFunction {
using Base = JSC::InternalFunction;
@@ -40,15 +88,20 @@ public:
Base::finishCreation(vm, 2, "File"_s);
}
static JSDOMFile* create(JSC::VM& vm, JSGlobalObject* globalObject)
static JSDOMFile* create(JSC::VM& vm, JSGlobalObject* globalObject, JSC::JSObject* filePrototype)
{
auto* zigGlobal = defaultGlobalObject(globalObject);
auto structure = createStructure(vm, globalObject, zigGlobal->functionPrototype());
auto* object = new (NotNull, JSC::allocateCell<JSDOMFile>(vm)) JSDOMFile(vm, structure);
object->finishCreation(vm);
// This is not quite right. But we'll fix it if someone files an issue about it.
object->putDirect(vm, vm.propertyNames->prototype, zigGlobal->JSBlobPrototype(), JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly | 0);
// Set File.prototype to the distinct FilePrototype object (which inherits from Blob.prototype).
object->putDirect(vm, vm.propertyNames->prototype, filePrototype,
JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly);
// Set FilePrototype.constructor = File
filePrototype->putDirect(vm, vm.propertyNames->constructor, object,
static_cast<unsigned>(JSC::PropertyAttribute::DontEnum));
return object;
}
@@ -69,7 +122,7 @@ public:
auto& vm = JSC::getVM(globalObject);
JSObject* newTarget = asObject(callFrame->newTarget());
auto* constructor = globalObject->JSDOMFileConstructor();
Structure* structure = globalObject->JSBlobStructure();
Structure* structure = globalObject->JSFileStructure();
if (constructor != newTarget) {
auto scope = DECLARE_THROW_SCOPE(vm);
@@ -77,7 +130,7 @@ public:
// ShadowRealm functions belong to a different global object.
getFunctionRealm(lexicalGlobalObject, newTarget));
RETURN_IF_EXCEPTION(scope, {});
structure = InternalFunction::createSubclassStructure(lexicalGlobalObject, newTarget, functionGlobalObject->JSBlobStructure());
structure = InternalFunction::createSubclassStructure(lexicalGlobalObject, newTarget, functionGlobalObject->JSFileStructure());
RETURN_IF_EXCEPTION(scope, {});
}
@@ -103,9 +156,30 @@ const JSC::ClassInfo JSDOMFile::s_info = { "File"_s, &Base::s_info, nullptr, nul
namespace Bun {
JSC::Structure* createJSFileStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject)
{
auto* zigGlobal = defaultGlobalObject(globalObject);
JSC::JSObject* blobPrototype = zigGlobal->JSBlobPrototype();
// Create FilePrototype with [[Prototype]] = Blob.prototype
auto* protoStructure = JSDOMFilePrototype::createStructure(vm, globalObject, blobPrototype);
auto* filePrototype = JSDOMFilePrototype::create(vm, globalObject, protoStructure);
// Create the structure for File instances: [[Prototype]] = FilePrototype
return JSC::Structure::create(vm, globalObject, filePrototype,
JSC::TypeInfo(static_cast<JSC::JSType>(0b11101110), WebCore::JSBlob::StructureFlags),
WebCore::JSBlob::info(), NonArray);
}
JSC::JSObject* createJSDOMFileConstructor(JSC::VM& vm, JSC::JSGlobalObject* globalObject)
{
return JSDOMFile::create(vm, globalObject);
auto* zigGlobal = defaultGlobalObject(globalObject);
// Get the File instance structure - its prototype is the FilePrototype we need
auto* fileStructure = zigGlobal->JSFileStructure();
auto* filePrototype = fileStructure->storedPrototypeObject();
return JSDOMFile::create(vm, globalObject, filePrototype);
}
}

View File

@@ -4,4 +4,5 @@
namespace Bun {
JSC::JSObject* createJSDOMFileConstructor(JSC::VM&, JSC::JSGlobalObject*);
JSC::Structure* createJSFileStructure(JSC::VM&, JSC::JSGlobalObject*);
}

View File

@@ -1805,6 +1805,11 @@ void GlobalObject::finishCreation(VM& vm)
init.set(CustomGetterSetter::create(init.vm, errorInstanceLazyStackCustomGetter, errorInstanceLazyStackCustomSetter));
});
m_JSFileStructure.initLater(
[](const Initializer<Structure>& init) {
init.set(Bun::createJSFileStructure(init.vm, init.owner));
});
m_JSDOMFileConstructor.initLater(
[](const Initializer<JSObject>& init) {
JSObject* fileConstructor = Bun::createJSDOMFileConstructor(init.vm, init.owner);

View File

@@ -610,6 +610,7 @@ public:
V(private, LazyPropertyOfGlobalObject<Structure>, m_importMetaBakeObjectStructure) \
V(private, LazyPropertyOfGlobalObject<Structure>, m_asyncBoundFunctionStructure) \
V(public, LazyPropertyOfGlobalObject<JSC::JSObject>, m_JSDOMFileConstructor) \
V(public, LazyPropertyOfGlobalObject<Structure>, m_JSFileStructure) \
V(public, LazyPropertyOfGlobalObject<JSC::JSObject>, m_JSMIMEParamsConstructor) \
V(public, LazyPropertyOfGlobalObject<JSC::JSObject>, m_JSMIMETypeConstructor) \
\
@@ -712,6 +713,7 @@ public:
JSObject* cryptoObject() const { return m_cryptoObject.getInitializedOnMainThread(this); }
JSObject* JSDOMFileConstructor() const { return m_JSDOMFileConstructor.getInitializedOnMainThread(this); }
JSC::Structure* JSFileStructure() const { return m_JSFileStructure.getInitializedOnMainThread(this); }
JSMap* nodeWorkerEnvironmentData() { return m_nodeWorkerEnvironmentData.get(); }
void setNodeWorkerEnvironmentData(JSMap* data);

View File

@@ -1,185 +0,0 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";
test("Module._compile handles ESM import/export syntax", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const Module = require('module');
const source = \`
import path from "node:path";
import { fileURLToPath } from "node:url";
const config = {
value: 42,
};
export default config;
\`;
const m = new Module('/tmp/test-26874.ts');
m.filename = '/tmp/test-26874.ts';
m.paths = Module._nodeModulePaths('/tmp/');
m._compile(source, '/tmp/test-26874.ts');
console.log(JSON.stringify(m.exports.default));
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout.trim()).toBe('{"value":42}');
expect(exitCode).toBe(0);
});
test("Module._compile handles TypeScript syntax", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const Module = require('module');
const source = \`
interface Config {
value: number;
name: string;
}
const config: Config = {
value: 123,
name: "test",
};
module.exports = config;
\`;
const m = new Module('/tmp/test-26874-ts.ts');
m.filename = '/tmp/test-26874-ts.ts';
m.paths = Module._nodeModulePaths('/tmp/');
m._compile(source, '/tmp/test-26874-ts.ts');
console.log(JSON.stringify(m.exports));
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout.trim()).toBe('{"value":123,"name":"test"}');
expect(exitCode).toBe(0);
});
test("Module._compile still works with plain CJS source", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const Module = require('module');
const source = \`
const x = 10;
const y = 20;
module.exports = { sum: x + y };
\`;
const m = new Module('/tmp/test-26874-cjs.js');
m.filename = '/tmp/test-26874-cjs.js';
m.paths = Module._nodeModulePaths('/tmp/');
m._compile(source, '/tmp/test-26874-cjs.js');
console.log(JSON.stringify(m.exports));
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout.trim()).toBe('{"sum":30}');
expect(exitCode).toBe(0);
});
test("Module._compile handles ESM with .js filename", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const Module = require('module');
const source = \`
export const hello = "world";
export default { greeting: "hi" };
\`;
const m = new Module('/tmp/test-26874-esm.js');
m.filename = '/tmp/test-26874-esm.js';
m.paths = Module._nodeModulePaths('/tmp/');
m._compile(source, '/tmp/test-26874-esm.js');
console.log(JSON.stringify(m.exports.default));
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout.trim()).toBe('{"greeting":"hi"}');
expect(exitCode).toBe(0);
});
test("Module._compile handles ESM TypeScript with imports and exports", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const Module = require('module');
const source = \`
import path from "node:path";
import { fileURLToPath } from "node:url";
interface Config {
turbopack: { root: string };
}
const config: Config = {
turbopack: {
root: "/test",
},
};
export default config;
\`;
const m = new Module('/tmp/test-26874-esm-ts.ts');
m.filename = '/tmp/test-26874-esm-ts.ts';
m.paths = Module._nodeModulePaths('/tmp/');
m._compile(source, '/tmp/test-26874-esm-ts.ts');
console.log(JSON.stringify(m.exports.default));
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout.trim()).toBe('{"turbopack":{"root":"/test"}}');
expect(exitCode).toBe(0);
});

View File

@@ -0,0 +1,67 @@
import { expect, test } from "bun:test";
// https://github.com/oven-sh/bun/issues/26899
// File.prototype should be distinct from Blob.prototype
test("File.prototype !== Blob.prototype", () => {
expect(File.prototype).not.toBe(Blob.prototype);
});
test("File.prototype inherits from Blob.prototype", () => {
expect(Object.getPrototypeOf(File.prototype)).toBe(Blob.prototype);
});
test("new File(...).constructor.name === 'File'", () => {
const file = new File(["hello"], "hello.txt");
expect(file.constructor.name).toBe("File");
});
test("new File(...).constructor === File", () => {
const file = new File(["hello"], "hello.txt");
expect(file.constructor).toBe(File);
});
test("new File(...).constructor !== Blob", () => {
const file = new File(["hello"], "hello.txt");
expect(file.constructor).not.toBe(Blob);
});
test("Object.prototype.toString.call(file) === '[object File]'", () => {
const file = new File(["hello"], "hello.txt");
expect(Object.prototype.toString.call(file)).toBe("[object File]");
});
test("file instanceof File", () => {
const file = new File(["hello"], "hello.txt");
expect(file instanceof File).toBe(true);
});
test("file instanceof Blob", () => {
const file = new File(["hello"], "hello.txt");
expect(file instanceof Blob).toBe(true);
});
test("blob is not instanceof File", () => {
const blob = new Blob(["hello"]);
expect(blob instanceof File).toBe(false);
});
test("File instances have Blob methods", () => {
const file = new File(["hello"], "hello.txt");
expect(typeof file.text).toBe("function");
expect(typeof file.arrayBuffer).toBe("function");
expect(typeof file.slice).toBe("function");
expect(typeof file.stream).toBe("function");
});
test("File name and lastModified work", () => {
const file = new File(["hello"], "hello.txt", { lastModified: 12345 });
expect(file.name).toBe("hello.txt");
expect(file.lastModified).toBe(12345);
});
test("File.prototype has correct Symbol.toStringTag", () => {
const desc = Object.getOwnPropertyDescriptor(File.prototype, Symbol.toStringTag);
expect(desc).toBeDefined();
expect(desc!.value).toBe("File");
});