diff --git a/cmake/sources/CxxSources.txt b/cmake/sources/CxxSources.txt index 0c4bcc6d58..8a00c70ae6 100644 --- a/cmake/sources/CxxSources.txt +++ b/cmake/sources/CxxSources.txt @@ -94,6 +94,7 @@ src/bun.js/bindings/JSX509CertificateConstructor.cpp src/bun.js/bindings/JSX509CertificatePrototype.cpp src/bun.js/bindings/JSYogaConfig.cpp src/bun.js/bindings/JSYogaConstructor.cpp +src/bun.js/bindings/JSYogaExports.cpp src/bun.js/bindings/JSYogaNode.cpp src/bun.js/bindings/JSYogaPrototype.cpp src/bun.js/bindings/linux_perf_tracing.cpp diff --git a/src/bun.js/bindings/JSYogaConfig.cpp b/src/bun.js/bindings/JSYogaConfig.cpp index 01dbd41982..b4368daaaa 100644 --- a/src/bun.js/bindings/JSYogaConfig.cpp +++ b/src/bun.js/bindings/JSYogaConfig.cpp @@ -16,8 +16,10 @@ JSYogaConfig::JSYogaConfig(JSC::VM& vm, JSC::Structure* structure) JSYogaConfig::~JSYogaConfig() { + // Only free if not already freed via free() method if (m_config) { YGConfigFree(m_config); + m_config = nullptr; } } diff --git a/src/bun.js/bindings/JSYogaConfig.h b/src/bun.js/bindings/JSYogaConfig.h index 5eec85550a..bf79fe2c8c 100644 --- a/src/bun.js/bindings/JSYogaConfig.h +++ b/src/bun.js/bindings/JSYogaConfig.h @@ -25,6 +25,7 @@ public: DECLARE_INFO; YGConfigRef internal() { return m_config; } + void clearInternal() { m_config = nullptr; } private: JSYogaConfig(JSC::VM&, JSC::Structure*); diff --git a/src/bun.js/bindings/JSYogaConstructor.cpp b/src/bun.js/bindings/JSYogaConstructor.cpp index 23f3e7852c..ac5566aa0b 100644 --- a/src/bun.js/bindings/JSYogaConstructor.cpp +++ b/src/bun.js/bindings/JSYogaConstructor.cpp @@ -28,6 +28,9 @@ void JSYogaConfigConstructor::finishCreation(JSC::VM& vm, JSC::JSObject* prototy { Base::finishCreation(vm, 0, "Config"_s); putDirectWithoutTransition(vm, vm.propertyNames->prototype, prototype, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly); + + // Add static methods - create() is an alias for the constructor + putDirectNativeFunction(vm, this->globalObject(), JSC::Identifier::fromString(vm, "create"_s), 0, constructJSYogaConfig, ImplementationVisibility::Public, NoIntrinsic, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly); } // Node Constructor implementation @@ -42,6 +45,9 @@ void JSYogaNodeConstructor::finishCreation(JSC::VM& vm, JSC::JSObject* prototype { Base::finishCreation(vm, 1, "Node"_s); // 1 for optional config parameter putDirectWithoutTransition(vm, vm.propertyNames->prototype, prototype, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly); + + // Add static methods - create() is an alias for the constructor + putDirectNativeFunction(vm, this->globalObject(), JSC::Identifier::fromString(vm, "create"_s), 1, constructJSYogaNode, ImplementationVisibility::Public, NoIntrinsic, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly); } // Constructor functions diff --git a/src/bun.js/bindings/JSYogaExports.cpp b/src/bun.js/bindings/JSYogaExports.cpp new file mode 100644 index 0000000000..ce76b82314 --- /dev/null +++ b/src/bun.js/bindings/JSYogaExports.cpp @@ -0,0 +1,17 @@ +#include "root.h" +#include "JSYogaConstructor.h" +#include "ZigGlobalObject.h" + +extern "C" { + +JSC::EncodedJSValue Bun__JSYogaConfigConstructor(Zig::GlobalObject* globalObject) +{ + return JSValue::encode(globalObject->m_JSYogaConfigClassStructure.constructor(globalObject)); +} + +JSC::EncodedJSValue Bun__JSYogaNodeConstructor(Zig::GlobalObject* globalObject) +{ + return JSValue::encode(globalObject->m_JSYogaNodeClassStructure.constructor(globalObject)); +} + +} // extern "C" \ No newline at end of file diff --git a/src/bun.js/bindings/JSYogaPrototype.cpp b/src/bun.js/bindings/JSYogaPrototype.cpp index f35e9b5668..3c2e547f4e 100644 --- a/src/bun.js/bindings/JSYogaPrototype.cpp +++ b/src/bun.js/bindings/JSYogaPrototype.cpp @@ -4,6 +4,7 @@ #include "JSYogaNode.h" #include #include +#include namespace Bun { @@ -16,7 +17,11 @@ static JSC_DECLARE_HOST_FUNCTION(jsYogaConfigProtoFuncUseWebDefaults); static JSC_DECLARE_HOST_FUNCTION(jsYogaConfigProtoFuncSetExperimentalFeatureEnabled); static JSC_DECLARE_HOST_FUNCTION(jsYogaConfigProtoFuncIsExperimentalFeatureEnabled); static JSC_DECLARE_HOST_FUNCTION(jsYogaConfigProtoFuncSetPointScaleFactor); +static JSC_DECLARE_HOST_FUNCTION(jsYogaConfigProtoFuncGetPointScaleFactor); +static JSC_DECLARE_HOST_FUNCTION(jsYogaConfigProtoFuncSetErrata); +static JSC_DECLARE_HOST_FUNCTION(jsYogaConfigProtoFuncGetErrata); static JSC_DECLARE_HOST_FUNCTION(jsYogaConfigProtoFuncIsEnabledForNodes); +static JSC_DECLARE_HOST_FUNCTION(jsYogaConfigProtoFuncFree); // Hash table for Config prototype properties static const JSC::HashTableValue JSYogaConfigPrototypeTableValues[] = { @@ -25,7 +30,11 @@ static const JSC::HashTableValue JSYogaConfigPrototypeTableValues[] = { { "setExperimentalFeatureEnabled"_s, static_cast(JSC::PropertyAttribute::Function), JSC::NoIntrinsic, { JSC::HashTableValue::NativeFunctionType, jsYogaConfigProtoFuncSetExperimentalFeatureEnabled, 2 } }, { "isExperimentalFeatureEnabled"_s, static_cast(JSC::PropertyAttribute::Function), JSC::NoIntrinsic, { JSC::HashTableValue::NativeFunctionType, jsYogaConfigProtoFuncIsExperimentalFeatureEnabled, 1 } }, { "setPointScaleFactor"_s, static_cast(JSC::PropertyAttribute::Function), JSC::NoIntrinsic, { JSC::HashTableValue::NativeFunctionType, jsYogaConfigProtoFuncSetPointScaleFactor, 1 } }, + { "getPointScaleFactor"_s, static_cast(JSC::PropertyAttribute::Function), JSC::NoIntrinsic, { JSC::HashTableValue::NativeFunctionType, jsYogaConfigProtoFuncGetPointScaleFactor, 0 } }, + { "setErrata"_s, static_cast(JSC::PropertyAttribute::Function), JSC::NoIntrinsic, { JSC::HashTableValue::NativeFunctionType, jsYogaConfigProtoFuncSetErrata, 1 } }, + { "getErrata"_s, static_cast(JSC::PropertyAttribute::Function), JSC::NoIntrinsic, { JSC::HashTableValue::NativeFunctionType, jsYogaConfigProtoFuncGetErrata, 0 } }, { "isEnabledForNodes"_s, static_cast(JSC::PropertyAttribute::Function), JSC::NoIntrinsic, { JSC::HashTableValue::NativeFunctionType, jsYogaConfigProtoFuncIsEnabledForNodes, 1 } }, + { "free"_s, static_cast(JSC::PropertyAttribute::Function), JSC::NoIntrinsic, { JSC::HashTableValue::NativeFunctionType, jsYogaConfigProtoFuncFree, 0 } }, }; void JSYogaConfigPrototype::finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject) @@ -61,34 +70,193 @@ void JSYogaNodePrototype::finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globa JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); } -// Placeholder implementations for now - these will be filled in Phase 2/3 +// Config method implementations JSC_DEFINE_HOST_FUNCTION(jsYogaConfigProtoFuncSetUseWebDefaults, (JSC::JSGlobalObject* globalObject, JSC::CallFrame* callFrame)) { + JSC::VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto* thisObject = jsDynamicCast(callFrame->thisValue()); + if (UNLIKELY(!thisObject)) { + return JSC::JSValue::encode(Bun::throwThisTypeError(*globalObject, scope, "Yoga.Config"_s, "setUseWebDefaults"_s)); + } + + bool enabled = true; + if (callFrame->argumentCount() > 0) { + enabled = callFrame->uncheckedArgument(0).toBoolean(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + } + + YGConfigSetUseWebDefaults(thisObject->internal(), enabled); return JSC::JSValue::encode(JSC::jsUndefined()); } JSC_DEFINE_HOST_FUNCTION(jsYogaConfigProtoFuncUseWebDefaults, (JSC::JSGlobalObject* globalObject, JSC::CallFrame* callFrame)) { + JSC::VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto* thisObject = jsDynamicCast(callFrame->thisValue()); + if (UNLIKELY(!thisObject)) { + return JSC::JSValue::encode(Bun::throwThisTypeError(*globalObject, scope, "Yoga.Config"_s, "useWebDefaults"_s)); + } + + // Legacy method - same as setUseWebDefaults(true) + YGConfigSetUseWebDefaults(thisObject->internal(), true); return JSC::JSValue::encode(JSC::jsUndefined()); } JSC_DEFINE_HOST_FUNCTION(jsYogaConfigProtoFuncSetExperimentalFeatureEnabled, (JSC::JSGlobalObject* globalObject, JSC::CallFrame* callFrame)) { + JSC::VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto* thisObject = jsDynamicCast(callFrame->thisValue()); + if (UNLIKELY(!thisObject)) { + return JSC::JSValue::encode(Bun::throwThisTypeError(*globalObject, scope, "Yoga.Config"_s, "setExperimentalFeatureEnabled"_s)); + } + + if (callFrame->argumentCount() < 2) { + throwTypeError(globalObject, scope, "setExperimentalFeatureEnabled requires 2 arguments"_s); + return {}; + } + + int32_t feature = callFrame->uncheckedArgument(0).toInt32(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + + bool enabled = callFrame->uncheckedArgument(1).toBoolean(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + + YGConfigSetExperimentalFeatureEnabled(thisObject->internal(), static_cast(feature), enabled); return JSC::JSValue::encode(JSC::jsUndefined()); } JSC_DEFINE_HOST_FUNCTION(jsYogaConfigProtoFuncIsExperimentalFeatureEnabled, (JSC::JSGlobalObject* globalObject, JSC::CallFrame* callFrame)) { - return JSC::JSValue::encode(JSC::jsUndefined()); + JSC::VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto* thisObject = jsDynamicCast(callFrame->thisValue()); + if (UNLIKELY(!thisObject)) { + return JSC::JSValue::encode(Bun::throwThisTypeError(*globalObject, scope, "Yoga.Config"_s, "isExperimentalFeatureEnabled"_s)); + } + + if (callFrame->argumentCount() < 1) { + throwTypeError(globalObject, scope, "isExperimentalFeatureEnabled requires 1 argument"_s); + return {}; + } + + int32_t feature = callFrame->uncheckedArgument(0).toInt32(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + + bool enabled = YGConfigIsExperimentalFeatureEnabled(thisObject->internal(), static_cast(feature)); + return JSC::JSValue::encode(JSC::jsBoolean(enabled)); } JSC_DEFINE_HOST_FUNCTION(jsYogaConfigProtoFuncSetPointScaleFactor, (JSC::JSGlobalObject* globalObject, JSC::CallFrame* callFrame)) { + JSC::VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto* thisObject = jsDynamicCast(callFrame->thisValue()); + if (UNLIKELY(!thisObject)) { + return JSC::JSValue::encode(Bun::throwThisTypeError(*globalObject, scope, "Yoga.Config"_s, "setPointScaleFactor"_s)); + } + + if (callFrame->argumentCount() < 1) { + throwTypeError(globalObject, scope, "setPointScaleFactor requires 1 argument"_s); + return {}; + } + + double scaleFactor = callFrame->uncheckedArgument(0).toNumber(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + + YGConfigSetPointScaleFactor(thisObject->internal(), static_cast(scaleFactor)); return JSC::JSValue::encode(JSC::jsUndefined()); } JSC_DEFINE_HOST_FUNCTION(jsYogaConfigProtoFuncIsEnabledForNodes, (JSC::JSGlobalObject* globalObject, JSC::CallFrame* callFrame)) { + JSC::VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto* thisObject = jsDynamicCast(callFrame->thisValue()); + if (UNLIKELY(!thisObject)) { + return JSC::JSValue::encode(Bun::throwThisTypeError(*globalObject, scope, "Yoga.Config"_s, "isEnabledForNodes"_s)); + } + + // This method checks if a config is actively being used by any nodes + // In the future, we might track this, but for now always return true if valid config + return JSC::JSValue::encode(JSC::jsBoolean(thisObject->internal() != nullptr)); +} + +JSC_DEFINE_HOST_FUNCTION(jsYogaConfigProtoFuncGetPointScaleFactor, (JSC::JSGlobalObject* globalObject, JSC::CallFrame* callFrame)) +{ + JSC::VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto* thisObject = jsDynamicCast(callFrame->thisValue()); + if (UNLIKELY(!thisObject)) { + return JSC::JSValue::encode(Bun::throwThisTypeError(*globalObject, scope, "Yoga.Config"_s, "getPointScaleFactor"_s)); + } + + float scaleFactor = YGConfigGetPointScaleFactor(thisObject->internal()); + return JSC::JSValue::encode(JSC::jsNumber(scaleFactor)); +} + +JSC_DEFINE_HOST_FUNCTION(jsYogaConfigProtoFuncSetErrata, (JSC::JSGlobalObject* globalObject, JSC::CallFrame* callFrame)) +{ + JSC::VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto* thisObject = jsDynamicCast(callFrame->thisValue()); + if (UNLIKELY(!thisObject)) { + return JSC::JSValue::encode(Bun::throwThisTypeError(*globalObject, scope, "Yoga.Config"_s, "setErrata"_s)); + } + + if (callFrame->argumentCount() < 1) { + throwTypeError(globalObject, scope, "setErrata requires 1 argument"_s); + return {}; + } + + int32_t errata = callFrame->uncheckedArgument(0).toInt32(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + + YGConfigSetErrata(thisObject->internal(), static_cast(errata)); + return JSC::JSValue::encode(JSC::jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsYogaConfigProtoFuncGetErrata, (JSC::JSGlobalObject* globalObject, JSC::CallFrame* callFrame)) +{ + JSC::VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto* thisObject = jsDynamicCast(callFrame->thisValue()); + if (UNLIKELY(!thisObject)) { + return JSC::JSValue::encode(Bun::throwThisTypeError(*globalObject, scope, "Yoga.Config"_s, "getErrata"_s)); + } + + YGErrata errata = YGConfigGetErrata(thisObject->internal()); + return JSC::JSValue::encode(JSC::jsNumber(static_cast(errata))); +} + +JSC_DEFINE_HOST_FUNCTION(jsYogaConfigProtoFuncFree, (JSC::JSGlobalObject* globalObject, JSC::CallFrame* callFrame)) +{ + JSC::VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto* thisObject = jsDynamicCast(callFrame->thisValue()); + if (UNLIKELY(!thisObject)) { + return JSC::JSValue::encode(Bun::throwThisTypeError(*globalObject, scope, "Yoga.Config"_s, "free"_s)); + } + + // Mark the config as freed by setting internal pointer to nullptr + // The actual cleanup will happen in the destructor + if (thisObject->internal()) { + YGConfigFree(thisObject->internal()); + thisObject->clearInternal(); + } + return JSC::JSValue::encode(JSC::jsUndefined()); } diff --git a/test/js/bun/yoga-config.test.js b/test/js/bun/yoga-config.test.js new file mode 100644 index 0000000000..5302b6fb21 --- /dev/null +++ b/test/js/bun/yoga-config.test.js @@ -0,0 +1,100 @@ +import { describe, expect, test } from "bun:test"; + +describe("Yoga.Config", () => { + test("Config constructor", () => { + const config = new Yoga.Config(); + expect(config).toBeDefined(); + expect(config.constructor.name).toBe("Config"); + }); + + test("Config.create() static method", () => { + const config = Yoga.Config.create(); + expect(config).toBeDefined(); + expect(config.constructor.name).toBe("Config"); + }); + + test("setUseWebDefaults", () => { + const config = new Yoga.Config(); + + // Should not throw + expect(() => config.setUseWebDefaults(true)).not.toThrow(); + expect(() => config.setUseWebDefaults(false)).not.toThrow(); + expect(() => config.setUseWebDefaults()).not.toThrow(); // defaults to true + }); + + test("useWebDefaults (legacy)", () => { + const config = new Yoga.Config(); + + // Should not throw + expect(() => config.useWebDefaults()).not.toThrow(); + }); + + test("setPointScaleFactor and getPointScaleFactor", () => { + const config = new Yoga.Config(); + + config.setPointScaleFactor(2.0); + expect(config.getPointScaleFactor()).toBe(2.0); + + config.setPointScaleFactor(0); // disable pixel rounding + expect(config.getPointScaleFactor()).toBe(0); + + config.setPointScaleFactor(3.5); + expect(config.getPointScaleFactor()).toBe(3.5); + }); + + test("setErrata and getErrata", () => { + const config = new Yoga.Config(); + + // Test with different errata values + config.setErrata(Yoga.Errata.None); + expect(config.getErrata()).toBe(Yoga.Errata.None); + + config.setErrata(Yoga.Errata.Classic); + expect(config.getErrata()).toBe(Yoga.Errata.Classic); + + config.setErrata(Yoga.Errata.All); + expect(config.getErrata()).toBe(Yoga.Errata.All); + }); + + test("setExperimentalFeatureEnabled and isExperimentalFeatureEnabled", () => { + const config = new Yoga.Config(); + + // Test with a hypothetical experimental feature + const feature = 0; // Assuming 0 is a valid experimental feature + + config.setExperimentalFeatureEnabled(feature, true); + expect(config.isExperimentalFeatureEnabled(feature)).toBe(true); + + config.setExperimentalFeatureEnabled(feature, false); + expect(config.isExperimentalFeatureEnabled(feature)).toBe(false); + }); + + test("isEnabledForNodes", () => { + const config = new Yoga.Config(); + + // Should return true for a valid config + expect(config.isEnabledForNodes()).toBe(true); + }); + + test("free", () => { + const config = new Yoga.Config(); + + // Should not throw + expect(() => config.free()).not.toThrow(); + + // After free, methods should throw or handle gracefully + // This depends on implementation - for now just test it doesn't crash + expect(() => config.free()).not.toThrow(); // double free should be safe + }); + + test("error handling", () => { + const config = new Yoga.Config(); + + // Test invalid arguments + expect(() => config.setErrata()).toThrow(); + expect(() => config.setExperimentalFeatureEnabled()).toThrow(); + expect(() => config.setExperimentalFeatureEnabled(0)).toThrow(); // missing second arg + expect(() => config.isExperimentalFeatureEnabled()).toThrow(); + expect(() => config.setPointScaleFactor()).toThrow(); + }); +}); \ No newline at end of file