diff options
author | 2022-09-03 03:57:43 -0700 | |
---|---|---|
committer | 2022-09-03 03:57:43 -0700 | |
commit | 0ca42e81f31d31270c44f81d88fff33361817720 (patch) | |
tree | 27e27b32d1850a1fada1a002207e3254cbc597e8 /src/bun.js/bindings/BunPlugin.cpp | |
parent | 8b2d07e82ee6249e0886109585b63429ccf3ab3a (diff) | |
download | bun-0ca42e81f31d31270c44f81d88fff33361817720.tar.gz bun-0ca42e81f31d31270c44f81d88fff33361817720.tar.zst bun-0ca42e81f31d31270c44f81d88fff33361817720.zip |
Plugin API (#1199)
* Plugin API
* Fix the bugs
* Implement `"object"` loader
Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com>
Diffstat (limited to 'src/bun.js/bindings/BunPlugin.cpp')
-rw-r--r-- | src/bun.js/bindings/BunPlugin.cpp | 505 |
1 files changed, 505 insertions, 0 deletions
diff --git a/src/bun.js/bindings/BunPlugin.cpp b/src/bun.js/bindings/BunPlugin.cpp new file mode 100644 index 000000000..0941d2722 --- /dev/null +++ b/src/bun.js/bindings/BunPlugin.cpp @@ -0,0 +1,505 @@ +#include "BunPlugin.h" + +#include "headers-handwritten.h" +#include "JavaScriptCore/CatchScope.h" +#include "JavaScriptCore/JSGlobalObject.h" +#include "JavaScriptCore/JSTypeInfo.h" +#include "JavaScriptCore/Structure.h" +#include "helpers.h" +#include "ZigGlobalObject.h" +#include "JavaScriptCore/JavaScript.h" +#include "JavaScriptCore/JSObjectInlines.h" +#include "wtf/text/WTFString.h" +#include "JavaScriptCore/JSCInlines.h" + +#include "JavaScriptCore/ObjectConstructor.h" +#include "JavaScriptCore/SubspaceInlines.h" +#include "JavaScriptCore/RegExpObject.h" + +#include "JavaScriptCore/RegularExpression.h" + +namespace Zig { + +extern "C" void Bun__onDidAppendPlugin(void* bunVM, JSGlobalObject* globalObject); + +static bool isValidNamespaceString(String& namespaceString) +{ + static JSC::Yarr::RegularExpression* namespaceRegex = nullptr; + if (!namespaceRegex) { + namespaceRegex = new JSC::Yarr::RegularExpression("^([a-zA-Z0-9_\\-]+)$"_s); + } + return namespaceRegex->match(namespaceString) > -1; +} + +static EncodedJSValue jsFunctionAppendOnLoadPluginBody(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callframe, BunPluginTarget target) +{ + JSC::VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + if (callframe->argumentCount() < 2) { + throwException(globalObject, scope, createError(globalObject, "onLoad() requires at least 2 arguments"_s)); + return JSValue::encode(jsUndefined()); + } + + auto* filterObject = callframe->uncheckedArgument(0).toObject(globalObject); + RETURN_IF_EXCEPTION(scope, encodedJSValue()); + auto clientData = WebCore::clientData(vm); + auto& builtinNames = clientData->builtinNames(); + JSC::RegExpObject* filter = nullptr; + if (JSValue filterValue = filterObject->getIfPropertyExists(globalObject, builtinNames.filterPublicName())) { + if (filterValue.isCell() && filterValue.asCell()->inherits<JSC::RegExpObject>()) + filter = jsCast<JSC::RegExpObject*>(filterValue); + } + + if (!filter) { + throwException(globalObject, scope, createError(globalObject, "onLoad() expects first argument to be an object with a filter RegExp"_s)); + return JSValue::encode(jsUndefined()); + } + + String namespaceString = String(); + if (JSValue namespaceValue = filterObject->getIfPropertyExists(globalObject, Identifier::fromString(vm, "namespace"_s))) { + if (namespaceValue.isString()) { + namespaceString = namespaceValue.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, encodedJSValue()); + if (!isValidNamespaceString(namespaceString)) { + throwException(globalObject, scope, createError(globalObject, "namespace can only contain letters, numbers, dashes, or underscores"_s)); + return JSValue::encode(jsUndefined()); + } + } + RETURN_IF_EXCEPTION(scope, encodedJSValue()); + } + + auto func = callframe->uncheckedArgument(1); + RETURN_IF_EXCEPTION(scope, encodedJSValue()); + + if (!func.isCell() || !func.isCallable()) { + throwException(globalObject, scope, createError(globalObject, "onLoad() expects second argument to be a function"_s)); + return JSValue::encode(jsUndefined()); + } + + Zig::GlobalObject* global = reinterpret_cast<Zig::GlobalObject*>(globalObject); + auto& plugins = global->onLoadPlugins[target]; + plugins.append(vm, filter->regExp(), jsCast<JSFunction*>(func), namespaceString); + Bun__onDidAppendPlugin(reinterpret_cast<Zig::GlobalObject*>(globalObject)->bunVM(), globalObject); + return JSValue::encode(jsUndefined()); +} + +static EncodedJSValue jsFunctionAppendOnResolvePluginBody(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callframe, BunPluginTarget target) +{ + JSC::VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + if (callframe->argumentCount() < 2) { + throwException(globalObject, scope, createError(globalObject, "onResolve() requires at least 2 arguments"_s)); + return JSValue::encode(jsUndefined()); + } + + auto* filterObject = callframe->uncheckedArgument(0).toObject(globalObject); + RETURN_IF_EXCEPTION(scope, encodedJSValue()); + auto clientData = WebCore::clientData(vm); + auto& builtinNames = clientData->builtinNames(); + JSC::RegExpObject* filter = nullptr; + if (JSValue filterValue = filterObject->getIfPropertyExists(globalObject, builtinNames.filterPublicName())) { + if (filterValue.isCell() && filterValue.asCell()->inherits<JSC::RegExpObject>()) + filter = jsCast<JSC::RegExpObject*>(filterValue); + } + + if (!filter) { + throwException(globalObject, scope, createError(globalObject, "onResolve() expects first argument to be an object with a filter RegExp"_s)); + return JSValue::encode(jsUndefined()); + } + + String namespaceString = String(); + if (JSValue namespaceValue = filterObject->getIfPropertyExists(globalObject, Identifier::fromString(vm, "namespace"_s))) { + if (namespaceValue.isString()) { + namespaceString = namespaceValue.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, encodedJSValue()); + if (!isValidNamespaceString(namespaceString)) { + throwException(globalObject, scope, createError(globalObject, "namespace can only contain letters, numbers, dashes, or underscores"_s)); + return JSValue::encode(jsUndefined()); + } + } + + RETURN_IF_EXCEPTION(scope, encodedJSValue()); + } + + auto func = callframe->uncheckedArgument(1); + RETURN_IF_EXCEPTION(scope, encodedJSValue()); + + if (!func.isCell() || !func.isCallable()) { + throwException(globalObject, scope, createError(globalObject, "onResolve() expects second argument to be a function"_s)); + return JSValue::encode(jsUndefined()); + } + + Zig::GlobalObject* global = reinterpret_cast<Zig::GlobalObject*>(globalObject); + auto& plugins = global->onResolvePlugins[target]; + plugins.append(vm, filter->regExp(), jsCast<JSFunction*>(func), namespaceString); + + Bun__onDidAppendPlugin(reinterpret_cast<Zig::GlobalObject*>(globalObject)->bunVM(), globalObject); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsFunctionAppendOnLoadPluginNode, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe)) +{ + return jsFunctionAppendOnLoadPluginBody(globalObject, callframe, BunPluginTargetNode); +} + +JSC_DEFINE_HOST_FUNCTION(jsFunctionAppendOnLoadPluginBun, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe)) +{ + return jsFunctionAppendOnLoadPluginBody(globalObject, callframe, BunPluginTargetBun); +} + +JSC_DEFINE_HOST_FUNCTION(jsFunctionAppendOnLoadPluginBrowser, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe)) +{ + return jsFunctionAppendOnLoadPluginBody(globalObject, callframe, BunPluginTargetBrowser); +} + +JSC_DEFINE_HOST_FUNCTION(jsFunctionAppendOnResolvePluginNode, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe)) +{ + return jsFunctionAppendOnResolvePluginBody(globalObject, callframe, BunPluginTargetNode); +} + +JSC_DEFINE_HOST_FUNCTION(jsFunctionAppendOnResolvePluginBun, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe)) +{ + return jsFunctionAppendOnResolvePluginBody(globalObject, callframe, BunPluginTargetBun); +} + +JSC_DEFINE_HOST_FUNCTION(jsFunctionAppendOnResolvePluginBrowser, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe)) +{ + return jsFunctionAppendOnResolvePluginBody(globalObject, callframe, BunPluginTargetBrowser); +} + +extern "C" EncodedJSValue jsFunctionBunPluginClear(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callframe) +{ + Zig::GlobalObject* global = reinterpret_cast<Zig::GlobalObject*>(globalObject); + for (uint8_t i = 0; i < BunPluginTargetMax + 1; i++) { + global->onLoadPlugins[i].fileNamespace.clear(); + global->onResolvePlugins[i].fileNamespace.clear(); + global->onLoadPlugins[i].groups.clear(); + global->onResolvePlugins[i].namespaces.clear(); + } + + return JSValue::encode(jsUndefined()); +} + +extern "C" EncodedJSValue jsFunctionBunPlugin(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callframe) +{ + JSC::VM& vm = globalObject->vm(); + auto clientData = WebCore::clientData(vm); + auto throwScope = DECLARE_THROW_SCOPE(vm); + if (callframe->argumentCount() < 1) { + JSC::throwTypeError(globalObject, throwScope, "Bun.plugin() needs at least one argument (an object)"_s); + return JSValue::encode(jsUndefined()); + } + + JSC::JSObject* obj = callframe->uncheckedArgument(0).getObject(); + if (!obj) { + JSC::throwTypeError(globalObject, throwScope, "Bun.plugin() needs an object as first argument"_s); + return JSValue::encode(jsUndefined()); + } + + JSC::JSValue setupFunctionValue = obj->get(globalObject, Identifier::fromString(vm, "setup"_s)); + if (!setupFunctionValue || setupFunctionValue.isUndefinedOrNull() || !setupFunctionValue.isCell() || !setupFunctionValue.isCallable()) { + JSC::throwTypeError(globalObject, throwScope, "Bun.plugin() needs a setup() function"_s); + return JSValue::encode(jsUndefined()); + } + + Zig::GlobalObject* global = reinterpret_cast<Zig::GlobalObject*>(globalObject); + BunPluginTarget target = global->defaultBunPluginTarget; + if (JSValue targetValue = obj->getIfPropertyExists(globalObject, Identifier::fromString(vm, "target"_s))) { + if (auto* targetJSString = targetValue.toStringOrNull(globalObject)) { + auto targetString = targetJSString->value(globalObject); + if (targetString == "node"_s) { + target = BunPluginTargetNode; + } else if (targetString == "bun"_s) { + target = BunPluginTargetBun; + } else if (targetString == "browser"_s) { + target = BunPluginTargetBrowser; + } else { + JSC::throwTypeError(globalObject, throwScope, "Bun.plugin() target must be one of 'node', 'bun' or 'browser'"_s); + return JSValue::encode(jsUndefined()); + } + } + } + + JSFunction* setupFunction = jsCast<JSFunction*>(setupFunctionValue); + JSObject* builderObject = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), 3); + + switch (target) { + case BunPluginTargetNode: { + builderObject->putDirect(vm, Identifier::fromString(vm, "target"_s), jsString(vm, String("node"_s)), 0); + builderObject->putDirectNativeFunction( + vm, + globalObject, + JSC::Identifier::fromString(vm, "onLoad"_s), + 1, + jsFunctionAppendOnLoadPluginNode, + ImplementationVisibility::Public, + NoIntrinsic, + JSC::PropertyAttribute::DontDelete | 0); + builderObject->putDirectNativeFunction( + vm, + globalObject, + JSC::Identifier::fromString(vm, "onResolve"_s), + 1, + jsFunctionAppendOnResolvePluginNode, + ImplementationVisibility::Public, + NoIntrinsic, + JSC::PropertyAttribute::DontDelete | 0); + break; + } + case BunPluginTargetBun: { + builderObject->putDirect(vm, Identifier::fromString(vm, "target"_s), jsString(vm, String("bun"_s)), 0); + builderObject->putDirectNativeFunction( + vm, + globalObject, + JSC::Identifier::fromString(vm, "onLoad"_s), + 1, + jsFunctionAppendOnLoadPluginBun, + ImplementationVisibility::Public, + NoIntrinsic, + JSC::PropertyAttribute::DontDelete | 0); + builderObject->putDirectNativeFunction( + vm, + globalObject, + JSC::Identifier::fromString(vm, "onResolve"_s), + 1, + jsFunctionAppendOnResolvePluginBun, + ImplementationVisibility::Public, + NoIntrinsic, + JSC::PropertyAttribute::DontDelete | 0); + break; + } + case BunPluginTargetBrowser: { + builderObject->putDirect(vm, Identifier::fromString(vm, "target"_s), jsString(vm, String("browser"_s)), 0); + builderObject->putDirectNativeFunction( + vm, + globalObject, + JSC::Identifier::fromString(vm, "onLoad"_s), + 1, + jsFunctionAppendOnLoadPluginBrowser, + ImplementationVisibility::Public, + NoIntrinsic, + JSC::PropertyAttribute::DontDelete | 0); + builderObject->putDirectNativeFunction( + vm, + globalObject, + JSC::Identifier::fromString(vm, "onResolve"_s), + 1, + jsFunctionAppendOnResolvePluginBrowser, + ImplementationVisibility::Public, + NoIntrinsic, + JSC::PropertyAttribute::DontDelete | 0); + break; + } + } + + JSC::MarkedArgumentBuffer args; + args.append(builderObject); + + JSFunction* function = jsCast<JSFunction*>(setupFunctionValue); + JSC::CallData callData = JSC::getCallData(function); + JSValue result = call(globalObject, function, callData, JSC::jsUndefined(), args); + RETURN_IF_EXCEPTION(throwScope, encodedJSValue()); + + if (auto* promise = JSC::jsDynamicCast<JSC::JSPromise*>(result)) { + JSC::throwTypeError(globalObject, throwScope, "setup() does not support promises yet"_s); + return JSValue::encode(jsUndefined()); + } + + RELEASE_AND_RETURN(throwScope, JSValue::encode(jsUndefined())); +} + +void BunPlugin::Group::append(JSC::VM& vm, JSC::RegExp* filter, JSC::JSFunction* func) +{ + filters.append(JSC::Strong<JSC::RegExp> { vm, filter }); + callbacks.append(JSC::Strong<JSC::JSFunction> { vm, func }); +} + +void BunPlugin::Base::append(JSC::VM& vm, JSC::RegExp* filter, JSC::JSFunction* func, String& namespaceString) +{ + if (namespaceString.isEmpty() || namespaceString == "file"_s) { + this->fileNamespace.append(vm, filter, func); + } else if (auto found = this->group(namespaceString)) { + found->append(vm, filter, func); + } else { + Group newGroup; + newGroup.append(vm, filter, func); + this->groups.append(WTFMove(newGroup)); + this->namespaces.append(namespaceString); + } +} + +JSFunction* BunPlugin::Group::find(JSC::JSGlobalObject* globalObject, String& path) +{ + size_t count = filters.size(); + for (size_t i = 0; i < count; i++) { + if (filters[i].get()->match(globalObject, path, 0)) { + return callbacks[i].get(); + } + } + + return nullptr; +} + +EncodedJSValue BunPlugin::OnLoad::run(JSC::JSGlobalObject* globalObject, ZigString* namespaceString, ZigString* path) +{ + Group* groupPtr = this->group(namespaceString ? Zig::toString(*namespaceString) : String()); + if (groupPtr == nullptr) { + return JSValue::encode(jsUndefined()); + } + Group& group = *groupPtr; + + auto pathString = Zig::toString(*path); + + JSC::JSFunction* function = group.find(globalObject, pathString); + if (!function) { + return JSValue::encode(JSC::jsUndefined()); + } + + JSC::MarkedArgumentBuffer arguments; + JSC::VM& vm = globalObject->vm(); + + JSC::JSObject* paramsObject = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), 1); + auto clientData = WebCore::clientData(vm); + auto& builtinNames = clientData->builtinNames(); + paramsObject->putDirect( + vm, clientData->builtinNames().pathPublicName(), + jsString(vm, pathString)); + arguments.append(paramsObject); + + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto scope = DECLARE_CATCH_SCOPE(vm); + scope.assertNoExceptionExceptTermination(); + + JSC::CallData callData = JSC::getCallData(function); + + auto result = call(globalObject, function, callData, JSC::jsUndefined(), arguments); + if (UNLIKELY(scope.exception())) { + JSC::Exception* exception = scope.exception(); + scope.clearException(); + return JSValue::encode(exception); + } + + if (auto* promise = JSC::jsDynamicCast<JSPromise*>(result)) { + switch (promise->status(vm)) { + case JSPromise::Status::Pending: { + JSC::throwTypeError(globalObject, throwScope, "onLoad() doesn't support pending promises yet"_s); + return JSValue::encode({}); + } + case JSPromise::Status::Rejected: { + promise->internalField(JSC::JSPromise::Field::Flags).set(vm, promise, jsNumber(static_cast<unsigned>(JSC::JSPromise::Status::Fulfilled))); + result = promise->result(vm); + return JSValue::encode(result); + } + case JSPromise::Status::Fulfilled: { + result = promise->result(vm); + break; + } + } + } + + if (!result.isObject()) { + JSC::throwTypeError(globalObject, throwScope, "onLoad() expects an object returned"_s); + return JSValue::encode({}); + } + + RELEASE_AND_RETURN(throwScope, JSValue::encode(result)); +} + +EncodedJSValue BunPlugin::OnResolve::run(JSC::JSGlobalObject* globalObject, ZigString* namespaceString, ZigString* path, ZigString* importer) +{ + Group* groupPtr = this->group(namespaceString ? Zig::toString(*namespaceString) : String()); + if (groupPtr == nullptr) { + return JSValue::encode(jsUndefined()); + } + Group& group = *groupPtr; + auto& filters = group.filters; + + if (filters.size() == 0) { + return JSValue::encode(jsUndefined()); + } + + auto& callbacks = group.callbacks; + + WTF::String pathString = Zig::toString(*path); + for (size_t i = 0; i < filters.size(); i++) { + if (!filters[i].get()->match(globalObject, pathString, 0)) { + continue; + } + JSC::JSFunction* function = callbacks[i].get(); + if (UNLIKELY(!function)) { + continue; + } + + JSC::MarkedArgumentBuffer arguments; + JSC::VM& vm = globalObject->vm(); + + JSC::JSObject* paramsObject = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), 2); + auto clientData = WebCore::clientData(vm); + auto& builtinNames = clientData->builtinNames(); + paramsObject->putDirect( + vm, clientData->builtinNames().pathPublicName(), + Zig::toJSStringValue(*path, globalObject)); + paramsObject->putDirect( + vm, clientData->builtinNames().importerPublicName(), + Zig::toJSStringValue(*importer, globalObject)); + arguments.append(paramsObject); + + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto scope = DECLARE_CATCH_SCOPE(vm); + scope.assertNoExceptionExceptTermination(); + + JSC::CallData callData = JSC::getCallData(function); + + auto result = call(globalObject, function, callData, JSC::jsUndefined(), arguments); + if (UNLIKELY(scope.exception())) { + JSC::Exception* exception = scope.exception(); + scope.clearException(); + return JSValue::encode(exception); + } + + if (result.isUndefinedOrNull()) { + continue; + } + + if (auto* promise = JSC::jsDynamicCast<JSPromise*>(result)) { + switch (promise->status(vm)) { + case JSPromise::Status::Pending: { + JSC::throwTypeError(globalObject, throwScope, "onResolve() doesn't support pending promises yet"_s); + return JSValue::encode({}); + } + case JSPromise::Status::Rejected: { + promise->internalField(JSC::JSPromise::Field::Flags).set(vm, promise, jsNumber(static_cast<unsigned>(JSC::JSPromise::Status::Fulfilled))); + result = promise->result(vm); + return JSValue::encode(result); + } + case JSPromise::Status::Fulfilled: { + result = promise->result(vm); + break; + } + } + } + + if (!result.isObject()) { + JSC::throwTypeError(globalObject, throwScope, "onResolve() expects an object returned"_s); + return JSValue::encode({}); + } + + RELEASE_AND_RETURN(throwScope, JSValue::encode(result)); + } + + return JSValue::encode(JSC::jsUndefined()); +} + +} // namespace Zig + +extern "C" JSC::EncodedJSValue Bun__runOnResolvePlugins(Zig::GlobalObject* globalObject, ZigString* namespaceString, ZigString* path, ZigString* from, BunPluginTarget target) +{ + return globalObject->onResolvePlugins[target].run(globalObject, namespaceString, path, from); +} + +extern "C" JSC::EncodedJSValue Bun__runOnLoadPlugins(Zig::GlobalObject* globalObject, ZigString* namespaceString, ZigString* path, BunPluginTarget target) +{ + return globalObject->onLoadPlugins[target].run(globalObject, namespaceString, path); +} |