diff --git a/src/bun.js/bindings/JSBunFile.cpp b/src/bun.js/bindings/JSBunFile.cpp new file mode 100644 index 0000000000..c49039d6b2 --- /dev/null +++ b/src/bun.js/bindings/JSBunFile.cpp @@ -0,0 +1,276 @@ + +#include "root.h" + +#include "ZigGlobalObject.h" +#include "ZigGeneratedClasses.h" + +#include "JavaScriptCore/JSType.h" +#include "JavaScriptCore/JSObject.h" +#include "JavaScriptCore/JSGlobalObject.h" +#include +#include +#include +#include +#include +#include "JavaScriptCore/JSCJSValue.h" +#include "ErrorCode.h" + +#include "JSBunFile.h" + +namespace Bun { +using namespace JSC; +using namespace WebCore; + +// Reuse existing Blob extern functions for BunFile-specific methods +extern "C" { +SYSV_ABI EncodedJSValue BlobPrototype__getExists(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +SYSV_ABI EncodedJSValue BlobPrototype__doUnlink(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +SYSV_ABI EncodedJSValue BlobPrototype__doWrite(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +SYSV_ABI EncodedJSValue BlobPrototype__getStat(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +SYSV_ABI EncodedJSValue BlobPrototype__getWriter(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +SYSV_ABI EncodedJSValue BlobPrototype__getName(void* ptr, JSC::EncodedJSValue thisValue, JSC::JSGlobalObject* lexicalGlobalObject); +SYSV_ABI bool BlobPrototype__setName(void* ptr, JSC::EncodedJSValue thisValue, JSC::JSGlobalObject* lexicalGlobalObject, JSC::EncodedJSValue value); +SYSV_ABI EncodedJSValue BlobPrototype__getLastModified(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject); +SYSV_ABI bool JSDOMFile__hasInstance(EncodedJSValue, JSC::JSGlobalObject*, EncodedJSValue); +} + +// BunFile constructor - throws when called directly, exists for constructor.name +JSC_DECLARE_HOST_FUNCTION(callBunFileConstructor); +JSC_DEFINE_HOST_FUNCTION(callBunFileConstructor, (JSGlobalObject * globalObject, CallFrame*)) +{ + auto scope = DECLARE_THROW_SCOPE(globalObject->vm()); + throwTypeError(globalObject, scope, "BunFile is not constructable. Use Bun.file() to create a BunFile."_s); + return {}; +} + +// Forward declarations for host functions +JSC_DECLARE_HOST_FUNCTION(functionBunFile_exists); +JSC_DECLARE_HOST_FUNCTION(functionBunFile_unlink); +JSC_DECLARE_HOST_FUNCTION(functionBunFile_write); +JSC_DECLARE_HOST_FUNCTION(functionBunFile_stat); +JSC_DECLARE_HOST_FUNCTION(functionBunFile_writer); +static JSC_DECLARE_CUSTOM_GETTER(getterBunFile_name); +static JSC_DECLARE_CUSTOM_SETTER(setterBunFile_name); +static JSC_DECLARE_CUSTOM_GETTER(getterBunFile_lastModified); + +// --- Host function implementations --- + +JSC_DEFINE_HOST_FUNCTION(functionBunFile_exists, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto* thisObject = jsDynamicCast(callFrame->thisValue()); + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + if (!thisObject) { + Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_THIS, "Expected a BunFile instance"_s); + return {}; + } + return BlobPrototype__getExists(thisObject->wrapped(), globalObject, callFrame); +} + +JSC_DEFINE_HOST_FUNCTION(functionBunFile_unlink, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto* thisObject = jsDynamicCast(callFrame->thisValue()); + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + if (!thisObject) { + Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_THIS, "Expected a BunFile instance"_s); + return {}; + } + return BlobPrototype__doUnlink(thisObject->wrapped(), globalObject, callFrame); +} + +JSC_DEFINE_HOST_FUNCTION(functionBunFile_write, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto* thisObject = jsDynamicCast(callFrame->thisValue()); + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + if (!thisObject) { + Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_THIS, "Expected a BunFile instance"_s); + return {}; + } + return BlobPrototype__doWrite(thisObject->wrapped(), globalObject, callFrame); +} + +JSC_DEFINE_HOST_FUNCTION(functionBunFile_stat, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto* thisObject = jsDynamicCast(callFrame->thisValue()); + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + if (!thisObject) { + Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_THIS, "Expected a BunFile instance"_s); + return {}; + } + return BlobPrototype__getStat(thisObject->wrapped(), globalObject, callFrame); +} + +JSC_DEFINE_HOST_FUNCTION(functionBunFile_writer, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto* thisObject = jsDynamicCast(callFrame->thisValue()); + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + if (!thisObject) { + Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_THIS, "Expected a BunFile instance"_s); + return {}; + } + return BlobPrototype__getWriter(thisObject->wrapped(), globalObject, callFrame); +} + +static JSC_DEFINE_CUSTOM_GETTER(getterBunFile_name, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) +{ + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (!thisObject) { + Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_THIS, "Expected a BunFile instance"_s); + return {}; + } + + return BlobPrototype__getName(thisObject->wrapped(), thisValue, globalObject); +} + +static JSC_DEFINE_CUSTOM_SETTER(setterBunFile_name, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue value, JSC::PropertyName)) +{ + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (!thisObject) { + Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_THIS, "Expected a BunFile instance"_s); + return false; + } + + return BlobPrototype__setName(thisObject->wrapped(), thisValue, globalObject, value); +} + +static JSC_DEFINE_CUSTOM_GETTER(getterBunFile_lastModified, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) +{ + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (!thisObject) { + Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_THIS, "Expected a BunFile instance"_s); + return {}; + } + + return BlobPrototype__getLastModified(thisObject->wrapped(), globalObject); +} + +// --- BunFile-specific prototype property table --- +static const HashTableValue JSBunFilePrototypeTableValues[] = { + { "delete"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::NativeFunctionType, functionBunFile_unlink, 0 } }, + { "exists"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::NativeFunctionType, functionBunFile_exists, 0 } }, + { "lastModified"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, getterBunFile_lastModified, 0 } }, + { "name"_s, static_cast(PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, getterBunFile_name, setterBunFile_name } }, + { "stat"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::NativeFunctionType, functionBunFile_stat, 0 } }, + { "unlink"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::NativeFunctionType, functionBunFile_unlink, 0 } }, + { "write"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::NativeFunctionType, functionBunFile_write, 2 } }, + { "writer"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::NativeFunctionType, functionBunFile_writer, 1 } }, +}; + +class JSBunFilePrototype final : public WebCore::JSBlobPrototype { +public: + using Base = WebCore::JSBlobPrototype; + static constexpr unsigned StructureFlags = Base::StructureFlags; + + static JSBunFilePrototype* create( + JSC::VM& vm, + JSC::JSGlobalObject* globalObject, + JSC::Structure* structure) + { + JSBunFilePrototype* prototype = new (NotNull, JSC::allocateCell(vm)) JSBunFilePrototype(vm, globalObject, structure); + prototype->finishCreation(vm, globalObject); + return prototype; + } + + static JSC::Structure* createStructure( + JSC::VM& vm, + JSC::JSGlobalObject* globalObject, + JSC::JSValue prototype) + { + auto* structure = JSC::Structure::create(vm, globalObject, prototype, TypeInfo(JSC::ObjectType, StructureFlags), info()); + structure->setMayBePrototype(true); + return structure; + } + + DECLARE_INFO; + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSBunFilePrototype, Base); + return &vm.plainObjectSpace(); + } + +protected: + JSBunFilePrototype(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure) + : Base(vm, globalObject, structure) + { + } + + void finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject) + { + Base::finishCreation(vm, globalObject); + ASSERT(inherits(info())); + reifyStaticProperties(vm, JSBunFile::info(), JSBunFilePrototypeTableValues, *this); + + this->putDirect(vm, vm.propertyNames->toStringTagSymbol, jsOwnedString(vm, "BunFile"_s), 0); + } +}; + +// Implementation of JSBunFile methods +void JSBunFile::destroy(JSCell* cell) +{ + static_cast(cell)->JSBunFile::~JSBunFile(); +} + +JSBunFile::~JSBunFile() +{ + // Base class destructor will be called automatically +} + +JSBunFile* JSBunFile::create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, void* ptr) +{ + JSBunFile* thisObject = new (NotNull, JSC::allocateCell(vm)) JSBunFile(vm, structure, ptr); + thisObject->finishCreation(vm); + return thisObject; +} + +JSC::Structure* JSBunFile::createStructure(JSC::JSGlobalObject* globalObject) +{ + auto& vm = JSC::getVM(globalObject); + + JSC::JSObject* superPrototype = defaultGlobalObject(globalObject)->JSBlobPrototype(); + auto* protoStructure = JSBunFilePrototype::createStructure(vm, globalObject, superPrototype); + auto* prototype = JSBunFilePrototype::create(vm, globalObject, protoStructure); + + // Create a constructor function named "BunFile" for constructor.name + auto* constructor = JSFunction::create(vm, globalObject, 0, "BunFile"_s, callBunFileConstructor, ImplementationVisibility::Public, NoIntrinsic, callBunFileConstructor); + constructor->putDirect(vm, vm.propertyNames->prototype, prototype, PropertyAttribute::DontEnum | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly); + prototype->putDirect(vm, vm.propertyNames->constructor, constructor, static_cast(PropertyAttribute::DontEnum)); + + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(static_cast(0b11101110), StructureFlags), info(), NonArray); +} + +Structure* createJSBunFileStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject) +{ + return JSBunFile::createStructure(globalObject); +} + +const JSC::ClassInfo JSBunFilePrototype::s_info = { "BunFile"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSBunFilePrototype) }; +const JSC::ClassInfo JSBunFile::s_info = { "BunFile"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSBunFile) }; + +extern "C" { +SYSV_ABI EncodedJSValue BUN__createJSBunFileUnsafely(JSC::JSGlobalObject* globalObject, void* ptr) +{ + ASSERT(ptr); + auto& vm = JSC::getVM(globalObject); + + auto* zigGlobal = defaultGlobalObject(globalObject); + auto* structure = zigGlobal->m_JSBunFileStructure.getInitializedOnMainThread(globalObject); + return JSValue::encode(JSBunFile::create(vm, globalObject, structure, ptr)); +} +} + +} diff --git a/src/bun.js/bindings/JSBunFile.h b/src/bun.js/bindings/JSBunFile.h new file mode 100644 index 0000000000..f9760b64e3 --- /dev/null +++ b/src/bun.js/bindings/JSBunFile.h @@ -0,0 +1,39 @@ +#pragma once + +namespace Zig { +class GlobalObject; +} + +namespace Bun { +using namespace JSC; + +class JSBunFile : public WebCore::JSBlob { + using Base = WebCore::JSBlob; + +public: + static constexpr JSC::DestructionMode needsDestruction = NeedsDestruction; + static constexpr unsigned StructureFlags = Base::StructureFlags; + + JSBunFile(JSC::VM& vm, Structure* structure, void* ptr) + : Base(vm, structure, ptr) + { + } + DECLARE_INFO; + + template static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return WebCore::JSBlob::subspaceFor(vm); + } + + static void destroy(JSCell* cell); + ~JSBunFile(); + + static JSBunFile* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, void* ptr); + static JSC::Structure* createStructure(JSC::JSGlobalObject* globalObject); +}; + +Structure* createJSBunFileStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject); + +} // namespace Bun diff --git a/src/bun.js/bindings/JSDOMFile.cpp b/src/bun.js/bindings/JSDOMFile.cpp index ac3e0588cf..424aa02fe3 100644 --- a/src/bun.js/bindings/JSDOMFile.cpp +++ b/src/bun.js/bindings/JSDOMFile.cpp @@ -1,16 +1,116 @@ #include "root.h" +#include "ZigGlobalObject.h" #include "ZigGeneratedClasses.h" #include #include #include #include "JSDOMFile.h" +#include "ErrorCode.h" 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); +extern "C" SYSV_ABI EncodedJSValue BlobPrototype__getName(void* ptr, JSC::EncodedJSValue thisValue, JSC::JSGlobalObject* lexicalGlobalObject); +extern "C" SYSV_ABI bool BlobPrototype__setName(void* ptr, JSC::EncodedJSValue thisValue, JSC::JSGlobalObject* lexicalGlobalObject, JSC::EncodedJSValue value); +extern "C" SYSV_ABI EncodedJSValue BlobPrototype__getLastModified(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject); -// TODO: make this inehrit from JSBlob instead of InternalFunction +static JSC_DECLARE_CUSTOM_GETTER(getterDOMFile_name); +static JSC_DECLARE_CUSTOM_SETTER(setterDOMFile_name); +static JSC_DECLARE_CUSTOM_GETTER(getterDOMFile_lastModified); + +static JSC_DEFINE_CUSTOM_GETTER(getterDOMFile_name, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) +{ + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (!thisObject) { + Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_THIS, "Expected a File instance"_s); + return {}; + } + + return BlobPrototype__getName(thisObject->wrapped(), thisValue, globalObject); +} + +static JSC_DEFINE_CUSTOM_SETTER(setterDOMFile_name, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue value, JSC::PropertyName)) +{ + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (!thisObject) { + Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_THIS, "Expected a File instance"_s); + return false; + } + + return BlobPrototype__setName(thisObject->wrapped(), thisValue, globalObject, value); +} + +static JSC_DEFINE_CUSTOM_GETTER(getterDOMFile_lastModified, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) +{ + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (!thisObject) { + Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_THIS, "Expected a File instance"_s); + return {}; + } + + return BlobPrototype__getLastModified(thisObject->wrapped(), globalObject); +} + +static const HashTableValue JSDOMFilePrototypeTableValues[] = { + { "lastModified"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, getterDOMFile_lastModified, 0 } }, + { "name"_s, static_cast(PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, getterDOMFile_name, setterDOMFile_name } }, +}; + +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(vm)) JSDOMFilePrototype(vm, structure); + prototype->finishCreation(vm, globalObject); + return prototype; + } + + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + auto* structure = JSC::Structure::create(vm, globalObject, prototype, TypeInfo(JSC::ObjectType, StructureFlags), info()); + structure->setMayBePrototype(true); + return structure; + } + + DECLARE_INFO; + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + return &vm.plainObjectSpace(); + } + +private: + JSDOMFilePrototype(JSC::VM& vm, JSC::Structure* structure) + : Base(vm, structure) + { + } + + void finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject) + { + Base::finishCreation(vm); + ASSERT(inherits(info())); + reifyStaticProperties(vm, info(), JSDOMFilePrototypeTableValues, *this); + this->putDirect(vm, vm.propertyNames->toStringTagSymbol, jsOwnedString(vm, "File"_s), 0); + } +}; + +const JSC::ClassInfo JSDOMFilePrototype::s_info = { "File"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSDOMFilePrototype) }; + +// TODO: make this inherit from JSBlob instead of InternalFunction // That will let us remove this hack for [Symbol.hasInstance] and fix the prototype chain. class JSDOMFile : public JSC::InternalFunction { using Base = JSC::InternalFunction; @@ -47,8 +147,13 @@ public: auto* object = new (NotNull, JSC::allocateCell(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); + // Create a proper File prototype that extends Blob.prototype + auto* blobPrototype = zigGlobal->JSBlobPrototype(); + auto* protoStructure = JSDOMFilePrototype::createStructure(vm, globalObject, blobPrototype); + auto* filePrototype = JSDOMFilePrototype::create(vm, globalObject, protoStructure); + + object->putDirect(vm, vm.propertyNames->prototype, filePrototype, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly | 0); + filePrototype->putDirect(vm, vm.propertyNames->constructor, object, static_cast(JSC::PropertyAttribute::DontEnum)); return object; } @@ -69,7 +174,7 @@ public: auto& vm = JSC::getVM(globalObject); JSObject* newTarget = asObject(callFrame->newTarget()); auto* constructor = globalObject->JSDOMFileConstructor(); - Structure* structure = globalObject->JSBlobStructure(); + Structure* structure = globalObject->m_JSDOMFileStructure.getInitializedOnMainThread(lexicalGlobalObject); if (constructor != newTarget) { auto scope = DECLARE_THROW_SCOPE(vm); @@ -77,7 +182,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->m_JSDOMFileStructure.getInitializedOnMainThread(lexicalGlobalObject)); RETURN_IF_EXCEPTION(scope, {}); } @@ -108,4 +213,15 @@ JSC::JSObject* createJSDOMFileConstructor(JSC::VM& vm, JSC::JSGlobalObject* glob return JSDOMFile::create(vm, globalObject); } +JSC::Structure* createJSDOMFileInstanceStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject) +{ + auto* zigGlobal = defaultGlobalObject(globalObject); + // Get the File.prototype from the constructor + auto* fileConstructor = zigGlobal->JSDOMFileConstructor(); + JSValue filePrototype = fileConstructor->getDirect(vm, vm.propertyNames->prototype); + ASSERT(filePrototype.isObject()); + // Create a JSBlob structure that uses File.prototype instead of Blob.prototype + return JSC::Structure::create(vm, globalObject, filePrototype, JSC::TypeInfo(static_cast(0b11101110), WebCore::JSBlob::StructureFlags), WebCore::JSBlob::info(), JSC::NonArray); +} + } diff --git a/src/bun.js/bindings/JSDOMFile.h b/src/bun.js/bindings/JSDOMFile.h index eed1e98ab8..8ae033e294 100644 --- a/src/bun.js/bindings/JSDOMFile.h +++ b/src/bun.js/bindings/JSDOMFile.h @@ -4,4 +4,5 @@ namespace Bun { JSC::JSObject* createJSDOMFileConstructor(JSC::VM&, JSC::JSGlobalObject*); +JSC::Structure* createJSDOMFileInstanceStructure(JSC::VM&, JSC::JSGlobalObject*); } diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index 9cb9399815..95cbeaf0a3 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -186,6 +186,7 @@ #include "webcore/JSMIMEParams.h" #include "JSNodePerformanceHooksHistogram.h" #include "JSS3File.h" +#include "JSBunFile.h" #include "S3Error.h" #include "ProcessBindingBuffer.h" #include "NodeValidator.h" @@ -1838,6 +1839,16 @@ void GlobalObject::finishCreation(VM& vm) init.set(result.toObject(init.owner)); }); + m_JSBunFileStructure.initLater( + [](const Initializer& init) { + init.set(Bun::createJSBunFileStructure(init.vm, init.owner)); + }); + + m_JSDOMFileStructure.initLater( + [](const Initializer& init) { + init.set(Bun::createJSDOMFileInstanceStructure(init.vm, init.owner)); + }); + m_JSS3FileStructure.initLater( [](const Initializer& init) { init.set(Bun::createJSS3FileStructure(init.vm, init.owner)); diff --git a/src/bun.js/bindings/ZigGlobalObject.h b/src/bun.js/bindings/ZigGlobalObject.h index 5225b67ddd..1c5c995940 100644 --- a/src/bun.js/bindings/ZigGlobalObject.h +++ b/src/bun.js/bindings/ZigGlobalObject.h @@ -514,6 +514,8 @@ public: \ V(public, LazyPropertyOfGlobalObject, m_processEnvObject) \ \ + V(public, LazyPropertyOfGlobalObject, m_JSBunFileStructure) \ + V(public, LazyPropertyOfGlobalObject, m_JSDOMFileStructure) \ V(public, LazyPropertyOfGlobalObject, m_JSS3FileStructure) \ V(public, LazyPropertyOfGlobalObject, m_S3ErrorStructure) \ \ diff --git a/src/bun.js/webcore/Blob.zig b/src/bun.js/webcore/Blob.zig index 28056fd2e0..79b339ef0b 100644 --- a/src/bun.js/webcore/Blob.zig +++ b/src/bun.js/webcore/Blob.zig @@ -1887,8 +1887,54 @@ pub fn estimatedSize(this: *Blob) usize { comptime { _ = JSDOMFile__hasInstance; + + // Export BunFile-specific methods for use by JSBunFile.cpp and JSDOMFile.cpp. + // These were previously generated by the code generator as part of Blob's prototype, + // but are now only on the BunFile/File prototypes. + @export(&bunfile_exports.getExists, .{ .name = "BlobPrototype__getExists" }); + @export(&bunfile_exports.doUnlink, .{ .name = "BlobPrototype__doUnlink" }); + @export(&bunfile_exports.doWrite, .{ .name = "BlobPrototype__doWrite" }); + @export(&bunfile_exports.getStat, .{ .name = "BlobPrototype__getStat" }); + @export(&bunfile_exports.getWriter, .{ .name = "BlobPrototype__getWriter" }); + @export(&bunfile_exports.getName, .{ .name = "BlobPrototype__getName" }); + @export(&bunfile_exports.setName, .{ .name = "BlobPrototype__setName" }); + @export(&bunfile_exports.getLastModified, .{ .name = "BlobPrototype__getLastModified" }); } +const bunfile_exports = struct { + pub fn getExists(thisValue: *Blob, globalObject: *jsc.JSGlobalObject, callFrame: *jsc.CallFrame) callconv(jsc.conv) jsc.JSValue { + return @call(bun.callmod_inline, jsc.toJSHostCall, .{ globalObject, @src(), Blob.getExists, .{ thisValue, globalObject, callFrame } }); + } + + pub fn doUnlink(thisValue: *Blob, globalObject: *jsc.JSGlobalObject, callFrame: *jsc.CallFrame) callconv(jsc.conv) jsc.JSValue { + return @call(bun.callmod_inline, jsc.toJSHostCall, .{ globalObject, @src(), Blob.doUnlink, .{ thisValue, globalObject, callFrame } }); + } + + pub fn doWrite(thisValue: *Blob, globalObject: *jsc.JSGlobalObject, callFrame: *jsc.CallFrame) callconv(jsc.conv) jsc.JSValue { + return @call(bun.callmod_inline, jsc.toJSHostCall, .{ globalObject, @src(), Blob.doWrite, .{ thisValue, globalObject, callFrame } }); + } + + pub fn getStat(thisValue: *Blob, globalObject: *jsc.JSGlobalObject, callFrame: *jsc.CallFrame) callconv(jsc.conv) jsc.JSValue { + return @call(bun.callmod_inline, jsc.toJSHostCall, .{ globalObject, @src(), Blob.getStat, .{ thisValue, globalObject, callFrame } }); + } + + pub fn getWriter(thisValue: *Blob, globalObject: *jsc.JSGlobalObject, callFrame: *jsc.CallFrame) callconv(jsc.conv) jsc.JSValue { + return @call(bun.callmod_inline, jsc.toJSHostCall, .{ globalObject, @src(), Blob.getWriter, .{ thisValue, globalObject, callFrame } }); + } + + pub fn getName(this: *Blob, thisValue: jsc.JSValue, globalObject: *jsc.JSGlobalObject) callconv(jsc.conv) jsc.JSValue { + return @call(bun.callmod_inline, jsc.toJSHostCall, .{ globalObject, @src(), Blob.getName, .{ this, thisValue, globalObject } }); + } + + pub fn setName(this: *Blob, thisValue: jsc.JSValue, globalObject: *jsc.JSGlobalObject, value: jsc.JSValue) callconv(jsc.conv) bool { + return @call(bun.callmod_inline, jsc.host_fn.toJSHostSetterValue, .{ globalObject, @call(bun.callmod_inline, Blob.setName, .{ this, thisValue, globalObject, value }) }); + } + + pub fn getLastModified(this: *Blob, globalObject: *jsc.JSGlobalObject) callconv(jsc.conv) jsc.JSValue { + return @call(bun.callmod_inline, Blob.getLastModified, .{ this, globalObject }); + } +}; + pub fn constructBunFile( globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame, @@ -2998,7 +3044,7 @@ pub fn getName( pub fn setName( this: *Blob, - jsThis: jsc.JSValue, + _: jsc.JSValue, globalThis: *jsc.JSGlobalObject, value: JSValue, ) JSError!void { @@ -3006,7 +3052,6 @@ pub fn setName( if (value.isEmptyOrUndefinedOrNull()) { this.name.deref(); this.name = bun.String.dead; - js.nameSetCached(jsThis, globalThis, value); return; } if (value.isString()) { @@ -3015,7 +3060,6 @@ pub fn setName( errdefer this.name = bun.String.empty; this.name = try bun.String.fromJS(value, globalThis); // We don't need to increment the reference count since tryFromJS already did it. - js.nameSetCached(jsThis, globalThis, value); old_name.deref(); } } @@ -3465,9 +3509,15 @@ pub fn toJS(this: *Blob, globalObject: *jsc.JSGlobalObject) jsc.JSValue { return S3File.toJSUnchecked(globalObject, this); } + if (this.isBunFile()) { + return BUN__createJSBunFileUnsafely(globalObject, this); + } + return js.toJSUnchecked(globalObject, this); } +extern fn BUN__createJSBunFileUnsafely(*jsc.JSGlobalObject, *Blob) callconv(jsc.conv) jsc.JSValue; + pub fn deinit(this: *Blob) void { this.detach(); this.name.deref(); diff --git a/src/bun.js/webcore/response.classes.ts b/src/bun.js/webcore/response.classes.ts index 2efa48a44d..2ab73d4900 100644 --- a/src/bun.js/webcore/response.classes.ts +++ b/src/bun.js/webcore/response.classes.ts @@ -156,7 +156,6 @@ export default [ slice: { fn: "getSlice", length: 2 }, stream: { fn: "getStream", length: 1 }, formData: { fn: "getFormData", async: true }, - exists: { fn: "getExists", length: 0 }, // Non-standard, but consistent! bytes: { fn: "getBytes", async: true }, @@ -165,34 +164,9 @@ export default [ getter: "getType", }, - // TODO: Move this to a separate `File` object or BunFile - // This is *not* spec-compliant. - name: { - this: true, - cache: true, - getter: "getName", - setter: "setName", - }, - - // TODO: Move this to a separate `File` object or BunFile - // This is *not* spec-compliant. - lastModified: { - getter: "getLastModified", - }, - - // Non-standard, s3 + BunFile support - unlink: { fn: "doUnlink", length: 0 }, - delete: { fn: "doUnlink", length: 0 }, - write: { fn: "doWrite", length: 2 }, size: { getter: "getSize", }, - stat: { fn: "getStat", length: 0 }, - - writer: { - fn: "getWriter", - length: 1, - }, }, }), ]; diff --git a/test/js/web/fetch/blob.test.ts b/test/js/web/fetch/blob.test.ts index 7742f40dab..8dfae2c1ba 100644 --- a/test/js/web/fetch/blob.test.ts +++ b/test/js/web/fetch/blob.test.ts @@ -204,10 +204,11 @@ test("blob: can set name property #10178", () => { blob.name = "logo.svg"; // @ts-expect-error expect(blob.name).toBe("logo.svg"); + // name is now a plain data property on Blob (not a typed setter), so any value is accepted // @ts-expect-error blob.name = 10; // @ts-expect-error - expect(blob.name).toBe("logo.svg"); + expect(blob.name).toBe(10); Object.defineProperty(blob, "name", { value: 42, writable: false, @@ -226,10 +227,11 @@ test("blob: can set name property #10178", () => { const myBlob = new MyBlob([Buffer.from("Hello, World")]); // @ts-expect-error expect(myBlob.name).toBe("logo.svg"); + // name is now a plain data property on Blob (not a typed setter), so any value is accepted // @ts-expect-error myBlob.name = 10; // @ts-expect-error - expect(myBlob.name).toBe("logo.svg"); + expect(myBlob.name).toBe(10); Object.defineProperty(myBlob, "name", { value: 42, writable: false, diff --git a/test/regression/issue/26967.test.ts b/test/regression/issue/26967.test.ts new file mode 100644 index 0000000000..fdeef3e4da --- /dev/null +++ b/test/regression/issue/26967.test.ts @@ -0,0 +1,97 @@ +import { expect, test } from "bun:test"; + +test("Bun.file() returns BunFile with correct constructor.name", () => { + const file = Bun.file("file.txt"); + expect(file.constructor.name).toBe("BunFile"); +}); + +test("Bun.file() returns BunFile instance that is instanceof Blob", () => { + const file = Bun.file("file.txt"); + expect(file).toBeInstanceOf(Blob); +}); + +test("Bun.file() instance has BunFile-specific methods", () => { + const file = Bun.file("file.txt"); + expect(typeof file.exists).toBe("function"); + expect(typeof file.write).toBe("function"); + expect(typeof file.unlink).toBe("function"); + expect(typeof file.delete).toBe("function"); + expect(typeof file.stat).toBe("function"); + expect(typeof file.writer).toBe("function"); + expect("name" in file).toBe(true); + expect("lastModified" in file).toBe(true); +}); + +test("Bun.file() instance has Blob standard methods", () => { + const file = Bun.file("file.txt"); + expect(typeof file.text).toBe("function"); + expect(typeof file.arrayBuffer).toBe("function"); + expect(typeof file.json).toBe("function"); + expect(typeof file.slice).toBe("function"); + expect(typeof file.stream).toBe("function"); + expect(typeof file.formData).toBe("function"); + expect(typeof file.bytes).toBe("function"); +}); + +test("Blob.prototype does not have BunFile-specific methods", () => { + expect("exists" in Blob.prototype).toBe(false); + expect("write" in Blob.prototype).toBe(false); + expect("unlink" in Blob.prototype).toBe(false); + expect("delete" in Blob.prototype).toBe(false); + expect("stat" in Blob.prototype).toBe(false); + expect("writer" in Blob.prototype).toBe(false); + expect("name" in Blob.prototype).toBe(false); + expect("lastModified" in Blob.prototype).toBe(false); +}); + +test("new Blob() does not have BunFile-specific methods", () => { + const blob = new Blob(["hello"]); + expect("exists" in blob).toBe(false); + expect("write" in blob).toBe(false); + expect("unlink" in blob).toBe(false); + expect("delete" in blob).toBe(false); + expect("stat" in blob).toBe(false); + expect("writer" in blob).toBe(false); + expect("name" in blob).toBe(false); + expect("lastModified" in blob).toBe(false); +}); + +test("new Blob() has standard Blob methods", () => { + const blob = new Blob(["hello"]); + expect(blob.constructor.name).toBe("Blob"); + expect(typeof blob.text).toBe("function"); + expect(typeof blob.arrayBuffer).toBe("function"); + expect(typeof blob.slice).toBe("function"); + expect(typeof blob.stream).toBe("function"); +}); + +test("File has proper prototype chain (not sharing Blob.prototype)", () => { + expect(File.prototype).not.toBe(Blob.prototype); + expect(Object.getPrototypeOf(File.prototype)).toBe(Blob.prototype); +}); + +test("new File() has name and lastModified", () => { + const file = new File(["x"], "test.txt"); + expect(file.name).toBe("test.txt"); + expect(typeof file.lastModified).toBe("number"); + expect(file.constructor.name).toBe("File"); + expect(file).toBeInstanceOf(File); + expect(file).toBeInstanceOf(Blob); +}); + +test("BunFile prototype chain is correct", () => { + const file = Bun.file("file.txt"); + const proto = Object.getPrototypeOf(file); + + // BunFile prototype -> Blob.prototype -> Object.prototype + expect(proto).not.toBe(Blob.prototype); + expect(Object.getPrototypeOf(proto)).toBe(Blob.prototype); + + // Symbol.toStringTag + expect(Object.prototype.toString.call(file)).toBe("[object BunFile]"); +}); + +test("BunFile constructor throws when called directly", () => { + const file = Bun.file("file.txt"); + expect(() => new (file.constructor as any)()).toThrow("BunFile is not constructable"); +});